Repository: decentralized-identity/did-jwt
Pull request #241 (2022-08-10 13:39:38)

Introduced Policies to override Timestamp checking
## Pull Request to Introduce Policies for VerifyJWT

### Description
This merge requests introduces a new interface in the JWT.ts module which JWTVerifyPolicies. The policies at the moment are only restricted to timestamps, but can be further expanded upon to serve better. The JWTVerifyPolicies are made a part of VerifyJWTOptions defined as `policies`. These policies are used to be able to override a certain checks that are made in the VerifyJWT command. It is not a breaking change but an extension to the current implementation.

### Policies
The checks for these policies will not me made only and only if they are explicitly stated to be 
- nbf  (Not Before)
- iat   (Issued At)
- exp (Expires At)

### Tests
Further tests were added to the JWT.test.ts to check whether the implemented policies were properly allowing the presentation to verify in case of a failure and the policy was made to be overridden. The following cases are covered:
- nbf is in the future and policy for nbf is false
- nbf is in the future and iat is in the past with nbf policy false
- expired JWT with exp policy false
- expired JWT without skew time but exp policy false
- iat is in the future with iat policy false


daniyal-khalil: @mirceanis 
Thank you for such a fast response, I have added the `now` policy which looks at all the timestamp checks, if it is set to false, the verification will not look at the timestamps at all. Similarly, as mentioned above, `now` can be true, while `nbf` can still be false. Let me know if I misunderstood anything regarding this, Thank you so much!

Patch:
diff --git a/src/JWT.ts b/src/JWT.ts
index be46aaa..b64f31f 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<JWTVerified> {
   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) {


Test patch:
diff --git a/src/__tests__/JWT.test.ts b/src/__tests__/JWT.test.ts
index 9883af7..15331fc 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 })