@@ -4,7 +4,6 @@ import type {
4
4
JWK ,
5
5
JSONWebKeySet ,
6
6
FlattenedJWSInput ,
7
- GetKeyFunction ,
8
7
} from '../types.d'
9
8
import { importJWK } from '../key/import.js'
10
9
import {
@@ -73,8 +72,8 @@ export class LocalJWKSet {
73
72
this . _jwks = clone < JSONWebKeySet > ( jwks )
74
73
}
75
74
76
- async getKey ( protectedHeader : JWSHeaderParameters , token : FlattenedJWSInput ) : Promise < KeyLike > {
77
- const { alg, kid } = { ...protectedHeader , ...token . header }
75
+ async getKey ( protectedHeader ? : JWSHeaderParameters , token ? : FlattenedJWSInput ) : Promise < KeyLike > {
76
+ const { alg, kid } = { ...protectedHeader , ...token ? .header }
78
77
const kty = getKtyFromAlg ( alg )
79
78
80
79
const candidates = this . _jwks ! . keys . filter ( ( jwk ) => {
@@ -132,29 +131,53 @@ export class LocalJWKSet {
132
131
if ( length === 0 ) {
133
132
throw new JWKSNoMatchingKey ( )
134
133
} else if ( length !== 1 ) {
135
- throw new JWKSMultipleMatchingKeys ( )
134
+ const error = new JWKSMultipleMatchingKeys ( )
135
+
136
+ const { _cached } = this
137
+ error [ Symbol . asyncIterator ] = async function * ( ) {
138
+ for ( const jwk of candidates ) {
139
+ try {
140
+ yield await importWithAlgCache ( _cached , jwk , alg ! )
141
+ } catch {
142
+ continue
143
+ }
144
+ }
145
+ }
146
+
147
+ throw error
136
148
}
137
149
138
- const cached = this . _cached . get ( jwk ) || this . _cached . set ( jwk , { } ) . get ( jwk ) !
139
- if ( cached [ alg ! ] === undefined ) {
140
- const keyObject = await importJWK ( { ... jwk , ext : true } , alg )
150
+ return importWithAlgCache ( this . _cached , jwk , alg ! )
151
+ }
152
+ }
141
153
142
- if ( keyObject instanceof Uint8Array || keyObject . type !== 'public' ) {
143
- throw new JWKSInvalid ( 'JSON Web Key Set members must be public keys' )
144
- }
154
+ async function importWithAlgCache ( cache : WeakMap < JWK , Cache > , jwk : JWK , alg : string ) {
155
+ const cached = cache . get ( jwk ) || cache . set ( jwk , { } ) . get ( jwk ) !
156
+ if ( cached [ alg ] === undefined ) {
157
+ const keyObject = < KeyLike > await importJWK ( { ...jwk , ext : true } , alg )
145
158
146
- cached [ alg ! ] = keyObject
159
+ if ( keyObject . type !== 'public' ) {
160
+ throw new JWKSInvalid ( 'JSON Web Key Set members must be public keys' )
147
161
}
148
162
149
- return cached [ alg ! ]
163
+ cached [ alg ] = keyObject
150
164
}
165
+
166
+ return cached [ alg ]
151
167
}
152
168
153
169
/**
154
170
* Returns a function that resolves to a key object from a locally stored, or otherwise available,
155
171
* JSON Web Key Set.
156
172
*
157
- * Only a single public key must match the selection process.
173
+ * It uses the "alg" (JWS Algorithm) Header Parameter to determine the right JWK "kty" (Key Type),
174
+ * then proceeds to match the JWK "kid" (Key ID) with one found in the JWS Header Parameters (if
175
+ * there is one) while also respecting the JWK "use" (Public Key Use) and JWK "key_ops" (Key
176
+ * Operations) Parameters (if they are present on the JWK).
177
+ *
178
+ * Only a single public key must match the selection process. As shown in the example below when
179
+ * multiple keys get matched it is possible to opt-in to iterate over the matched keys and attempt
180
+ * verification in an iterative manner.
158
181
*
159
182
* @example Usage
160
183
*
@@ -185,10 +208,38 @@ export class LocalJWKSet {
185
208
* console.log(payload)
186
209
* ```
187
210
*
211
+ * @example Opting-in to multiple JWKS matches using `createLocalJWKSet`
212
+ *
213
+ * ```js
214
+ * const options = {
215
+ * issuer: 'urn:example:issuer',
216
+ * audience: 'urn:example:audience',
217
+ * }
218
+ * const { payload, protectedHeader } = await jose
219
+ * .jwtVerify(jwt, JWKS, options)
220
+ * .catch(async (error) => {
221
+ * if (error?.code === 'ERR_JWKS_MULTIPLE_MATCHING_KEYS') {
222
+ * for await (const publicKey of error) {
223
+ * try {
224
+ * return await jose.jwtVerify(jwt, publicKey, options)
225
+ * } catch (innerError) {
226
+ * if (innerError?.code === 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED') {
227
+ * continue
228
+ * }
229
+ * throw innerError
230
+ * }
231
+ * }
232
+ * throw new jose.errors.JWSSignatureVerificationFailed()
233
+ * }
234
+ *
235
+ * throw error
236
+ * })
237
+ * console.log(protectedHeader)
238
+ * console.log(payload)
239
+ * ```
240
+ *
188
241
* @param jwks JSON Web Key Set formatted object.
189
242
*/
190
- export function createLocalJWKSet (
191
- jwks : JSONWebKeySet ,
192
- ) : GetKeyFunction < JWSHeaderParameters , FlattenedJWSInput > {
243
+ export function createLocalJWKSet ( jwks : JSONWebKeySet ) {
193
244
return LocalJWKSet . prototype . getKey . bind ( new LocalJWKSet ( jwks ) )
194
245
}
0 commit comments