diff --git a/bun.lockb b/bun.lockb index c875dfc..b5854e0 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index d4e53f1..389cfb3 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "release": "npm run build && npm run test && npm publish --access public" }, "dependencies": { - "jose": "^4.14.4" + "jose": "^5.2.3" }, "devDependencies": { "@elysiajs/cookie": "^0.3.0", diff --git a/src/index.ts b/src/index.ts index 0e70509..86cba34 100644 --- a/src/index.ts +++ b/src/index.ts @@ -52,8 +52,19 @@ export interface JWTOption< name?: Name /** * JWT Secret + * Only `secret` or both `privateKey`, `publicKey` must be set */ - secret: string | Uint8Array | KeyLike + secret?: string | Uint8Array | KeyLike + /** + * JWT Private Key + * Only `secret` or both `privateKey`, `publicKey` must be set + */ + privateKey?: Uint8Array | KeyLike + /** + * JWT Public Key + * Only `secret` or both `privateKey`, `publicKey` must be set + */ + publicKey?: Uint8Array | KeyLike /** * Type strict validation for JWT payload */ @@ -80,6 +91,8 @@ export const jwt = < >({ name = 'jwt' as Name, secret, + publicKey, + privateKey, // Start JWT Header alg = 'HS256', crit, @@ -91,11 +104,29 @@ export const jwt = < ...payload }: // End JWT Payload JWTOption) => { - if (!secret) throw new Error("Secret can't be empty") - const key = typeof secret === 'string' ? new TextEncoder().encode(secret) : secret + let asymmetric = false + + if (secret && (privateKey || publicKey)) { + throw new Error("When using asymmetric algorithm, only `privateKey` and `publicKey` is accepted") + } + + if (privateKey && !publicKey) { + throw new Error("When using asymmetric algorithm, both `privateKey` and `publicKey` must be set. Public key is missing") + } + + if (publicKey && !privateKey) { + throw new Error("When using asymmetric algorithm, both `privateKey` and `publicKey` must be set. Private key is missing") + } + + if (privateKey && privateKey) { + asymmetric = true + } else if (!secret) { + throw new Error("Secret can't be empty") + } + const validator = schema ? getSchemaValidator( t.Intersect([ @@ -146,7 +177,7 @@ JWTOption) => { if (nbf) jwt = jwt.setNotBefore(nbf) if (exp) jwt = jwt.setExpirationTime(exp) - return jwt.sign(key) + return jwt.sign(asymmetric ? privateKey! : key!) }, verify: async ( jwt?: string @@ -158,7 +189,7 @@ JWTOption) => { if (!jwt) return false try { - const data: any = (await jwtVerify(jwt, key)).payload + const data: any = (await jwtVerify(jwt, asymmetric ? publicKey! : key!)).payload if (validator && !validator!.Check(data)) throw new ValidationError('JWT', validator, data) diff --git a/test/index.test.ts b/test/index.test.ts index 12faa59..971cd5d 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -1,9 +1,9 @@ import { Elysia, t } from 'elysia' +import { importJWK } from 'jose' import { jwt } from '../src' import { describe, expect, it } from 'bun:test' -const req = (path: string) => new Request(`http://localhost${path}`) const post = (path: string, body = {}) => new Request(`http://localhost${path}`, { method: 'POST', @@ -14,8 +14,22 @@ const post = (path: string, body = {}) => }) describe('Static Plugin', () => { + async function signTest() { + const name = 'Shirokami' + + const _sign = post('/sign', { name }) + await _sign.text() + + const _verified = post('/verify', { name }) + const signed = (await _verified.json()) as { + name: string + } + + expect(name).toBe(signed.name) + } + it('sign JWT', async () => { - const app = new Elysia() + new Elysia() .use( jwt({ name: 'jwt', @@ -31,16 +45,31 @@ describe('Static Plugin', () => { body: t.Object({ name: t.String() }) }) - const name = 'Shirokami' - - const _sign = post('/sign', { name }) - const token = await _sign.text() + await signTest() + }) + it('sign JWT (asymmetric)', async () => { + const crv = 'Ed25519' + const d = 'N3cOzsFZwiIbtNiBYQP9bcbcTIdkITC8a4iRslrbW7Q' + const x = 'RjnTe-mqZcVls6SQ5CgW0X__jRaa-Quj5HBDREzVLhc' + const kty = 'OKP' - const _verified = post('/verify', { name }) - const signed = (await _verified.json()) as { - name: string - } + new Elysia() + .use( + jwt({ + name: 'jwt', + privateKey: await importJWK({ crv, d, x, kty }, 'EdDSA'), + publicKey: await importJWK({ crv, x, kty }, 'EdDSA') + }) + ) + .post('/validate', ({ jwt, body }) => jwt.sign(body), { + body: t.Object({ + name: t.String() + }) + }) + .post('/validate', ({ jwt, body: { name } }) => jwt.verify(name), { + body: t.Object({ name: t.String() }) + }) - expect(name).toBe(signed.name) - }) + await signTest() + }) })