diff --git a/bun.lockb b/bun.lockb index c875dfc..08be1dc 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index d4e53f1..03cea79 100644 --- a/package.json +++ b/package.json @@ -28,10 +28,12 @@ "auth", "authentication" ], + "packageManager": "pnpm@8.6.0", "license": "MIT", "scripts": { "dev": "bun run --hot example/index.ts", "test": "bun test && npm run test:node", + "postinstall": "if [ ! -d dist ] ; then bun install --dev --ignore-scripts || npm install --dev --ignore-scripts; npm run build; fi;", "test:node": "npm install --prefix ./test/node/cjs/ && npm install --prefix ./test/node/esm/ && node ./test/node/cjs/index.js && node ./test/node/esm/index.js", "build": "rimraf dist && tsc --project tsconfig.esm.json && tsc --project tsconfig.cjs.json", "release": "npm run build && npm run test && npm publish --access public" @@ -45,12 +47,12 @@ "@types/node": "^20.1.4", "@typescript-eslint/eslint-plugin": "^6.6.0", "@typescript-eslint/parser": "^6.6.0", - "elysia": "1.0.2", + "elysia": "1.0.13", "eslint": "^8.40.0", "rimraf": "4.3", "typescript": "^5.1.6" }, "peerDependencies": { - "elysia": ">= 1.0.2" + "elysia": ">= 1.0.13" } } \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 0e70509..768476e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,7 +5,11 @@ import { jwtVerify, type JWTPayload, type JWSHeaderParameters, - type KeyLike + type KeyLike, + errors, + type JWTVerifyGetKey, + type JWTVerifyOptions, + type JWTVerifyResult, } from 'jose' import { Type as t } from '@sinclair/typebox' @@ -30,7 +34,7 @@ export interface JWTOption< Name extends string | undefined = 'jwt', Schema extends TSchema | undefined = undefined > extends JWSHeaderParameters, - Omit { + Omit { /** * Name to decorate method as * @@ -53,7 +57,7 @@ export interface JWTOption< /** * JWT Secret */ - secret: string | Uint8Array | KeyLike + secret: string | Uint8Array | KeyLike | JWTVerifyGetKey /** * Type strict validation for JWT payload */ @@ -74,6 +78,15 @@ export interface JWTOption< exp?: string | number } +const verifier = (key: Uint8Array | KeyLike | JWTVerifyGetKey): { + (jwt: string, options?: JWTVerifyOptions): Promise +} => { + return typeof key === 'function' + ? (jwt, options) => jwtVerify(jwt, key, options) + : (jwt, options) => jwtVerify(jwt, key, options) + ; +} + export const jwt = < const Name extends string = 'jwt', const Schema extends TSchema | undefined = undefined @@ -90,49 +103,55 @@ export const jwt = < exp, ...payload }: // End JWT Payload -JWTOption) => { + JWTOption +) => (app: Elysia) => { if (!secret) throw new Error("Secret can't be empty") const key = typeof secret === 'string' ? new TextEncoder().encode(secret) : secret - + const verifyKey = verifier(key); const validator = schema ? getSchemaValidator( - t.Intersect([ - schema, - t.Object({ - iss: t.Optional(t.String()), - sub: t.Optional(t.String()), - aud: t.Optional( - t.Union([t.String(), t.Array(t.String())]) - ), - jti: t.Optional(t.String()), - nbf: t.Optional(t.Union([t.String(), t.Number()])), - exp: t.Optional(t.Union([t.String(), t.Number()])), - iat: t.Optional(t.String()) - }) - ]), - {} - ) + t.Intersect([ + schema, + t.Object({ + iss: t.Optional(t.String()), + sub: t.Optional(t.String()), + aud: t.Optional( + t.Union([t.String(), t.Array(t.String())]) + ), + jti: t.Optional(t.String()), + nbf: t.Optional(t.Union([t.String(), t.Number()])), + exp: t.Optional(t.Union([t.String(), t.Number()])), + iat: t.Optional(t.String()) + }) + ]), + {} + ) : undefined - return new Elysia({ - name: '@elysiajs/jwt', - seed: { - name, - secret, - alg, - crit, - schema, - nbf, - exp, - ...payload - } - }).decorate(name as Name extends string ? Name : 'jwt', { + // return new Elysia({ + // name: '@elysiajs/jwt', + // seed: { + // name, + // secret, + // alg, + // crit, + // schema, + // nbf, + // exp, + // ...payload + // } + // }) + return app.decorate(name as Name extends string ? Name : 'jwt', { sign: ( morePayload: UnwrapSchema> & JWTPayloadSpec ) => { + if (typeof key === 'function') { + throw new TypeError('Cannot use that secret to sign, likely only verify.'); + } + let jwt = new SignJWT({ ...payload, ...morePayload, @@ -149,16 +168,40 @@ JWTOption) => { return jwt.sign(key) }, verify: async ( - jwt?: string + jwt?: string, + options?: JWTVerifyOptions, ): Promise< | (UnwrapSchema> & - JWTPayloadSpec) + JWTPayloadSpec) | false > => { if (!jwt) return false try { - const data: any = (await jwtVerify(jwt, key)).payload + // note: this is to satisfy typescript. + const data: any = ( + await (verifyKey(jwt, options) + .catch(async (error) => { + if (error?.code === 'ERR_JWKS_MULTIPLE_MATCHING_KEYS') { + for await (const publicKey of error) { + try { + return await jwtVerify(jwt, publicKey, options) + } + catch (innerError: any) { + if ('code' in innerError && innerError?.code === 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED') { + continue; + } + + throw innerError + } + } + + throw new errors.JWSSignatureVerificationFailed() + } + + throw error + })) + ).payload if (validator && !validator!.Check(data)) throw new ValidationError('JWT', validator, data)