diff --git a/README.md b/README.md index b4559922..607efc30 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: ''})}); -// 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 00000000..3f10d1b8 --- /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 b64f31f7..c4de1725 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 export type SignerAlgorithm = (payload: string, signer: Signer) => Promise @@ -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 + + /** + * 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} a promise which resolves with a signed JSON Web Token or rejects with an error + * @return {Promise} a promise which resolves with a signed JSON Web Token or + * rejects with an error */ export async function createJWT( payload: Partial, @@ -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} a promise which resolves with a response object or rejects with an error + * @return {Promise} 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 { 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} 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} 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 { 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/__tests__/JWT.test.ts b/src/__tests__/JWT.test.ts index 15331fc6..c2932082 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 }) diff --git a/src/index.ts b/src/index.ts index a854eb08..74bbc7ac 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'