Skip to content

Commit

Permalink
feat: define and export known error prefixes
Browse files Browse the repository at this point in the history
  • Loading branch information
mirceanis committed Aug 18, 2022
1 parent 79998c0 commit 3c5ddbb
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 37 deletions.
35 changes: 35 additions & 0 deletions src/Errors.ts
Original file line number Diff line number Diff line change
@@ -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',
}
104 changes: 71 additions & 33 deletions src/JWT.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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<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
}

/**
* RESERVED.
*/
export interface JWTInvalid {
verified: false
error: {
message: string
errorCode: JWT_ERROR
}
/**
* Any overrides that were used during verification
*/
policies: JWTVerifyPolicies
}

export interface PublicKeyTypes {
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand All @@ -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
* ...
* })
* ```
Expand All @@ -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<JWTVerified> {
if (!options.resolver) throw new Error('missing_resolver: No DID resolver has been configured')
Expand All @@ -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
Expand All @@ -396,15 +430,15 @@ 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 {
did = payload.iss
}

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(
Expand All @@ -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.`
)
}

Expand Down Expand Up @@ -476,7 +512,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

Expand All @@ -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 => {
Expand Down Expand Up @@ -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 }
}
11 changes: 7 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -54,3 +55,5 @@ export {
export { JWTOptions, JWTVerifyOptions } from './JWT'

export { base64ToBytes, base58ToBytes, hexToBytes } from './util'

export * from './Errors'

0 comments on commit 3c5ddbb

Please sign in to comment.