diff --git a/src/JWT.ts b/src/JWT.ts index be46aaab..b64f31f7 100644 --- a/src/JWT.ts +++ b/src/JWT.ts @@ -34,6 +34,14 @@ export interface JWTVerifyOptions { skewTime?: number /** See https://www.w3.org/TR/did-spec-registries/#verification-relationships */ proofPurpose?: ProofPurposeTypes + policies?: JWTVerifyPolicies +} + +export interface JWTVerifyPolicies { + now?: number + nbf?: boolean + iat?: boolean + exp?: boolean } export interface JWSCreationOptions { @@ -141,6 +149,14 @@ export const SUPPORTED_PUBLIC_KEY_TYPES: PublicKeyTypes = { export const SELF_ISSUED_V2 = 'https://self-issued.me/v2' export const SELF_ISSUED_V0_1 = 'https://self-issued.me' +// Exporting errorCodes in a machine readable format rather than human readable format to be used in higher level module +export const INVALID_JWT = 'invalid_jwt' +export const INAVLID_CONFIG = 'invalid_config' +export const INVALID_SIGNATURE = 'invalid_signature' +export const NOT_SUPPORTED = 'not_supported' +export const NO_SUITABLE_KEYS = 'no_suitable_keys' +export const RESOLVE_ERROR = 'resolve_error' + type LegacyVerificationMethod = { publicKey?: string } const defaultAlg = 'ES256K' @@ -319,6 +335,12 @@ export async function verifyJWT( callbackUrl: undefined, skewTime: undefined, proofPurpose: undefined, + policies: { + nbf: undefined, + iat: undefined, + exp: undefined, + now: undefined, + }, } ): Promise { if (!options.resolver) throw new Error('missing_resolver: No DID resolver has been configured') @@ -328,10 +350,13 @@ export async function verifyJWT( ? 'authentication' : undefined : options.proofPurpose + + let did = '' + if (!payload.iss) { throw new Error('invalid_jwt: JWT iss is required') } - let did = '' + if (payload.iss === SELF_ISSUED_V2) { if (!payload.sub) { throw new Error('invalid_jwt: JWT sub is required') @@ -349,9 +374,11 @@ export async function verifyJWT( } else { did = payload.iss } + if (!did) { throw new Error(`invalid_jwt: No DID has been found in the JWT`) } + const { didResolutionResult, authenticators, issuer }: DIDAuthenticator = await resolveAuthenticator( options.resolver, header.alg, @@ -359,18 +386,18 @@ export async function verifyJWT( proofPurpose ) const signer: VerificationMethod = await verifyJWSDecoded({ header, data, signature } as JWSDecoded, authenticators) - const now: number = Math.floor(Date.now() / 1000) + const now: number = options.policies?.now ? options.policies.now : Math.floor(Date.now() / 1000) const skewTime = typeof options.skewTime !== 'undefined' && options.skewTime >= 0 ? options.skewTime : NBF_SKEW if (signer) { const nowSkewed = now + skewTime - if (payload.nbf) { + if (options.policies?.nbf !== false && payload.nbf) { if (payload.nbf > nowSkewed) { throw new Error(`invalid_jwt: JWT not valid before nbf: ${payload.nbf}`) } - } else if (payload.iat && payload.iat > nowSkewed) { + } else if (options.policies?.iat !== false && payload.iat && payload.iat > nowSkewed) { throw new Error(`invalid_jwt: JWT not valid yet (issued in the future) iat: ${payload.iat}`) } - if (payload.exp && payload.exp <= now - skewTime) { + if (options.policies?.exp !== false && payload.exp && payload.exp <= now - skewTime) { throw new Error(`invalid_jwt: JWT has expired: exp: ${payload.exp} < now: ${now}`) } if (payload.aud) { diff --git a/src/__tests__/JWT.test.ts b/src/__tests__/JWT.test.ts index 9883af7d..15331fc6 100644 --- a/src/__tests__/JWT.test.ts +++ b/src/__tests__/JWT.test.ts @@ -462,6 +462,29 @@ describe('verifyJWT()', () => { // const jwt = await createJWT({nbf:FUTURE},{issuer:did,signer}) await expect(verifyJWT(jwt, { resolver })).rejects.toThrowError() }) + it('passes when nbf is in the future and policy for nbf is false', async () => { + expect.assertions(2) + // const jwt = await createJWT({nbf:FUTURE},{issuer:did,signer}) + + const jwt = await createJWT({ requested: ['name', 'phone'], nbf: new Date().getTime() + 1000000 }, { issuer: did, signer }) + expect(verifier.verify(jwt)).toBe(true) + + const { payload } = await verifyJWT(jwt, { resolver, policies: { nbf: false } }) + return expect(payload).toBeDefined() + }) + + it('passes when nbf is in the future and now is provided to be higher than nbf', async () => { + expect.assertions(2) + // const jwt = await createJWT({nbf:FUTURE},{issuer:did,signer}) + + const jwt = await createJWT({ requested: ['name', 'phone'], nbf: new Date().getTime() + 10000 }, { issuer: did, signer }) + expect(verifier.verify(jwt)).toBe(true) + + const { payload } = await verifyJWT(jwt, { resolver, policies: { now: new Date().getTime() + 100000 } }) + return expect(payload).toBeDefined() + }) + + it('fails when nbf is in the future and iat is in the past', async () => { expect.assertions(1) const jwt = @@ -469,6 +492,15 @@ describe('verifyJWT()', () => { // const jwt = await createJWT({nbf:FUTURE,iat:PAST},{issuer:did,signer}) await expect(verifyJWT(jwt, { resolver })).rejects.toThrowError() }) + it('passes when nbf is in the future and iat is in the past with nbf policy false', async () => { + expect.assertions(1) + const jwt = + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJpYXQiOjE0ODUyNjExMzMsIm5iZiI6MTQ4NTM4MTEzMywiaXNzIjoiZGlkOmV0aHI6MHhmM2JlYWMzMGM0OThkOWUyNjg2NWYzNGZjYWE1N2RiYjkzNWIwZDc0In0.JjEn_huxI9SsBY_3PlD0ShpXvrRgUGFDKAgxJBc1Q5GToVpUTw007-o9BTt7JNi_G2XWmcu2aXXnDn0QFsRIrg' + // const jwt = await createJWT({nbf:FUTURE,iat:PAST},{issuer:did,signer}) + + const { payload } = await verifyJWT(jwt, { resolver, policies: { nbf: false } }) + expect(payload).toBeDefined(); + }) it('passes when nbf is missing and iat is in the past', async () => { expect.assertions(1) const jwt = @@ -483,6 +515,14 @@ describe('verifyJWT()', () => { // const jwt = await createJWT({iat:FUTURE},{issuer:did,signer}) await expect(verifyJWT(jwt, { resolver })).rejects.toThrowError() }) + it('passes when nbf is missing and iat is in the future with iat policy to be false', async () => { + expect.assertions(1) + const jwt = + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJpYXQiOjE0ODUzODExMzMsImlzcyI6ImRpZDpldGhyOjB4ZjNiZWFjMzBjNDk4ZDllMjY4NjVmMzRmY2FhNTdkYmI5MzViMGQ3NCJ9.FJuHvf9Tby7b4I54Cm1nh8CvLg4QH2wt2K0WfyQaLqlr3NKKI5hAdLalgZksI25gLhNrZwQFnC-nzEOs9PI1SQ' + // const jwt = await createJWT({iat:FUTURE},{issuer:did,signer}) + const { payload } = await verifyJWT(jwt, { resolver, policies: { iat: false }}) + return expect(payload).toBeDefined() + }) it('passes when nbf and iat are both missing', async () => { expect.assertions(1) const jwt = @@ -586,12 +626,26 @@ describe('verifyJWT()', () => { await expect(verifyJWT(jwt, { resolver })).rejects.toThrowError(/JWT has expired/) }) + it('accepts an expired JWT with exp policy false', async () => { + expect.assertions(1) + const jwt = await createJWT({ exp: NOW - NBF_SKEW - 1 }, { issuer: did, signer }) + const { payload } = await verifyJWT(jwt, { resolver, policies: { exp: false } }) + return expect(payload).toBeDefined() + }) + it('rejects an expired JWT without skew time', async () => { expect.assertions(1) const jwt = await createJWT({ exp: NOW - 1 }, { issuer: did, signer }) await expect(verifyJWT(jwt, { resolver, skewTime: 0 })).rejects.toThrowError(/JWT has expired/) }) + it('accepts an expired JWT without skew time but exp policy false', async () => { + expect.assertions(1) + const jwt = await createJWT({ exp: NOW - 1 }, { issuer: did, signer }) + const { payload } = await verifyJWT(jwt, { resolver, skewTime: 0, policies: { exp: false } }) + return expect(payload).toBeDefined(); + }) + it('accepts a valid audience', async () => { expect.assertions(1) const jwt = await createJWT({ aud }, { issuer: did, signer }) @@ -685,7 +739,6 @@ describe('verifyJWT()', () => { const { payload } = await verifyJWT(jwt, { resolver }) return expect(payload).toBeDefined() }) - it('rejects a self-issued v2 JWT (sub type: jkt) without a header.kid DID', async () => { expect.assertions(1) const jwt = await createJWT({ sub: 'sub', sub_jwk: {} }, { issuer: SELF_ISSUED_V2, signer })