diff --git a/src/Errors.ts b/src/Errors.ts new file mode 100644 index 00000000..fcbebfb0 --- /dev/null +++ b/src/Errors.ts @@ -0,0 +1,35 @@ +/** + * 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 is not configured for the given JWT input + */ + INVALID_CONFIG = 'invalid_config', + /** + * Thrown when none of the public keys of the issuer match the signature of the JWT + */ + INVALID_SIGNATURE = 'invalid_signature', + /** + * Thrown when the `alg` of the JWT or the encoding of the key is not supported + */ + NOT_SUPPORTED = 'not_supported', + /** + * Thrown when the DID document of the issuer does not have any keys that match the signature for the given + * `proofPurpose` + */ + NO_SUITABLE_KEYS = 'no_suitable_keys', + /** + * 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 041a21c2..fbe8ca0b 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 @@ -98,13 +99,59 @@ 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 +} + +/** + * RESERVED. + */ +export interface JWTInvalid { + verified: false + error: { + message: string + errorCode: JWT_ERROR + } + /** + * Any overrides that were used during verification + */ + policies: JWTVerifyPolicies } export interface PublicKeyTypes { @@ -168,14 +215,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 INVALID_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 RESOLVER_ERROR = 'resolver_error' - type LegacyVerificationMethod = { publicKey?: string } const defaultAlg = 'ES256K' @@ -328,7 +367,7 @@ 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 * ```ts @@ -338,9 +377,9 @@ export function verifyJWS(jws: string, pubKeys: VerificationMethod | Verificatio * ).then(obj => { * const did = obj.did // DID of signer * const payload = obj.payload - * const doc = obj.doc // DID Document of signer + * const doc = obj.didResolutionResult.didDocument // DID Document of issuer * const jwt = obj.jwt - * const signerKeyId = obj.signerKeyId // ID of key in DID document that signed JWT + * const signerKeyId = obj.signer.id // ID of key in DID document that signed JWT * ... * }) * ``` @@ -363,12 +402,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') @@ -382,12 +416,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 @@ -396,7 +430,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 { @@ -404,7 +438,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( @@ -414,35 +448,37 @@ 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 (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_CONFIG}: 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_CONFIG}: JWT audience does not match your DID or callback url`) } } - return { verified: true, payload, didResolutionResult, issuer, signer, jwt } + return { verified: true, payload, didResolutionResult, issuer, signer, jwt, 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.` ) } @@ -476,7 +512,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 @@ -494,7 +530,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 => { @@ -536,11 +574,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 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'