Implement JWT

This commit is contained in:
2024-05-21 16:00:20 +02:00
parent d845ae1391
commit 4b94352d8b
3 changed files with 141 additions and 0 deletions
View File
+101
View File
@@ -0,0 +1,101 @@
import {Result} from 'rust'
import * as jose from 'jose'
import logger from 'log'
const log = logger('crypto:jwt')
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> {
log.trace(`Generate new ${alg} key`)
log.debug('Key extractable :', extractable ? 'yes' : 'no')
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>> {
log.trace('sign JWT')
log.debug('Config :', {
set_issued,
exp,
issuer,
})
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>> {
log.trace('Verify JWT')
log.debug('Issuers :', issuer)
const key = this.get_key(false)
try {
let payload = await jose.jwtVerify(jwt.jwt, key)
return Result.ok(payload.payload.payload as T)
} catch(e) {
log.warn('JWT verification failed')
log.debug(`Error : ${e}`)
}
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 {
log.trace("Decode payload of JWT")
return jose.decodeJwt(this.jwt).payload as T
}
public get jwt() : JWTstring {
return this._jwt
}
}
+40
View File
@@ -0,0 +1,40 @@
import {beforeAll, expect, test} from 'bun:test'
import {JWTcontext, type 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"
}
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 ?)")