Repository: decentralized-identity/did-jwt (ts)

Issue:
[proposal] Improvements to the verification API
**Is your feature request related to a problem? Please describe.**

The verification of JWT/JWS uses exceptions to signal failures, but this creates a problem in distinguishing between legitimate failures (bad signature, invalid audience, timestamp issue, etc) and exceptional situations (network timeout while resolving DID, malformed input, unsupported algorithm, etc).

I propose separating these 2 scenarios internally so that users get a better idea about what failed and so that control flow for invalid JWTs no longer relies on parsing error messages.

**Describe the solution you'd like**
The result of `verifyJWT()` should be an object with a `verified` property set to true only when signature, timestamps, and audience are valid, and false otherwise.
When one of the validation criteria fails, the reason can be returned as an error code with a message.
`verifyJWT()` should not throw errors.

The verification options should accept a `now` parameter for checks involving past/future validity, or allow the user to disable timestamp checking. Similarly, it should be possible to skip audience checking (although, this is already indirectly possible by pre-inspecting the desired audience).

**Describe alternatives you've considered**
The desired behavior can already be achieved by calling `verifyJWS` directly for signature checks and by performing timestamp and audience checking out of band, but that defeats the purpose of having a library to do this.

Another acceptable solution would be to introduce a new method (`verifyJWTSafe`) that can give detailed reasons for failure without using exceptions for flow control, while leaving the existing `verifyJWT` method intact for backward compatibility.

Please add your opinion about this subject if you have one.

Pull request description:
feat: add `aud` override policy for verification
fixes #239 
.. without the mentioned breaking change.
Errors are still thrown, but the prefixes for the known failure cases are also exported now as an enum.

