@@ -1,2 +1,3 @@
|
||||
export * as signature from 'signature'
|
||||
export * as boxes from 'boxes'
|
||||
export * as JWT from 'jwt'
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
},
|
||||
|
||||
"dependencies": {
|
||||
"jose": "^5.3.0",
|
||||
"result": "git+git@git.pband.ch:typescript/result",
|
||||
"misc": "git+git@git.pband.ch:typescript/misc",
|
||||
"zxcvbn": "^4.4.2"
|
||||
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
import {Result} from 'result'
|
||||
import * as jose from 'jose'
|
||||
|
||||
export type JWTstring = `${string}.${string}.${string}`
|
||||
export type JWTalgorithm = "HS256" | "HS512" | "ES256" | "ES512" | "EdDSA"
|
||||
export type Key = jose.KeyLike | Uint8Array
|
||||
/** KeyPair for asymmetric algorithms, [PrivKey, PubKey] */
|
||||
export type KeyPair = [Key, Key]
|
||||
|
||||
export class JWTcontext {
|
||||
public constructor(
|
||||
private readonly key: Key | KeyPair,
|
||||
private readonly alg: JWTalgorithm,
|
||||
) {}
|
||||
|
||||
public static async gen_key(alg: JWTalgorithm, extractable: boolean = false) : Promise<Key | KeyPair> {
|
||||
switch (alg) {
|
||||
case "HS256":
|
||||
case "HS512":
|
||||
return jose.generateSecret(alg, { extractable })
|
||||
case "EdDSA":
|
||||
case "ES256":
|
||||
case "ES512":
|
||||
{
|
||||
const key = await jose.generateKeyPair(alg)
|
||||
return [key.privateKey, key.publicKey]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async sign<T extends jose.JWTPayload>(message: T, set_issued: boolean = false, exp?: number | string | Date, audience?: string | string[], issuer?: string): Promise<JWT<T>> {
|
||||
let jwt = new jose.SignJWT({payload: message}).setProtectedHeader({ alg: this.alg })
|
||||
|
||||
if (set_issued) jwt = jwt.setIssuedAt()
|
||||
if (issuer !== undefined) jwt = jwt.setIssuer(issuer)
|
||||
if (audience !== undefined) jwt = jwt.setAudience(audience)
|
||||
if (exp !== undefined) jwt = jwt.setExpirationTime(exp)
|
||||
|
||||
const key = this.get_key(true)
|
||||
const res = await jwt.sign(key) as JWTstring
|
||||
|
||||
return new JWT<T>(res)
|
||||
}
|
||||
|
||||
public async verify<T>(jwt: JWT<T>, audience?: string | string[], issuer?: string | string[]): Promise<Result<T>> {
|
||||
const key = this.get_key(false)
|
||||
|
||||
try {
|
||||
let payload = await jose.jwtVerify(jwt.jwt, key)
|
||||
return Result.ok(payload.payload.payload as T)
|
||||
} catch(_) {}
|
||||
|
||||
return Result.error([])
|
||||
}
|
||||
|
||||
private get_key(sign: boolean) : Key {
|
||||
switch (this.alg) {
|
||||
case "HS256":
|
||||
case "HS512":
|
||||
return this.key as Key
|
||||
case "ES256":
|
||||
case "ES512":
|
||||
case "EdDSA":
|
||||
return (this.key as KeyPair)[sign ? 0 : 1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class JWT<T> {
|
||||
public constructor(
|
||||
public readonly _jwt: `${string}.${string}.${string}`
|
||||
) {}
|
||||
|
||||
public get payload() : T {
|
||||
return jose.decodeJwt(this.jwt).payload as T
|
||||
}
|
||||
|
||||
public get jwt() : JWTstring {
|
||||
return this._jwt
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import {beforeAll, expect, test} from 'bun:test'
|
||||
import {JWTcontext, JWTalgorithm} from '../src/jwt'
|
||||
|
||||
const algs: JWTalgorithm[] = ["HS256", "HS512", "ES256", "ES512", "EdDSA"]
|
||||
const contexts: Map<JWTalgorithm, JWTcontext> = new Map()
|
||||
|
||||
beforeAll(async () => {
|
||||
for (const alg of algs) {
|
||||
const key = await JWTcontext.gen_key(alg as JWTalgorithm)
|
||||
expect(key).not.toBeUndefined()
|
||||
|
||||
const context = new JWTcontext(key, alg as JWTalgorithm)
|
||||
contexts.set(alg as JWTalgorithm, context)
|
||||
}
|
||||
})
|
||||
|
||||
test('Base case', async () => {
|
||||
let payload = {
|
||||
yeet: "yaat",
|
||||
lol: "yes"
|
||||
}
|
||||
|
||||
// @ts-expect-error : idc bout your iterator thingy >:(
|
||||
for (const context of contexts.values()) {
|
||||
const jwt = await context.sign(payload, true, "2 days", "pascal", "server")
|
||||
|
||||
const decoded = jwt.payload
|
||||
expect(decoded).toEqual(payload)
|
||||
|
||||
const verified = (await context.verify(jwt, "pascal", "server")).expect("Should verify the JWT")
|
||||
expect(verified).toEqual(payload)
|
||||
}
|
||||
})
|
||||
|
||||
test.todo("Multiple audience can be verified")
|
||||
test.todo("Multiple issuer can be verified")
|
||||
test.todo("Wrong audience is rejected")
|
||||
test.todo("Wrong issuer is rejected")
|
||||
test.todo("Expired JWT is rejected")
|
||||
test.todo("Wrong key won't decrypt")
|
||||
test.todo("tampered JWT are rejected (test 3 parts) (TODO : decode ?)")
|
||||
Reference in New Issue
Block a user