UNPKG

24.6 kBPlain TextView Raw
1import canonicalizeData from 'canonicalize'
2import { DIDDocument, DIDResolutionResult, parse, ParsedDID, Resolvable, VerificationMethod } from 'did-resolver'
3import SignerAlg from './SignerAlgorithm.js'
4import { decodeBase64url, EcdsaSignature, encodeBase64url, KNOWN_JWA, SUPPORTED_PUBLIC_KEY_TYPES } from './util.js'
5import VerifierAlgorithm from './VerifierAlgorithm.js'
6import { JWT_ERROR } from './Errors.js'
7import { verifyProof } from './ConditionalAlgorithm.js'
8
9export type Signer = (data: string | Uint8Array) => Promise<EcdsaSignature | string>
10export type SignerAlgorithm = (payload: string, signer: Signer) => Promise<string>
11
12export type ProofPurposeTypes =
13 | 'assertionMethod'
14 | 'authentication'
15 // | 'keyAgreement' // keyAgreement VerificationMethod should not be used for signing
16 | 'capabilityDelegation'
17 | 'capabilityInvocation'
18
19export interface JWTOptions {
20 issuer: string
21 signer: Signer
22 /**
23 * @deprecated Please use `header.alg` to specify the JWT algorithm.
24 */
25 alg?: string
26 expiresIn?: number
27 canonicalize?: boolean
28}
29
30export interface JWTVerifyOptions {
31 /** @deprecated Please use `proofPurpose: 'authentication' instead` */
32 auth?: boolean
33 audience?: string
34 callbackUrl?: string
35 resolver?: Resolvable
36 skewTime?: number
37 /** See https://www.w3.org/TR/did-spec-registries/#verification-relationships */
38 proofPurpose?: ProofPurposeTypes
39 policies?: JWTVerifyPolicies
40 didAuthenticator?: DIDAuthenticator
41}
42
43/**
44 * Overrides the different types of checks performed on the JWT besides the signature check
45 */
46export interface JWTVerifyPolicies {
47 // overrides the timestamp against which the validity interval is checked
48 now?: number
49 // when set to false, the timestamp checks ignore the Not Before(`nbf`) property
50 nbf?: boolean
51 // when set to false, the timestamp checks ignore the Issued At(`iat`) property
52 iat?: boolean
53 // when set to false, the timestamp checks ignore the Expires At(`exp`) property
54 exp?: boolean
55 // when set to false, the JWT audience check is skipped
56 aud?: boolean
57}
58
59export interface JWSCreationOptions {
60 canonicalize?: boolean
61}
62
63export interface DIDAuthenticator {
64 authenticators: VerificationMethod[]
65 issuer: string
66 didResolutionResult: DIDResolutionResult
67}
68
69export interface JWTHeader {
70 typ: 'JWT'
71 alg: string
72
73 // eslint-disable-next-line @typescript-eslint/no-explicit-any
74 [x: string]: any
75}
76
77export interface JWTPayload {
78 iss?: string
79 sub?: string
80 aud?: string | string[]
81 iat?: number
82 nbf?: number
83 exp?: number
84 rexp?: number
85
86 // eslint-disable-next-line @typescript-eslint/no-explicit-any
87 [x: string]: any
88}
89
90export interface JWTDecoded {
91 header: JWTHeader
92 payload: JWTPayload
93 signature: string
94 data: string
95}
96
97export interface JWSDecoded {
98 header: JWTHeader
99 payload: string
100 signature: string
101 data: string
102}
103
104/**
105 * Result object returned by {@link verifyJWT}
106 */
107export interface JWTVerified {
108 /**
109 * Set to true for a JWT that passes all the required checks minus any verification overrides.
110 */
111 verified: true
112
113 /**
114 * The decoded JWT payload
115 */
116 payload: Partial<JWTPayload>
117
118 /**
119 * The result of resolving the issuer DID
120 */
121 didResolutionResult: DIDResolutionResult
122
123 /**
124 * the issuer DID
125 */
126 issuer: string
127
128 /**
129 * The public key of the issuer that matches the JWT signature
130 */
131 signer: VerificationMethod
132
133 /**
134 * The original JWT that was verified
135 */
136 jwt: string
137
138 /**
139 * Any overrides that were used during verification
140 */
141 policies?: JWTVerifyPolicies
142}
143
144export const SELF_ISSUED_V2 = 'https://self-issued.me/v2'
145export const SELF_ISSUED_V2_VC_INTEROP = 'https://self-issued.me/v2/openid-vc' // https://identity.foundation/jwt-vc-presentation-profile/#id-token-validation
146export const SELF_ISSUED_V0_1 = 'https://self-issued.me'
147
148type LegacyVerificationMethod = { publicKey?: string }
149
150const defaultAlg: KNOWN_JWA = 'ES256K'
151const DID_JSON = 'application/did+json'
152
153// eslint-disable-next-line @typescript-eslint/no-explicit-any
154function encodeSection(data: any, shouldCanonicalize = false): string {
155 if (shouldCanonicalize) {
156 return encodeBase64url(<string>canonicalizeData(data))
157 } else {
158 return encodeBase64url(JSON.stringify(data))
159 }
160}
161
162export const NBF_SKEW = 300
163
164function decodeJWS(jws: string): JWSDecoded {
165 const parts = jws.match(/^([a-zA-Z0-9_-]+)\.([a-zA-Z0-9_-]+)\.([a-zA-Z0-9_-]+)$/)
166 if (parts) {
167 return {
168 header: JSON.parse(decodeBase64url(parts[1])),
169 payload: parts[2],
170 signature: parts[3],
171 data: `${parts[1]}.${parts[2]}`,
172 }
173 }
174 throw new Error('invalid_argument: Incorrect format JWS')
175}
176
177/**
178 * Decodes a JWT and returns an object representing the payload
179 *
180 * @example
181 * decodeJWT('eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJpYXQiOjE1...')
182 *
183 * @param {String} jwt a JSON Web Token to verify
184 * @param {Object} [recurse] whether to recurse into the payload to decode any nested JWTs
185 * @return {Object} a JS object representing the decoded JWT
186 */
187export function decodeJWT(jwt: string, recurse = true): JWTDecoded {
188 if (!jwt) throw new Error('invalid_argument: no JWT passed into decodeJWT')
189 try {
190 const jws = decodeJWS(jwt)
191 const decodedJwt: JWTDecoded = Object.assign(jws, { payload: JSON.parse(decodeBase64url(jws.payload)) })
192 const iss = decodedJwt.payload.iss
193
194 if (decodedJwt.header.cty === 'JWT' && recurse) {
195 const innerDecodedJwt = decodeJWT(decodedJwt.payload.jwt)
196
197 if (innerDecodedJwt.payload.iss !== iss) throw new Error(`${JWT_ERROR.INVALID_JWT}: multiple issuers`)
198 return innerDecodedJwt
199 }
200 return decodedJwt
201 } catch (e) {
202 throw new Error('invalid_argument: Incorrect format JWT')
203 }
204}
205
206/**
207 * Creates a signed JWS given a payload, a signer, and an optional header.
208 *
209 * @example
210 * const signer = ES256KSigner(process.env.PRIVATE_KEY)
211 * const jws = await createJWS({ my: 'payload' }, signer)
212 *
213 * @param {Object} payload payload object
214 * @param {Signer} signer a signer, see `ES256KSigner or `EdDSASigner`
215 * @param {Object} header optional object to specify or customize the JWS header
216 * @param {Object} options can be used to trigger automatic canonicalization of header and
217 * payload properties
218 * @return {Promise<string>} a Promise which resolves to a JWS string or rejects with an error
219 */
220export async function createJWS(
221 payload: string | Partial<JWTPayload>,
222 signer: Signer,
223 header: Partial<JWTHeader> = {},
224 options: JWSCreationOptions = {}
225): Promise<string> {
226 if (!header.alg) header.alg = defaultAlg
227 const encodedPayload = typeof payload === 'string' ? payload : encodeSection(payload, options.canonicalize)
228 const signingInput: string = [encodeSection(header, options.canonicalize), encodedPayload].join('.')
229
230 const jwtSigner: SignerAlgorithm = SignerAlg(header.alg)
231 const signature: string = await jwtSigner(signingInput, signer)
232
233 // JWS Compact Serialization
234 // https://www.rfc-editor.org/rfc/rfc7515#section-7.1
235 return [signingInput, signature].join('.')
236}
237
238/**
239 * Creates a signed JWT given an address which becomes the issuer, a signer, and a payload for which the signature is
240 * over.
241 *
242 * @example
243 * const signer = ES256KSigner(process.env.PRIVATE_KEY)
244 * createJWT({address: '5A8bRWU3F7j3REx3vkJ...', signer}, {key1: 'value', key2: ..., ... }).then(jwt => {
245 * ...
246 * })
247 *
248 * @param {Object} payload payload object
249 * @param {Object} [options] an unsigned credential object
250 * @param {String} options.issuer The DID of the issuer (signer) of JWT
251 * @param {String} options.alg [DEPRECATED] The JWT signing algorithm to use. Supports:
252 * [ES256K, ES256K-R, Ed25519, EdDSA], Defaults to: ES256K. Please use `header.alg` to specify the algorithm
253 * @param {Signer} options.signer a `Signer` function, Please see `ES256KSigner` or `EdDSASigner`
254 * @param {boolean} options.canonicalize optional flag to canonicalize header and payload before signing
255 * @param {Object} header optional object to specify or customize the JWT header
256 * @return {Promise<Object, Error>} a promise which resolves with a signed JSON Web Token or
257 * rejects with an error
258 */
259export async function createJWT(
260 payload: Partial<JWTPayload>,
261 { issuer, signer, alg, expiresIn, canonicalize }: JWTOptions,
262 header: Partial<JWTHeader> = {}
263): Promise<string> {
264 if (!signer) throw new Error('missing_signer: No Signer functionality has been configured')
265 if (!issuer) throw new Error('missing_issuer: No issuing DID has been configured')
266 if (!header.typ) header.typ = 'JWT'
267 if (!header.alg) header.alg = alg
268 const timestamps: Partial<JWTPayload> = {
269 iat: Math.floor(Date.now() / 1000),
270 exp: undefined,
271 }
272 if (expiresIn) {
273 if (typeof expiresIn === 'number') {
274 timestamps.exp = <number>(payload.nbf || timestamps.iat) + Math.floor(expiresIn)
275 } else {
276 throw new Error('invalid_argument: JWT expiresIn is not a number')
277 }
278 }
279 const fullPayload = { ...timestamps, ...payload, iss: issuer }
280 return createJWS(fullPayload, signer, header, { canonicalize })
281}
282
283/**
284 * Creates a multi-signature signed JWT given multiple issuers and their corresponding signers, and a payload for
285 * which the signature is over.
286 *
287 * @example
288 * const signer = ES256KSigner(process.env.PRIVATE_KEY)
289 * createJWT({address: '5A8bRWU3F7j3REx3vkJ...', signer}, {key1: 'value', key2: ..., ... }).then(jwt => {
290 * ...
291 * })
292 *
293 * @param {Object} payload payload object
294 * @param {Object} [options] an unsigned credential object
295 * @param {boolean} options.expiresIn optional flag to denote the expiration time
296 * @param {boolean} options.canonicalize optional flag to canonicalize header and payload before signing
297 * @param {Object[]} issuers array of the issuers, their signers and algorithms
298 * @param {string} issuers[].issuer The DID of the issuer (signer) of JWT
299 * @param {Signer} issuers[].signer a `Signer` function, Please see `ES256KSigner` or `EdDSASigner`
300 * @param {String} issuers[].alg [DEPRECATED] The JWT signing algorithm to use. Supports:
301 * [ES256K, ES256K-R, Ed25519, EdDSA], Defaults to: ES256K. Please use `header.alg` to specify the algorithm
302 * @return {Promise<Object, Error>} a promise which resolves with a signed JSON Web Token or
303 * rejects with an error
304 */
305export async function createMultisignatureJWT(
306 payload: Partial<JWTPayload>,
307 { expiresIn, canonicalize }: Partial<JWTOptions>,
308 issuers: { issuer: string; signer: Signer; alg: string }[]
309): Promise<string> {
310 if (issuers.length === 0) throw new Error('invalid_argument: must provide one or more issuers')
311
312 let payloadResult: Partial<JWTPayload> = payload
313
314 let jwt = ''
315 for (let i = 0; i < issuers.length; i++) {
316 const issuer = issuers[i]
317
318 const header: Partial<JWTHeader> = {
319 typ: 'JWT',
320 alg: issuer.alg,
321 }
322
323 // Create nested JWT
324 // See Point 5 of https://www.rfc-editor.org/rfc/rfc7519#section-7.1
325 // After the first JWT is created (the first JWS), the next JWT is created by inputting the previous JWT as the
326 // payload
327 if (i !== 0) {
328 header.cty = 'JWT'
329 }
330
331 jwt = await createJWT(payloadResult, { ...issuer, canonicalize, expiresIn }, header)
332
333 payloadResult = { jwt }
334 }
335 return jwt
336}
337
338export function verifyJWTDecoded(
339 { header, payload, data, signature }: JWTDecoded,
340 pubKeys: VerificationMethod | VerificationMethod[]
341): VerificationMethod {
342 if (!Array.isArray(pubKeys)) pubKeys = [pubKeys]
343
344 const iss = payload.iss
345 let recurse = true
346 do {
347 if (iss !== payload.iss) throw new Error(`${JWT_ERROR.INVALID_JWT}: multiple issuers`)
348
349 try {
350 const result = VerifierAlgorithm(header.alg)(data, signature, pubKeys)
351
352 return result
353 } catch (e) {
354 if (!(e as Error).message.startsWith(JWT_ERROR.INVALID_SIGNATURE)) throw e
355 }
356
357 // TODO probably best to create copy objects than replace reference objects
358 if (header.cty !== 'JWT') {
359 recurse = false
360 } else {
361 ;({ payload, header, signature, data } = decodeJWT(payload.jwt, false))
362 }
363 } while (recurse)
364
365 throw new Error(`${JWT_ERROR.INVALID_SIGNATURE}: no matching public key found`)
366}
367
368export function verifyJWSDecoded(
369 { header, data, signature }: JWSDecoded,
370 pubKeys: VerificationMethod | VerificationMethod[]
371): VerificationMethod {
372 if (!Array.isArray(pubKeys)) pubKeys = [pubKeys]
373 const signer: VerificationMethod = VerifierAlgorithm(header.alg)(data, signature, pubKeys)
374 return signer
375}
376
377/**
378 * Verifies given JWS. If the JWS is valid, returns the public key that was
379 * used to sign the JWS, or throws an `Error` if none of the `pubKeys` match.
380 *
381 * @example
382 * const pubKey = verifyJWS('eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJyZXF1Z....', { publicKeyHex: '0x12341...' })
383 *
384 * @param {String} jws A JWS string to verify
385 * @param {Array<VerificationMethod> | VerificationMethod} pubKeys The public keys used to verify the JWS
386 * @return {VerificationMethod} The public key used to sign the JWS
387 */
388export function verifyJWS(jws: string, pubKeys: VerificationMethod | VerificationMethod[]): VerificationMethod {
389 const jwsDecoded: JWSDecoded = decodeJWS(jws)
390 return verifyJWSDecoded(jwsDecoded, pubKeys)
391}
392
393/**
394 * Verifies given JWT. If the JWT is valid, the promise returns an object including the JWT, the payload of the JWT,
395 * and the DID document of the issuer of the JWT.
396 *
397 * @example
398 * ```ts
399 * verifyJWT(
400 * 'did:uport:eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJyZXF1Z....',
401 * {audience: '5A8bRWU3F7j3REx3vkJ...', callbackUrl: 'https://...'}
402 * ).then(obj => {
403 * const did = obj.did // DID of signer
404 * const payload = obj.payload
405 * const doc = obj.didResolutionResult.didDocument // DID Document of issuer
406 * const jwt = obj.jwt
407 * const signerKeyId = obj.signer.id // ID of key in DID document that signed JWT
408 * ...
409 * })
410 * ```
411 *
412 * @param {String} jwt a JSON Web Token to verify
413 * @param {Object} [options] an unsigned credential object
414 * @param {Boolean} options.auth Require signer to be listed in the authentication section of the
415 * DID document (for Authentication purposes)
416 * @param {String} options.audience DID of the recipient of the JWT
417 * @param {String} options.callbackUrl callback url in JWT
418 * @return {Promise<Object, Error>} a promise which resolves with a response object or rejects with an
419 * error
420 */
421export async function verifyJWT(
422 jwt: string,
423 options: JWTVerifyOptions = {
424 resolver: undefined,
425 auth: undefined,
426 audience: undefined,
427 callbackUrl: undefined,
428 skewTime: undefined,
429 proofPurpose: undefined,
430 policies: {},
431 didAuthenticator: undefined,
432 }
433): Promise<JWTVerified> {
434 if (!options.resolver) throw new Error('missing_resolver: No DID resolver has been configured')
435 const { payload, header, signature, data }: JWTDecoded = decodeJWT(jwt, false)
436 const proofPurpose: ProofPurposeTypes | undefined = Object.prototype.hasOwnProperty.call(options, 'auth')
437 ? options.auth
438 ? 'authentication'
439 : undefined
440 : options.proofPurpose
441
442 let didUrl: string | undefined
443
444 if (!payload.iss && !payload.client_id) {
445 throw new Error(`${JWT_ERROR.INVALID_JWT}: JWT iss or client_id are required`)
446 }
447
448 if (options.didAuthenticator) {
449 didUrl = options.didAuthenticator.issuer
450 } else if (payload.iss === SELF_ISSUED_V2 || payload.iss === SELF_ISSUED_V2_VC_INTEROP) {
451 if (!payload.sub) {
452 throw new Error(`${JWT_ERROR.INVALID_JWT}: JWT sub is required`)
453 }
454 if (typeof payload.sub_jwk === 'undefined') {
455 didUrl = payload.sub
456 } else {
457 didUrl = (header.kid || '').split('#')[0]
458 }
459 } else if (payload.iss === SELF_ISSUED_V0_1) {
460 if (!payload.did) {
461 throw new Error(`${JWT_ERROR.INVALID_JWT}: JWT did is required`)
462 }
463 didUrl = payload.did
464 } else if (!payload.iss && payload.scope === 'openid' && payload.redirect_uri) {
465 // SIOP Request payload
466 // https://identity.foundation/jwt-vc-presentation-profile/#self-issued-op-request-object
467 if (!payload.client_id) {
468 throw new Error(`${JWT_ERROR.INVALID_JWT}: JWT client_id is required`)
469 }
470 didUrl = payload.client_id
471 } else {
472 didUrl = payload.iss
473 }
474
475 if (!didUrl) {
476 throw new Error(`${JWT_ERROR.INVALID_JWT}: No DID has been found in the JWT`)
477 }
478
479 let authenticators: VerificationMethod[]
480 let issuer: string
481 let didResolutionResult: DIDResolutionResult
482 if (options.didAuthenticator) {
483 ;({ didResolutionResult, authenticators, issuer } = options.didAuthenticator)
484 } else {
485 ;({ didResolutionResult, authenticators, issuer } = await resolveAuthenticator(
486 options.resolver,
487 header.alg,
488 didUrl,
489 proofPurpose
490 ))
491 // Add to options object for recursive reference
492 options.didAuthenticator = { didResolutionResult, authenticators, issuer }
493 }
494
495 const { did } = parse(didUrl) as ParsedDID
496
497 let signer: VerificationMethod | null = null
498
499 if (did !== didUrl) {
500 const authenticator = authenticators.find((auth) => auth.id === didUrl)
501 if (!authenticator) {
502 throw new Error(`${JWT_ERROR.INVALID_JWT}: No authenticator found for did URL ${didUrl}`)
503 }
504
505 signer = await verifyProof(jwt, { payload, header, signature, data }, authenticator, options)
506 } else {
507 let i = 0
508 while (!signer && i < authenticators.length) {
509 const authenticator = authenticators[i]
510 try {
511 signer = await verifyProof(jwt, { payload, header, signature, data }, authenticator, options)
512 } catch (e) {
513 if (!(e as Error).message.includes(JWT_ERROR.INVALID_SIGNATURE) || i === authenticators.length - 1) throw e
514 }
515
516 i++
517 }
518 }
519
520 if (signer) {
521 const now: number = typeof options.policies?.now === 'number' ? options.policies.now : Math.floor(Date.now() / 1000)
522 const skewTime = typeof options.skewTime !== 'undefined' && options.skewTime >= 0 ? options.skewTime : NBF_SKEW
523
524 const nowSkewed = now + skewTime
525 if (options.policies?.nbf !== false && payload.nbf) {
526 if (payload.nbf > nowSkewed) {
527 throw new Error(`${JWT_ERROR.INVALID_JWT}: JWT not valid before nbf: ${payload.nbf}`)
528 }
529 } else if (options.policies?.iat !== false && payload.iat && payload.iat > nowSkewed) {
530 throw new Error(`${JWT_ERROR.INVALID_JWT}: JWT not valid yet (issued in the future) iat: ${payload.iat}`)
531 }
532 if (options.policies?.exp !== false && payload.exp && payload.exp <= now - skewTime) {
533 throw new Error(`${JWT_ERROR.INVALID_JWT}: JWT has expired: exp: ${payload.exp} < now: ${now}`)
534 }
535 if (options.policies?.aud !== false && payload.aud) {
536 if (!options.audience && !options.callbackUrl) {
537 throw new Error(
538 `${JWT_ERROR.INVALID_AUDIENCE}: JWT audience is required but your app address has not been configured`
539 )
540 }
541 const audArray = Array.isArray(payload.aud) ? payload.aud : [payload.aud]
542 const matchedAudience = audArray.find((item) => options.audience === item || options.callbackUrl === item)
543
544 if (typeof matchedAudience === 'undefined') {
545 throw new Error(`${JWT_ERROR.INVALID_AUDIENCE}: JWT audience does not match your DID or callback url`)
546 }
547 }
548
549 return { verified: true, payload, didResolutionResult, issuer, signer, jwt, policies: options.policies }
550 }
551 throw new Error(
552 `${JWT_ERROR.INVALID_SIGNATURE}: JWT not valid. issuer DID document does not contain a verificationMethod that matches the signature.`
553 )
554}
555
556/**
557 * Resolves relevant public keys or other authenticating material used to verify signature from the DID document of
558 * provided DID
559 *
560 * @example
561 * ```ts
562 * resolveAuthenticator(resolver, 'ES256K', 'did:uport:2nQtiQG6Cgm1GYTBaaKAgr76uY7iSexUkqX').then(obj => {
563 * const payload = obj.payload
564 * const profile = obj.profile
565 * const jwt = obj.jwt
566 * // ...
567 * })
568 * ```
569 *
570 * @param resolver - {Resolvable} a DID resolver function that can obtain the `DIDDocument` for the `issuer`
571 * @param alg - {String} a JWT algorithm
572 * @param issuer - {String} a Decentralized Identifier (DID) to lookup
573 * @param proofPurpose - {ProofPurposeTypes} *Optional* Use the verificationMethod linked in that section of the
574 * issuer DID document
575 * @return {Promise<DIDAuthenticator>} a promise which resolves with an object containing an array of authenticators
576 * or rejects with an error if none exist
577 */
578export async function resolveAuthenticator(
579 resolver: Resolvable,
580 alg: string,
581 issuer: string,
582 proofPurpose?: ProofPurposeTypes
583): Promise<DIDAuthenticator> {
584 const types: string[] = SUPPORTED_PUBLIC_KEY_TYPES[alg as KNOWN_JWA]
585 if (!types || types.length === 0) {
586 throw new Error(`${JWT_ERROR.NOT_SUPPORTED}: No supported signature types for algorithm ${alg}`)
587 }
588 let didResult: DIDResolutionResult
589
590 const result = (await resolver.resolve(issuer, { accept: DID_JSON })) as unknown
591 // support legacy resolvers that do not produce DIDResolutionResult
592 if (Object.getOwnPropertyNames(result).indexOf('didDocument') === -1) {
593 didResult = {
594 didDocument: result as DIDDocument,
595 didDocumentMetadata: {},
596 didResolutionMetadata: { contentType: DID_JSON },
597 }
598 } else {
599 didResult = result as DIDResolutionResult
600 }
601
602 if (didResult.didResolutionMetadata?.error || didResult.didDocument == null) {
603 const { error, message } = didResult.didResolutionMetadata
604 throw new Error(
605 `${JWT_ERROR.RESOLVER_ERROR}: Unable to resolve DID document for ${issuer}: ${error}, ${message || ''}`
606 )
607 }
608
609 const getPublicKeyById = (verificationMethods: VerificationMethod[], pubid?: string): VerificationMethod | null => {
610 const filtered = verificationMethods.filter(({ id }) => pubid === id)
611 return filtered.length > 0 ? filtered[0] : null
612 }
613
614 let publicKeysToCheck: VerificationMethod[] = [
615 ...(didResult?.didDocument?.verificationMethod || []),
616 ...(didResult?.didDocument?.publicKey || []),
617 ]
618 if (typeof proofPurpose === 'string') {
619 // support legacy DID Documents that do not list assertionMethod
620 if (
621 proofPurpose.startsWith('assertion') &&
622 !Object.getOwnPropertyNames(didResult?.didDocument).includes('assertionMethod')
623 ) {
624 didResult.didDocument = { ...(<DIDDocument>didResult.didDocument) }
625 didResult.didDocument.assertionMethod = [...publicKeysToCheck.map((pk) => pk.id)]
626 }
627
628 publicKeysToCheck = (didResult.didDocument[proofPurpose] || [])
629 .map((verificationMethod) => {
630 if (typeof verificationMethod === 'string') {
631 return getPublicKeyById(publicKeysToCheck, verificationMethod)
632 } else if (typeof (<LegacyVerificationMethod>verificationMethod).publicKey === 'string') {
633 // this is a legacy format
634 return getPublicKeyById(publicKeysToCheck, (<LegacyVerificationMethod>verificationMethod).publicKey)
635 } else {
636 return <VerificationMethod>verificationMethod
637 }
638 })
639 .filter((key) => key != null) as VerificationMethod[]
640 }
641
642 const authenticators: VerificationMethod[] = publicKeysToCheck.filter(({ type }) =>
643 types.find((supported) => supported === type)
644 )
645
646 if (typeof proofPurpose === 'string' && (!authenticators || authenticators.length === 0)) {
647 throw new Error(
648 `${JWT_ERROR.NO_SUITABLE_KEYS}: DID document for ${issuer} does not have public keys suitable for ${alg} with ${proofPurpose} purpose`
649 )
650 }
651 if (!authenticators || authenticators.length === 0) {
652 throw new Error(`${JWT_ERROR.NO_SUITABLE_KEYS}: DID document for ${issuer} does not have public keys for ${alg}`)
653 }
654 return { authenticators, issuer, didResolutionResult: didResult }
655}