Patch:
diff --git a/README.md b/README.md
index b455992..607efc3 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
 [![npm](https://img.shields.io/npm/dt/did-jwt.svg)](https://www.npmjs.com/package/did-jwt)
 [![npm](https://img.shields.io/npm/v/did-jwt.svg)](https://www.npmjs.com/package/did-jwt)
-[![Twitter Follow](https://img.shields.io/twitter/follow/uport_me.svg?style=social&label=Follow)](https://twitter.com/uport_me)
+[![Twitter Follow](https://img.shields.io/twitter/follow/veramolabs.svg?style=social&label=Follow)](https://twitter.com/veramolabs)
 [![codecov](https://codecov.io/gh/decentralized-identity/did-jwt/branch/master/graph/badge.svg)](https://codecov.io/gh/decentralized-identity/did-jwt)
 
 # did-jwt
@@ -14,7 +14,7 @@ identity of the token, which is passed as the `iss` attribute of the JWT payload
 
 ## DID methods
 
-All DID methods that can be resolved using the [`did-resolver'](https://github.com/decentralized-identity/did-resolver)
+All DID methods that can be resolved using the [`did-resolver`](https://github.com/decentralized-identity/did-resolver)
 interface are supported for verification.
 
 If your DID method requires a different signing algorithm than what is already supported, please create an issue.
@@ -35,15 +35,16 @@ yarn add did-jwt
 
 ### 1. Create a did-JWT
 
-In practice, you must secure the key passed to ES256KSigner. The key provided in code below is for informational
+In practice, you must secure the key passed to `ES256KSigner`. The key provided in code below is for informational
 purposes only.
 
-```js
-const didJWT = require('did-jwt')
+```ts
+import didJWT from 'did-jwt';
+
 const signer = didJWT.ES256KSigner(didJWT.hexToBytes('278a5de700e29faae8e40e366ec5012b5ec63d36ec77e8a2417154cc1d25383f'))
 
 let jwt = await didJWT.createJWT(
-  { aud: 'did:ethr:0xf3beac30c498d9e26865f34fcaa57dbb935b0d74', exp: 1957463421, name: 'uPort Developer' },
+  { aud: 'did:ethr:0xf3beac30c498d9e26865f34fcaa57dbb935b0d74', iat: undefined, name: 'uPort Developer' },
   { issuer: 'did:ethr:0xf3beac30c498d9e26865f34fcaa57dbb935b0d74', signer },
   { alg: 'ES256K' }
 )
@@ -62,19 +63,17 @@ console.log(decoded)
 
 Once decoded a did-JWT will resemble:
 
-```js
-{
-  header: { typ: 'JWT', alg: 'ES256K' },
+```ts
+expect(decoded).toEqual({
+  header: { alg: 'ES256K', typ: 'JWT' },
   payload: {
-    iat: 1571692233,
-    exp: 1957463421,
     aud: 'did:ethr:0xf3beac30c498d9e26865f34fcaa57dbb935b0d74',
     name: 'uPort Developer',
     iss: 'did:ethr:0xf3beac30c498d9e26865f34fcaa57dbb935b0d74'
   },
-  signature: 'kkSmdNE9Xbiql_KCg3IptuJotm08pSEeCOICBCN_4YcgyzFc4wIfBdDQcz76eE-z7xUR3IBb6-r-lRfSJcHMiAA',
-  data: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NkstUiJ9.eyJpYXQiOjE1NzE2OTIyMzMsImV4cCI6MTk1NzQ2MzQyMSwiYXVkIjoiZGlkOmV0aHI6MHhmM2JlYWMzMGM0OThkOWUyNjg2NWYzNGZjYWE1N2RiYjkzNWIwZDc0IiwibmFtZSI6InVQb3J0IERldmVsb3BlciIsImlzcyI6ImRpZDpldGhyOjB4ZjNiZWFjMzBjNDk4ZDllMjY4NjVmMzRmY2FhNTdkYmI5MzViMGQ3NCJ9'
-}
+  signature: 'mAhpAnw-9u57hyAaDufj2GPMbmuZyPDlU7aYSUMKk7P_9_cF3iLk-hFjFhb5xaUQB5nXYrciw6ZJ2RSAZI-IDQ',
+  data: 'eyJhbGciOiJFUzI1NksiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJkaWQ6ZXRocjoweGYzYmVhYzMwYzQ5OGQ5ZTI2ODY1ZjM0ZmNhYTU3ZGJiOTM1YjBkNzQiLCJuYW1lIjoidVBvcnQgRGV2ZWxvcGVyIiwiaXNzIjoiZGlkOmV0aHI6MHhmM2JlYWMzMGM0OThkOWUyNjg2NWYzNGZjYWE1N2RiYjkzNWIwZDc0In0'
+})
 ```
 
 ### 3. Verify a did-JWT
@@ -88,14 +87,14 @@ npm install ethr-did-resolver
 ```
 
 ```js
-const Resolver = require('did-resolver')
-const ethrDid = require('ethr-did-resolver').getResolver({ rpcUrl: 'https://mainnet.infura.io/v3/...' })
+import {Resolver} from 'did-resolver';
+import {getResolver} from 'ethr-did-resolver'
 
-let resolver = new Resolver.Resolver(ethrDid)
+let resolver = new Resolver({...getResolver({infuraProjectId: '<get a free ID from infura.io>'})});
 
-// pass the JWT from step 1
+// use the JWT from step 1
 let verificationResponse = await didJWT.verifyJWT(jwt, {
-  resolver: resolver,
+  resolver,
   audience: 'did:ethr:0xf3beac30c498d9e26865f34fcaa57dbb935b0d74'
 })
 console.log(verificationResponse)
@@ -104,31 +103,45 @@ console.log(verificationResponse)
 A verification response is an object resembling:
 
 ```typescript
-{
+expect(verificationResponse).toEqual({
   payload: {
-    iat: 1571692448,
-    exp: 1957463421,
     aud: 'did:ethr:0xf3beac30c498d9e26865f34fcaa57dbb935b0d74',
     name: 'uPort Developer',
     iss: 'did:ethr:0xf3beac30c498d9e26865f34fcaa57dbb935b0d74'
   },
   didResolutionResult: {
     didDocumentMetadata: {},
-    didResolutionMetadata: {},
+    didResolutionMetadata: { contentType: 'application/did+ld+json' },
     didDocument: {
-      '@context': 'https://w3id.org/did/v1',
+      '@context': [
+        'https://www.w3.org/ns/did/v1',
+        'https://w3id.org/security/suites/secp256k1recovery-2020/v2'
+      ],
       id: 'did:ethr:0xf3beac30c498d9e26865f34fcaa57dbb935b0d74',
-      publicKey: [ [Object] ],
-      authentication: [ [Object] ]
+      verificationMethod: [
+        {
+          id: 'did:ethr:0xf3beac30c498d9e26865f34fcaa57dbb935b0d74#controller',
+          type: 'EcdsaSecp256k1RecoveryMethod2020',
+          controller: 'did:ethr:0xf3beac30c498d9e26865f34fcaa57dbb935b0d74',
+          blockchainAccountId: 'eip155:1:0xF3beAC30C498D9E26865F34fCAa57dBB935b0D74'
+        }
+      ],
+      authentication: [
+        'did:ethr:0xf3beac30c498d9e26865f34fcaa57dbb935b0d74#controller'
+      ],
+      assertionMethod: [
+        'did:ethr:0xf3beac30c498d9e26865f34fcaa57dbb935b0d74#controller'
+      ]
     }
   },
   issuer: 'did:ethr:0xf3beac30c498d9e26865f34fcaa57dbb935b0d74',
   signer: {
-    id: 'did:ethr:0xf3beac30c498d9e26865f34fcaa57dbb935b0d74#owner',
-    type: 'Secp256k1VerificationKey2018',
-    owner: 'did:ethr:0xf3beac30c498d9e26865f34fcaa57dbb935b0d74',
-    ethereumAddress: '0xf3beac30c498d9e26865f34fcaa57dbb935b0d74'
+    id: 'did:ethr:0xf3beac30c498d9e26865f34fcaa57dbb935b0d74#controller',
+    type: 'EcdsaSecp256k1RecoveryMethod2020',
+    controller: 'did:ethr:0xf3beac30c498d9e26865f34fcaa57dbb935b0d74',
+    blockchainAccountId: 'eip155:1:0xF3beAC30C498D9E26865F34fCAa57dBB935b0D74'
   },
-  jwt: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NkstUiJ9.eyJpYXQiOjE1NzE2OTI0NDgsImV4cCI6MTk1NzQ2MzQyMSwiYXVkIjoiZGlkOmV0aHI6MHhmM2JlYWMzMGM0OThkOWUyNjg2NWYzNGZjYWE1N2RiYjkzNWIwZDc0IiwibmFtZSI6InVQb3J0IERldmVsb3BlciIsImlzcyI6ImRpZDpldGhyOjB4ZjNiZWFjMzBjNDk4ZDllMjY4NjVmMzRmY2FhNTdkYmI5MzViMGQ3NCJ9.xd_CSWukS6rK8y7GVvyH_c5yRsDXojM6BuKaf1ZMg0fsgpSBioS7jBfyk4ZZvS0iuFu4u4_771_PNWvmsvaZQQE'
-}
+  jwt: 'eyJhbGciOiJFUzI1NksiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJkaWQ6ZXRocjoweGYzYmVhYzMwYzQ5OGQ5ZTI2ODY1ZjM0ZmNhYTU3ZGJiOTM1YjBkNzQiLCJuYW1lIjoidVBvcnQgRGV2ZWxvcGVyIiwiaXNzIjoiZGlkOmV0aHI6MHhmM2JlYWMzMGM0OThkOWUyNjg2NWYzNGZjYWE1N2RiYjkzNWIwZDc0In0.mAhpAnw-9u57hyAaDufj2GPMbmuZyPDlU7aYSUMKk7P_9_cF3iLk-hFjFhb5xaUQB5nXYrciw6ZJ2RSAZI-IDQ',
+  policies: {}
+})
 ```
diff --git a/src/Errors.ts b/src/Errors.ts
new file mode 100644
index 0000000..3f10d1b
--- /dev/null
+++ b/src/Errors.ts
@@ -0,0 +1,39 @@
+/**
+ * Error prefixes used for known verification failure cases.
+ *
+ * For compatibility, these error prefixes match the existing error messages, but will be adjusted in a future major
+ * version update to match the scenarios better.
+ *
+ * @beta
+ */
+export const enum JWT_ERROR {
+  /**
+   * Thrown when a JWT payload schema is unexpected or when validity period does not match
+   */
+  INVALID_JWT = 'invalid_jwt',
+  /**
+   * Thrown when the verifier audience does not match the one set in the JWT payload
+   */
+  INVALID_AUDIENCE = 'invalid_config',
+  /**
+   * Thrown when none of the public keys of the issuer match the signature of the JWT.
+   *
+   * This is equivalent to `NO_SUITABLE_KEYS` when the `proofPurpose` is NOT specified.
+   */
+  INVALID_SIGNATURE = 'invalid_signature',
+  /**
+   * Thrown when the DID document of the issuer does not have any keys that match the signature for the given
+   * `proofPurpose`.
+   *
+   * This is equivalent to `invalid_signature`, when a `proofPurpose` is specified.
+   */
+  NO_SUITABLE_KEYS = 'no_suitable_keys',
+  /**
+   * Thrown when the `alg` of the JWT or the encoding of the key is not supported
+   */
+  NOT_SUPPORTED = 'not_supported',
+  /**
+   * Thrown when the DID resolver is unable to resolve the issuer DID.
+   */
+  RESOLVER_ERROR = 'resolver_error',
+}
diff --git a/src/JWT.ts b/src/JWT.ts
index b64f31f..c4de172 100644
--- a/src/JWT.ts
+++ b/src/JWT.ts
@@ -3,6 +3,7 @@ import type { DIDDocument, DIDResolutionResult, Resolvable, VerificationMethod }
 import SignerAlg from './SignerAlgorithm'
 import { decodeBase64url, EcdsaSignature, encodeBase64url } from './util'
 import VerifierAlgorithm from './VerifierAlgorithm'
+import { JWT_ERROR } from './Errors'
 
 export type Signer = (data: string | Uint8Array) => Promise<EcdsaSignature | string>
 export type SignerAlgorithm = (payload: string, signer: Signer) => Promise<string>
@@ -37,11 +38,20 @@ export interface JWTVerifyOptions {
   policies?: JWTVerifyPolicies
 }
 
+/**
+ * Overrides the different types of checks performed on the JWT besides the signature check
+ */
 export interface JWTVerifyPolicies {
+  // overrides the timestamp against which the validity interval is checked
   now?: number
+  // when set to false, the timestamp checks ignore the Not Before(`nbf`) property
   nbf?: boolean
+  // when set to false, the timestamp checks ignore the Issued At(`iat`) property
   iat?: boolean
+  // when set to false, the timestamp checks ignore the Expires At(`exp`) property
   exp?: boolean
+  // when set to false, the JWT audience check is skipped
+  aud?: boolean
 }
 
 export interface JWSCreationOptions {
@@ -57,6 +67,7 @@ export interface DIDAuthenticator {
 export interface JWTHeader {
   typ: 'JWT'
   alg: string
+
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
   [x: string]: any
 }
@@ -69,6 +80,7 @@ export interface JWTPayload {
   nbf?: number
   exp?: number
   rexp?: number
+
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
   [x: string]: any
 }
@@ -87,17 +99,50 @@ export interface JWSDecoded {
   data: string
 }
 
+/**
+ * Result object returned by {@link verifyJWT}
+ */
 export interface JWTVerified {
+  /**
+   * Set to true for a JWT that passes all the required checks minus any verification overrides.
+   */
+  verified: true
+
+  /**
+   * The decoded JWT payload
+   */
   payload: Partial<JWTPayload>
+
+  /**
+   * The result of resolving the issuer DID
+   */
   didResolutionResult: DIDResolutionResult
+
+  /**
+   * the issuer DID
+   */
   issuer: string
+
+  /**
+   * The public key of the issuer that matches the JWT signature
+   */
   signer: VerificationMethod
+
+  /**
+   * The original JWT that was verified
+   */
   jwt: string
+
+  /**
+   * Any overrides that were used during verification
+   */
+  policies?: JWTVerifyPolicies
 }
 
 export interface PublicKeyTypes {
   [name: string]: string[]
 }
+
 export const SUPPORTED_PUBLIC_KEY_TYPES: PublicKeyTypes = {
   ES256K: [
     'EcdsaSecp256k1VerificationKey2019',
@@ -106,15 +151,18 @@ export const SUPPORTED_PUBLIC_KEY_TYPES: PublicKeyTypes = {
      */
     'EcdsaSecp256k1RecoveryMethod2020',
     /**
-     * @deprecated, supported for backward compatibility. Equivalent to EcdsaSecp256k1VerificationKey2019 when key is not an ethereumAddress
+     * @deprecated, supported for backward compatibility. Equivalent to EcdsaSecp256k1VerificationKey2019 when key is
+     *   not an ethereumAddress
      */
     'Secp256k1VerificationKey2018',
     /**
-     * @deprecated, supported for backward compatibility. Equivalent to EcdsaSecp256k1VerificationKey2019 when key is not an ethereumAddress
+     * @deprecated, supported for backward compatibility. Equivalent to EcdsaSecp256k1VerificationKey2019 when key is
+     *   not an ethereumAddress
      */
     'Secp256k1SignatureVerificationKey2018',
     /**
-     * @deprecated, supported for backward compatibility. Equivalent to EcdsaSecp256k1VerificationKey2019 when key is not an ethereumAddress
+     * @deprecated, supported for backward compatibility. Equivalent to EcdsaSecp256k1VerificationKey2019 when key is
+     *   not an ethereumAddress
      */
     'EcdsaPublicKeySecp256k1',
   ],
@@ -125,15 +173,18 @@ export const SUPPORTED_PUBLIC_KEY_TYPES: PublicKeyTypes = {
      */
     'EcdsaSecp256k1RecoveryMethod2020',
     /**
-     * @deprecated, supported for backward compatibility. Equivalent to EcdsaSecp256k1VerificationKey2019 when key is not an ethereumAddress
+     * @deprecated, supported for backward compatibility. Equivalent to EcdsaSecp256k1VerificationKey2019 when key is
+     *   not an ethereumAddress
      */
     'Secp256k1VerificationKey2018',
     /**
-     * @deprecated, supported for backward compatibility. Equivalent to EcdsaSecp256k1VerificationKey2019 when key is not an ethereumAddress
+     * @deprecated, supported for backward compatibility. Equivalent to EcdsaSecp256k1VerificationKey2019 when key is
+     *   not an ethereumAddress
      */
     'Secp256k1SignatureVerificationKey2018',
     /**
-     * @deprecated, supported for backward compatibility. Equivalent to EcdsaSecp256k1VerificationKey2019 when key is not an ethereumAddress
+     * @deprecated, supported for backward compatibility. Equivalent to EcdsaSecp256k1VerificationKey2019 when key is
+     *   not an ethereumAddress
      */
     'EcdsaPublicKeySecp256k1',
   ],
@@ -149,14 +200,6 @@ 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'
@@ -238,7 +281,8 @@ export async function createJWS(
 }
 
 /**
- *  Creates a signed JWT given an address which becomes the issuer, a signer, and a payload for which the signature is over.
+ *  Creates a signed JWT given an address which becomes the issuer, a signer, and a payload for which the signature is
+ * over.
  *
  *  @example
  *  const signer = ES256KSigner(process.env.PRIVATE_KEY)
@@ -249,12 +293,13 @@ export async function createJWS(
  *  @param    {Object}            payload               payload object
  *  @param    {Object}            [options]             an unsigned credential object
  *  @param    {String}            options.issuer        The DID of the issuer (signer) of JWT
- *  @param    {String}            options.alg           [DEPRECATED] The JWT signing algorithm to use. Supports: [ES256K, ES256K-R, Ed25519, EdDSA], Defaults to: ES256K.
- *                                                      Please use `header.alg` to specify the algorithm
+ *  @param    {String}            options.alg           [DEPRECATED] The JWT signing algorithm to use. Supports:
+ *   [ES256K, ES256K-R, Ed25519, EdDSA], Defaults to: ES256K. Please use `header.alg` to specify the algorithm
  *  @param    {Signer}            options.signer        a `Signer` function, Please see `ES256KSigner` or `EdDSASigner`
  *  @param    {boolean}           options.canonicalize  optional flag to canonicalize header and payload before signing
  *  @param    {Object}            header                optional object to specify or customize the JWT header
- *  @return   {Promise<Object, Error>}                  a promise which resolves with a signed JSON Web Token or rejects with an error
+ *  @return   {Promise<Object, Error>}                  a promise which resolves with a signed JSON Web Token or
+ *   rejects with an error
  */
 export async function createJWT(
   payload: Partial<JWTPayload>,
@@ -307,24 +352,31 @@ export function verifyJWS(jws: string, pubKeys: VerificationMethod | Verificatio
 
 /**
  *  Verifies given JWT. If the JWT is valid, the promise returns an object including the JWT, the payload of the JWT,
- *  and the did doc of the issuer of the JWT.
+ *  and the DID document of the issuer of the JWT.
  *
  *  @example
- *  verifyJWT('did:uport:eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJyZXF1Z....', {audience: '5A8bRWU3F7j3REx3vkJ...', callbackUrl: 'https://...'}).then(obj => {
- *      const did = obj.did // DID of signer
- *      const payload = obj.payload
- *      const doc = obj.doc // DID Document of signer
- *      const jwt = obj.jwt
- *      const signerKeyId = obj.signerKeyId // ID of key in DID document that signed JWT
- *      ...
- *  })
+ *  ```ts
+ *  verifyJWT(
+ *      'did:uport:eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJyZXF1Z....',
+ *      {audience: '5A8bRWU3F7j3REx3vkJ...', callbackUrl: 'https://...'}
+ *    ).then(obj => {
+ *        const did = obj.did // DID of signer
+ *        const payload = obj.payload
+ *        const doc = obj.didResolutionResult.didDocument // DID Document of issuer
+ *        const jwt = obj.jwt
+ *        const signerKeyId = obj.signer.id // ID of key in DID document that signed JWT
+ *        ...
+ *    })
+ *  ```
  *
  *  @param    {String}            jwt                a JSON Web Token to verify
  *  @param    {Object}            [options]           an unsigned credential object
- *  @param    {Boolean}           options.auth        Require signer to be listed in the authentication section of the DID document (for Authentication purposes)
+ *  @param    {Boolean}           options.auth        Require signer to be listed in the authentication section of the
+ *   DID document (for Authentication purposes)
  *  @param    {String}            options.audience    DID of the recipient of the JWT
  *  @param    {String}            options.callbackUrl callback url in JWT
- *  @return   {Promise<Object, Error>}               a promise which resolves with a response object or rejects with an error
+ *  @return   {Promise<Object, Error>}               a promise which resolves with a response object or rejects with an
+ *   error
  */
 export async function verifyJWT(
   jwt: string,
@@ -335,12 +387,7 @@ export async function verifyJWT(
     callbackUrl: undefined,
     skewTime: undefined,
     proofPurpose: undefined,
-    policies: {
-      nbf: undefined,
-      iat: undefined,
-      exp: undefined,
-      now: undefined,
-    },
+    policies: {},
   }
 ): Promise<JWTVerified> {
   if (!options.resolver) throw new Error('missing_resolver: No DID resolver has been configured')
@@ -354,12 +401,12 @@ export async function verifyJWT(
   let did = ''
 
   if (!payload.iss) {
-    throw new Error('invalid_jwt: JWT iss is required')
+    throw new Error(`${JWT_ERROR.INVALID_JWT}: JWT iss is required`)
   }
 
   if (payload.iss === SELF_ISSUED_V2) {
     if (!payload.sub) {
-      throw new Error('invalid_jwt: JWT sub is required')
+      throw new Error(`${JWT_ERROR.INVALID_JWT}: JWT sub is required`)
     }
     if (typeof payload.sub_jwk === 'undefined') {
       did = payload.sub
@@ -368,7 +415,7 @@ export async function verifyJWT(
     }
   } else if (payload.iss === SELF_ISSUED_V0_1) {
     if (!payload.did) {
-      throw new Error('invalid_jwt: JWT did is required')
+      throw new Error(`${JWT_ERROR.INVALID_JWT}: JWT did is required`)
     }
     did = payload.did
   } else {
@@ -376,7 +423,7 @@ export async function verifyJWT(
   }
 
   if (!did) {
-    throw new Error(`invalid_jwt: No DID has been found in the JWT`)
+    throw new Error(`${JWT_ERROR.INVALID_JWT}: No DID has been found in the JWT`)
   }
 
   const { didResolutionResult, authenticators, issuer }: DIDAuthenticator = await resolveAuthenticator(
@@ -386,53 +433,61 @@ export async function verifyJWT(
     proofPurpose
   )
   const signer: VerificationMethod = await verifyJWSDecoded({ header, data, signature } as JWSDecoded, authenticators)
-  const now: number = options.policies?.now ? options.policies.now : Math.floor(Date.now() / 1000)
+  const now: number = typeof options.policies?.now === 'number' ? 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 (options.policies?.nbf !== false && payload.nbf) {
       if (payload.nbf > nowSkewed) {
-        throw new Error(`invalid_jwt: JWT not valid before nbf: ${payload.nbf}`)
+        throw new Error(`${JWT_ERROR.INVALID_JWT}: JWT not valid before nbf: ${payload.nbf}`)
       }
     } 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}`)
+      throw new Error(`${JWT_ERROR.INVALID_JWT}: JWT not valid yet (issued in the future) iat: ${payload.iat}`)
     }
     if (options.policies?.exp !== false && payload.exp && payload.exp <= now - skewTime) {
-      throw new Error(`invalid_jwt: JWT has expired: exp: ${payload.exp} < now: ${now}`)
+      throw new Error(`${JWT_ERROR.INVALID_JWT}: JWT has expired: exp: ${payload.exp} < now: ${now}`)
     }
-    if (payload.aud) {
+    if (options.policies?.aud !== false && payload.aud) {
       if (!options.audience && !options.callbackUrl) {
-        throw new Error('invalid_config: JWT audience is required but your app address has not been configured')
+        throw new Error(
+          `${JWT_ERROR.INVALID_AUDIENCE}: JWT audience is required but your app address has not been configured`
+        )
       }
       const audArray = Array.isArray(payload.aud) ? payload.aud : [payload.aud]
       const matchedAudience = audArray.find((item) => options.audience === item || options.callbackUrl === item)
 
       if (typeof matchedAudience === 'undefined') {
-        throw new Error(`invalid_config: JWT audience does not match your DID or callback url`)
+        throw new Error(`${JWT_ERROR.INVALID_AUDIENCE}: JWT audience does not match your DID or callback url`)
       }
     }
-    return { payload, didResolutionResult, issuer, signer, jwt }
+    return { verified: true, payload, didResolutionResult, issuer, signer, jwt, policies: options.policies }
   }
   throw new Error(
-    `invalid_signature: JWT not valid. issuer DID document does not contain a verificationMethod that matches the signature.`
+    `${JWT_ERROR.INVALID_SIGNATURE}: JWT not valid. issuer DID document does not contain a verificationMethod that matches the signature.`
   )
 }
 
 /**
- * Resolves relevant public keys or other authenticating material used to verify signature from the DID document of provided DID
+ * Resolves relevant public keys or other authenticating material used to verify signature from the DID document of
+ * provided DID
  *
  *  @example
+ *  ```ts
  *  resolveAuthenticator(resolver, 'ES256K', 'did:uport:2nQtiQG6Cgm1GYTBaaKAgr76uY7iSexUkqX').then(obj => {
  *      const payload = obj.payload
  *      const profile = obj.profile
  *      const jwt = obj.jwt
- *      ...
+ *      // ...
  *  })
+ *  ```
  *
- *  @param    {String}            alg                a JWT algorithm
- *  @param    {String}            did                a Decentralized IDentifier (DID) to lookup
- *  @param    {Boolean}           auth               Restrict public keys to ones specifically listed in the 'authentication' section of DID document
- *  @return   {Promise<DIDAuthenticator>}               a promise which resolves with a response object containing an array of authenticators or if non exist rejects with an error
+ *  @param resolver - {Resolvable} a DID resolver function that can obtain the `DIDDocument` for the `issuer`
+ *  @param alg - {String} a JWT algorithm
+ *  @param issuer - {String} a Decentralized Identifier (DID) to lookup
+ *  @param proofPurpose - {ProofPurposeTypes} *Optional* Use the verificationMethod linked in that section of the
+ *   issuer DID document
+ *  @return {Promise<DIDAuthenticator>} a promise which resolves with an object containing an array of authenticators
+ *   or rejects with an error if none exist
  */
 export async function resolveAuthenticator(
   resolver: Resolvable,
@@ -442,7 +497,7 @@ export async function resolveAuthenticator(
 ): Promise<DIDAuthenticator> {
   const types: string[] = SUPPORTED_PUBLIC_KEY_TYPES[alg]
   if (!types || types.length === 0) {
-    throw new Error(`not_supported: No supported signature types for algorithm ${alg}`)
+    throw new Error(`${JWT_ERROR.NOT_SUPPORTED}: No supported signature types for algorithm ${alg}`)
   }
   let didResult: DIDResolutionResult
 
@@ -460,7 +515,9 @@ export async function resolveAuthenticator(
 
   if (didResult.didResolutionMetadata?.error || didResult.didDocument == null) {
     const { error, message } = didResult.didResolutionMetadata
-    throw new Error(`resolver_error: Unable to resolve DID document for ${issuer}: ${error}, ${message || ''}`)
+    throw new Error(
+      `${JWT_ERROR.RESOLVER_ERROR}: Unable to resolve DID document for ${issuer}: ${error}, ${message || ''}`
+    )
   }
 
   const getPublicKeyById = (verificationMethods: VerificationMethod[], pubid?: string): VerificationMethod | null => {
@@ -502,11 +559,11 @@ export async function resolveAuthenticator(
 
   if (typeof proofPurpose === 'string' && (!authenticators || authenticators.length === 0)) {
     throw new Error(
-      `no_suitable_keys: DID document for ${issuer} does not have public keys suitable for ${alg} with ${proofPurpose} purpose`
+      `${JWT_ERROR.NO_SUITABLE_KEYS}: DID document for ${issuer} does not have public keys suitable for ${alg} with ${proofPurpose} purpose`
     )
   }
   if (!authenticators || authenticators.length === 0) {
-    throw new Error(`no_suitable_keys: DID document for ${issuer} does not have public keys for ${alg}`)
+    throw new Error(`${JWT_ERROR.NO_SUITABLE_KEYS}: DID document for ${issuer} does not have public keys for ${alg}`)
   }
   return { authenticators, issuer, didResolutionResult: didResult }
 }
diff --git a/src/index.ts b/src/index.ts
index a854eb0..74bbc7a 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -5,17 +5,18 @@ import { ES256KSigner } from './signers/ES256KSigner'
 import { ES256Signer } from './signers/ES256Signer'
 import { EdDSASigner } from './signers/EdDSASigner'
 import {
-  verifyJWT,
+  createJWS,
   createJWT,
   decodeJWT,
-  verifyJWS,
-  createJWS,
-  Signer,
   JWTHeader,
   JWTPayload,
   JWTVerified,
+  Signer,
+  verifyJWS,
+  verifyJWT,
 } from './JWT'
 import { toEthereumAddress } from './Digest'
+
 export { JWE, createJWE, decryptJWE, Encrypter, Decrypter, ProtectedHeader, Recipient, RecipientHeader } from './JWE'
 export { ECDH, createX25519ECDH } from './ECDH'
 export {
@@ -54,3 +55,5 @@ export {
 export { JWTOptions, JWTVerifyOptions } from './JWT'
 
 export { base64ToBytes, base58ToBytes, hexToBytes } from './util'
+
+export * from './Errors'


Test patch:
diff --git a/src/__tests__/JWT.test.ts b/src/__tests__/JWT.test.ts
index 15331fc..c293208 100644
--- a/src/__tests__/JWT.test.ts
+++ b/src/__tests__/JWT.test.ts
@@ -3,7 +3,7 @@ import { VerificationMethod } from 'did-resolver'
 import { TokenVerifier } from 'jsontokens'
 import MockDate from 'mockdate'
 import { fromString } from 'uint8arrays/from-string'
-import { getAddress } from "@ethersproject/address"
+import { getAddress } from '@ethersproject/address'
 import {
   createJWS,
   createJWT,
@@ -466,7 +466,13 @@ describe('verifyJWT()', () => {
       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 })
+      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 } })
@@ -477,14 +483,19 @@ describe('verifyJWT()', () => {
       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 })
+      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 =
@@ -499,7 +510,7 @@ describe('verifyJWT()', () => {
       // const jwt = await createJWT({nbf:FUTURE,iat:PAST},{issuer:did,signer})
 
       const { payload } = await verifyJWT(jwt, { resolver, policies: { nbf: false } })
-      expect(payload).toBeDefined();
+      expect(payload).toBeDefined()
     })
     it('passes when nbf is missing and iat is in the past', async () => {
       expect.assertions(1)
@@ -520,7 +531,7 @@ describe('verifyJWT()', () => {
       const jwt =
         'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJpYXQiOjE0ODUzODExMzMsImlzcyI6ImRpZDpldGhyOjB4ZjNiZWFjMzBjNDk4ZDllMjY4NjVmMzRmY2FhNTdkYmI5MzViMGQ3NCJ9.FJuHvf9Tby7b4I54Cm1nh8CvLg4QH2wt2K0WfyQaLqlr3NKKI5hAdLalgZksI25gLhNrZwQFnC-nzEOs9PI1SQ'
       // const jwt = await createJWT({iat:FUTURE},{issuer:did,signer})
-      const { payload } = await verifyJWT(jwt, { resolver, policies: { iat: false }})
+      const { payload } = await verifyJWT(jwt, { resolver, policies: { iat: false } })
       return expect(payload).toBeDefined()
     })
     it('passes when nbf and iat are both missing', async () => {
@@ -602,9 +613,7 @@ describe('verifyJWT()', () => {
       resolve: jest.fn().mockReturnValue({
         didDocument: {
           id: did,
-          verificationMethod: [
-            verificationMethod
-          ],
+          verificationMethod: [verificationMethod],
         },
       }),
     }
@@ -643,7 +652,7 @@ describe('verifyJWT()', () => {
     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();
+    return expect(payload).toBeDefined()
   })
 
   it('accepts a valid audience', async () => {
@@ -686,6 +695,17 @@ describe('verifyJWT()', () => {
     )
   })
 
+  it('accepts invalid audience when override policy is used', async () => {
+    expect.assertions(2)
+    const jwt = await createJWT({ aud }, { issuer: did, signer })
+    const { payload, issuer } = await verifyJWT(jwt, {
+      resolver,
+      policies: { aud: false }
+    })
+    expect(payload).toBeDefined()
+    expect(issuer).toEqual(did)
+  })
+
   it('rejects an invalid audience using callback_url where callback is wrong', async () => {
     expect.assertions(1)
     const jwt = await createJWT({ aud: 'http://pututu.uport.me/unique' }, { issuer: did, signer })