Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
72f118f
Some tests pass.
simonmika Dec 8, 2024
90a3226
Cleanup.
simonmika Dec 8, 2024
7e396f1
type fixes
EliasEriksson Dec 9, 2024
74fb749
type fix in encoder/decoder
EliasEriksson Dec 9, 2024
5b73233
removed commented code
EliasEriksson Dec 9, 2024
8d7eca4
supports promises
EliasEriksson Dec 9, 2024
364ccf5
reimplemented one test
EliasEriksson Dec 9, 2024
7852c0c
Encrypter in place but having issues running tests.
simonmika Dec 9, 2024
6c3dc14
Fixed issue with newer cryptly.
simonmika Dec 9, 2024
c47d266
updated typedly. tests can run
EliasEriksson Dec 12, 2024
90af2e4
encrypt / decrypt with processor
EliasEriksson Dec 12, 2024
b863546
WIP
EliasEriksson Dec 12, 2024
a862f3b
added required claims
EliasEriksson Dec 13, 2024
c887b62
progress
EliasEriksson Dec 13, 2024
77d2c1e
WIP
EliasEriksson Dec 16, 2024
eb2c1d0
works?
EliasEriksson Dec 17, 2024
cbf7618
removed old files
EliasEriksson Dec 17, 2024
8999f0a
issuing + verifying + encryptor workflow
EliasEriksson Dec 17, 2024
21c2609
file restructure
EliasEriksson Dec 17, 2024
7619662
fixed relevant TODOs
EliasEriksson Dec 17, 2024
33245c0
WIP
EliasEriksson Dec 17, 2024
0edb326
naming
EliasEriksson Dec 17, 2024
59b6831
naming
EliasEriksson Dec 17, 2024
b9c08c1
renamed fixtures to test
EliasEriksson Dec 17, 2024
8c75203
payload
EliasEriksson Dec 17, 2024
4271aef
tests passing
EliasEriksson Dec 19, 2024
6b82085
formatting
EliasEriksson Dec 19, 2024
905563e
fixed some TODOs
EliasEriksson Dec 20, 2024
a9e6eda
removed promisify
EliasEriksson Dec 20, 2024
827ae01
latest isly and skip lib checks
EliasEriksson Dec 20, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"authly",
"cloudly",
"cryptly",
"Encrypter",
"flagly",
"gracely",
"isly",
Expand All @@ -58,6 +59,8 @@
"smoothly",
"tidily",
"transactly",
"typedly",
"uply"
]
],
"typescript.tsdk": "node_modules/typescript/lib"
}
26 changes: 8 additions & 18 deletions Actor.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,12 @@
import * as Property from "./Property"
export class Actor<T extends Actor<T>> {
protected readonly transformers: Property.Transformer[] = []
constructor(readonly id?: string) {}
import { isoly } from "isoly"
import { Processor } from "./Processor"

add(...argument: (Property.Configuration | Property.Transformer | undefined)[]): T {
argument.forEach(
value =>
value && this.transformers.push(Property.Configuration.is(value) ? this.creatableToTransformer(value) : value)
)
return this as unknown as T
}

private creatableToTransformer(creatable: Property.Configuration): Property.Transformer {
return Property.Converter.Configuration.is(creatable)
? new Property.Converter(creatable)
: Property.Crypto.Configuration.is(creatable)
? Property.Crypto.create(creatable[0], ...creatable.slice(1))
: new Property.Renamer(creatable)
export class Actor<T extends Processor.Type.Constraints<T>> {
protected constructor(protected readonly processor: Processor<T>) {}
protected time(): number {
const time = (this.constructor as typeof Actor).staticTime ?? Actor.staticTime
return typeof time == "number" ? time : Math.floor(isoly.DateTime.epoch(time ? time : isoly.DateTime.now()) / 1_000)
}
static staticTime?: isoly.Date | number
}
export namespace Actor {}
2 changes: 1 addition & 1 deletion Algorithm/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ describe("Algorithm.RS256", () => {
"amount=2050&currency=EUR&ip=1.1.1.1&card[pan]=4111111111111111&card[expire_month]=06&card[expire_year]=2022&card[csc]=123"
)
: ""
const hexadecimalSignature = cryptly.Identifier.toHexadecimal(signature)
const hexadecimalSignature = cryptly.Identifier.toBase16(signature)
expect(hexadecimalSignature).toEqual(
"881de666d7f61c796c1c900784901b1b9bb8298af26bf10f79b2fd59231ab3383d9183ea8120bac3abead1000c4fab63bf6cf1345a31209acfe910c9286e9a3db7d4d15d1ae8e279ef05d5e43184b608ad39741ab04e28095fb8a0c48974e55fd0fe58ac5ef1f6c16670f67fd1c1b9a2a06273b079dc29e0daef6319a2e63545"
)
Expand Down
59 changes: 16 additions & 43 deletions Algorithm/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,16 @@ export class Algorithm {
return this.signer.verify(data, signature)
}

static create(name: "none"): Algorithm | undefined
static create(name: "none"): Algorithm
static create(name: AlgorithmName.Symmetric, key: Uint8Array | string): Algorithm
static create(name: AlgorithmName.Symmetric, key: Uint8Array | string | undefined): Algorithm | undefined
static create(name: AlgorithmName.Symmetric, key: Uint8Array | string | undefined): Algorithm
static create(
name: AlgorithmName.Asymmetric,
publicKey: Uint8Array | string | undefined,
privateKey?: Uint8Array | string
): Algorithm | undefined
static create(name: AlgorithmName, ...keys: (string | Uint8Array)[]): Algorithm | undefined {
let result: cryptly.Signer | undefined
): Algorithm
static create(name: AlgorithmName, ...keys: (string | Uint8Array)[]): Algorithm {
let result: cryptly.Signer
switch (name) {
case "ES256":
result = cryptly.Signer.create("ECDSA", "SHA-256", keys[0], keys[1])
Expand Down Expand Up @@ -71,9 +71,9 @@ export class Algorithm {
result = cryptly.Signer.create("None")
break
}
return result && new Algorithm(name, result)
return new Algorithm(name, result)
}
static none(): Algorithm | undefined {
static none(): Algorithm {
return Algorithm.create("none")
}
static HS256(key: Uint8Array | string): Algorithm
Expand All @@ -92,58 +92,31 @@ export class Algorithm {
return Algorithm.create("HS512", key)
}

static RS256(
publicKey: Uint8Array | string | undefined,
privateKey?: Uint8Array | string | undefined
): Algorithm | undefined {
static RS256(publicKey: Uint8Array | string | undefined, privateKey?: Uint8Array | string | undefined): Algorithm {
return Algorithm.create("RS256", publicKey, privateKey)
}
static RS384(
publicKey: Uint8Array | string | undefined,
privateKey?: Uint8Array | string | undefined
): Algorithm | undefined {
static RS384(publicKey: Uint8Array | string | undefined, privateKey?: Uint8Array | string | undefined): Algorithm {
return Algorithm.create("RS384", publicKey, privateKey)
}
static RS512(
publicKey: Uint8Array | string | undefined,
privateKey?: Uint8Array | string | undefined
): Algorithm | undefined {
static RS512(publicKey: Uint8Array | string | undefined, privateKey?: Uint8Array | string | undefined): Algorithm {
return Algorithm.create("RS512", publicKey, privateKey)
}
static ES256(
publicKey: Uint8Array | string | undefined,
privateKey?: Uint8Array | string | undefined
): Algorithm | undefined {
static ES256(publicKey: Uint8Array | string | undefined, privateKey?: Uint8Array | string | undefined): Algorithm {
return Algorithm.create("ES256", publicKey, privateKey)
}
static ES384(
publicKey: Uint8Array | string | undefined,
privateKey?: Uint8Array | string | undefined
): Algorithm | undefined {
static ES384(publicKey: Uint8Array | string | undefined, privateKey?: Uint8Array | string | undefined): Algorithm {
return Algorithm.create("ES384", publicKey, privateKey)
}
static ES512(
publicKey: Uint8Array | string | undefined,
privateKey?: Uint8Array | string | undefined
): Algorithm | undefined {
static ES512(publicKey: Uint8Array | string | undefined, privateKey?: Uint8Array | string | undefined): Algorithm {
return Algorithm.create("ES512", publicKey, privateKey)
}
static PS256(
publicKey: Uint8Array | string | undefined,
privateKey?: Uint8Array | string | undefined
): Algorithm | undefined {
static PS256(publicKey: Uint8Array | string | undefined, privateKey?: Uint8Array | string | undefined): Algorithm {
return Algorithm.create("PS256", publicKey, privateKey)
}
static PS384(
publicKey: Uint8Array | string | undefined,
privateKey?: Uint8Array | string | undefined
): Algorithm | undefined {
static PS384(publicKey: Uint8Array | string | undefined, privateKey?: Uint8Array | string | undefined): Algorithm {
return Algorithm.create("PS384", publicKey, privateKey)
}
static PS512(
publicKey: Uint8Array | string | undefined,
privateKey?: Uint8Array | string | undefined
): Algorithm | undefined {
static PS512(publicKey: Uint8Array | string | undefined, privateKey?: Uint8Array | string | undefined): Algorithm {
return Algorithm.create("PS512", publicKey, privateKey)
}
}
Expand Down
26 changes: 26 additions & 0 deletions Claims.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { isly } from "isly"

/**
* JWT public claim names
* https://datatracker.ietf.org/doc/html/rfc7519#section-4.1
*/
export interface Claims {
aud?: string // audience
iss?: string // issuer
iat?: number // issued at
sub?: string // subject
exp?: number // expires
nbf?: number // not before
jti?: string // JWT id
}
export namespace Claims {
export const type = isly.object<Claims>({
aud: isly.string().optional(),
iss: isly.string().optional(),
iat: isly.number().optional(),
sub: isly.string().optional(),
exp: isly.number().optional(),
nbf: isly.number().optional(),
jti: isly.string().optional(),
})
}
28 changes: 28 additions & 0 deletions Issuer.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { authly } from "./index"
import { Test } from "./Test"

authly.Actor.staticTime = Test.times.issued

describe("authly.Issuer", () => {
const issuer: authly.Issuer<Test.Type> = authly.Issuer.create(
Test.configuration,
"issuer",
"audience",
authly.Algorithm.RS256(Test.keys.public, Test.keys.private)
)
it("staticTime", async () => {
expect(authly.Issuer.staticTime).toEqual(Test.times.issued)
})
it("signing", async () => {
expect(await issuer.sign(Test.payload)).toEqual(Test.token.signed)
})
it("unsigned", async () => {
const issuer: authly.Issuer<Test.Type> = authly.Issuer.create(
Test.configuration,
"issuer",
"audience",
authly.Algorithm.none()
)
expect(await issuer.sign(Test.payload)).toEqual(Test.token.unsigned)
})
})
119 changes: 75 additions & 44 deletions Issuer.ts
Original file line number Diff line number Diff line change
@@ -1,57 +1,88 @@
import { cryptly } from "cryptly"
import { isoly } from "isoly"
import { Actor } from "./Actor"
import { Algorithm } from "./Algorithm"
import { Header } from "./Header"
import { Payload } from "./Payload"
import { Processor } from "./Processor"
import { Token } from "./Token"

export class Issuer<T extends Payload> extends Actor<Issuer<T>> {
audience?: string | string[]
/** Duration in seconds */
duration?: number
get header(): Header {
return {
alg: this.algorithm.name,
typ: "JWT",
...(this.algorithm.kid && { kid: this.algorithm.kid }),
}
export class Issuer<T extends Processor.Type.Constraints<T>> extends Actor<T> {
private readonly header: Header
private constructor(
processor: Processor<T>,
private readonly issuer: string,
private readonly audience: string,
readonly algorithm: Algorithm
) {
super(processor)
this.header = { alg: algorithm.name, typ: "JWT", ...(algorithm.kid && { kid: algorithm.kid }) }
}
get payload(): Payload {
const result: Payload = { iss: this.id, iat: Issuer.issuedAt }
if (this.audience)
result.aud = this.audience
if (this.duration && result.iat)
result.exp = result.iat + this.duration
return result
private async process(claims: Processor.Type.Payload<T>): Promise<Processor.Type.Claims<T>> {
return await this.processor.encode(claims)
}
private constructor(issuer: string, readonly algorithm: Algorithm) {
super(issuer)
private issued(issued: isoly.DateTime | number): number {
return typeof issued == "number" ? issued : isoly.DateTime.epoch(issued, "seconds")
}
async sign(payload: T, issuedAt?: Date | number): Promise<Token | undefined> {
payload = { ...this.payload, ...payload }
if (issuedAt)
payload.iat = typeof issuedAt == "number" ? issuedAt : issuedAt.getTime() / 1000
const transformed = await this.transformers.reduce(async (p, c) => c.apply(await p), Promise.resolve(payload))
const data =
transformed &&
`${cryptly.Base64.encode(new TextEncoder().encode(JSON.stringify(this.header)), "url")}.${cryptly.Base64.encode(
new TextEncoder().encode(JSON.stringify(transformed)),
"url"
)}`
return data && `${data}.${await this.algorithm.sign(data)}`
async sign(payload: Processor.Type.Payload.Creatable<T>, { ...options }: Issuer.Options = {}): Promise<Token> {
payload = {
...payload,
...(await (async name => ({
[name]: (
(await this.processor.decode({
iat: options.issued == undefined ? this.time() : this.issued(options.issued),
} as Processor.Type.Claims<T>)) as Processor.Type.Payload<T>
)[name],
}))(this.processor.name("iat"))),
...(await (async name => ({
[name]: (
(await this.processor.decode({
iss: this.issuer,
} as Processor.Type.Claims<T>)) as Processor.Type.Payload<T>
)[name],
}))(this.processor.name("iss"))),
...(await (async name => ({
[name]: (
(await this.processor.decode({
aud: this.audience,
} as Processor.Type.Claims<T>)) as Processor.Type.Payload<T>
)[name],
}))(this.processor.name("aud"))),
}
const encoder = new TextEncoder()
const result = `${cryptly.Base64.encode(
encoder.encode(JSON.stringify(this.header)),
"url"
)}.${cryptly.Base64.encode(
encoder.encode(JSON.stringify(await this.process(payload as any as Processor.Type.Payload<T>))),
"url"
)}`
return `${result}.${await this.algorithm.sign(result)}`
}
private static get issuedAt(): number {
return Issuer.defaultIssuedAt == undefined
? Math.floor(Date.now() / 1000)
: typeof Issuer.defaultIssuedAt == "number"
? Issuer.defaultIssuedAt
: Math.floor(Issuer.defaultIssuedAt.getTime() / 1000)
static create<T extends Processor.Type.Constraints<T>>(
configuration: Processor.Configuration<T>,
issuer: string,
audience: string,
algorithm: Algorithm
): Issuer<T>
static create<T extends Processor.Type.Constraints<T>>(
processor: Processor<T>,
issuer: string,
audience: string,
algorithm: Algorithm
): Issuer<T>
static create<T extends Processor.Type.Constraints<T>>(
source: Processor<T> | Processor.Configuration<T>,
issuer: string,
audience: string,
algorithm: Algorithm
): Issuer<T> {
return source instanceof Processor
? new this(source, issuer, audience, algorithm)
: this.create(Processor.create(source), issuer, audience, algorithm)
}
static defaultIssuedAt: undefined | Date | number
static create<T extends Payload>(issuer: string, algorithm: Algorithm): Issuer<T>
static create<T extends Payload>(issuer: string, algorithm: Algorithm | undefined): Issuer<T> | undefined
static create<T extends Payload>(issuer: string, algorithm: Algorithm | undefined): Issuer<T> | undefined {
return (algorithm && new Issuer(issuer, algorithm)) || undefined
}
export namespace Issuer {
export interface Options {
issued?: isoly.DateTime | number
}
}
export namespace Issuer {}
14 changes: 0 additions & 14 deletions Payload.ts

This file was deleted.

20 changes: 20 additions & 0 deletions Processor/Configuration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { typedly } from "typedly"
import { Converter } from "./Converter"
import { Type } from "./Type"

export type Configuration<T extends Type.Constraints<T> = Type.Standard> = {
[Claim in keyof T]: Configuration.Property<T, Claim>
}
export namespace Configuration {
export type Property<T extends Type.Constraints<T>, P extends keyof T> = {
name: T[P]["name"]
encode: (
value: T[P]["original"],
context: Converter.Context.Encode<Type.Payload<T>, Type.Claims<T>>
) => typedly.Promise.Maybe<T[P]["encoded"]>
decode: (
value: T[P]["encoded"],
context: Converter.Context.Decode<Type.Payload<T>, Type.Claims<T>>
) => typedly.Promise.Maybe<T[P]["original"]>
}
}
Loading
Loading