mirror of
https://github.com/hanzoai/jose.git
synced 2026-10-30 14:46:24 +00:00
197 lines
5.2 KiB
TypeScript
197 lines
5.2 KiB
TypeScript
import type {
|
|
KeyLike,
|
|
JWSHeaderParameters,
|
|
JWK,
|
|
JSONWebKeySet,
|
|
FlattenedJWSInput,
|
|
} from '../types.d.ts'
|
|
import { importJWK } from '../key/import.ts'
|
|
import {
|
|
JWKSInvalid,
|
|
JOSENotSupported,
|
|
JWKSNoMatchingKey,
|
|
JWKSMultipleMatchingKeys,
|
|
} from '../util/errors.ts'
|
|
import isObject from '../lib/is_object.ts'
|
|
|
|
function getKtyFromAlg(alg: unknown) {
|
|
switch (typeof alg === 'string' && alg.slice(0, 2)) {
|
|
case 'RS':
|
|
case 'PS':
|
|
return 'RSA'
|
|
case 'ES':
|
|
return 'EC'
|
|
case 'Ed':
|
|
return 'OKP'
|
|
default:
|
|
throw new JOSENotSupported('Unsupported "alg" value for a JSON Web Key Set')
|
|
}
|
|
}
|
|
|
|
interface Cache<T extends KeyLike = KeyLike> {
|
|
[alg: string]: T
|
|
}
|
|
|
|
/** @private */
|
|
export function isJWKSLike(jwks: unknown): jwks is JSONWebKeySet {
|
|
return (
|
|
jwks &&
|
|
typeof jwks === 'object' &&
|
|
// @ts-expect-error
|
|
Array.isArray(jwks.keys) &&
|
|
// @ts-expect-error
|
|
jwks.keys.every(isJWKLike)
|
|
)
|
|
}
|
|
|
|
function isJWKLike(key: unknown) {
|
|
return isObject<JWK>(key)
|
|
}
|
|
|
|
function clone<T>(obj: T): T {
|
|
// @ts-ignore
|
|
if (typeof structuredClone === 'function') {
|
|
// @ts-ignore
|
|
return structuredClone(obj)
|
|
}
|
|
|
|
return JSON.parse(JSON.stringify(obj))
|
|
}
|
|
|
|
/** @private */
|
|
export class LocalJWKSet<T extends KeyLike = KeyLike> {
|
|
protected _jwks?: JSONWebKeySet
|
|
|
|
private _cached: WeakMap<JWK, Cache<T>> = new WeakMap()
|
|
|
|
constructor(jwks: unknown) {
|
|
if (!isJWKSLike(jwks)) {
|
|
throw new JWKSInvalid('JSON Web Key Set malformed')
|
|
}
|
|
|
|
this._jwks = clone<JSONWebKeySet>(jwks)
|
|
}
|
|
|
|
async getKey(protectedHeader?: JWSHeaderParameters, token?: FlattenedJWSInput): Promise<T> {
|
|
const { alg, kid } = { ...protectedHeader, ...token?.header }
|
|
const kty = getKtyFromAlg(alg)
|
|
|
|
const candidates = this._jwks!.keys.filter((jwk) => {
|
|
// filter keys based on the mapping of signature algorithms to Key Type
|
|
let candidate = kty === jwk.kty
|
|
|
|
// filter keys based on the JWK Key ID in the header
|
|
if (candidate && typeof kid === 'string') {
|
|
candidate = kid === jwk.kid
|
|
}
|
|
|
|
// filter keys based on the key's declared Algorithm
|
|
if (candidate && typeof jwk.alg === 'string') {
|
|
candidate = alg === jwk.alg
|
|
}
|
|
|
|
// filter keys based on the key's declared Public Key Use
|
|
if (candidate && typeof jwk.use === 'string') {
|
|
candidate = jwk.use === 'sig'
|
|
}
|
|
|
|
// filter keys based on the key's declared Key Operations
|
|
if (candidate && Array.isArray(jwk.key_ops)) {
|
|
candidate = jwk.key_ops.includes('verify')
|
|
}
|
|
|
|
// filter out non-applicable OKP Sub Types
|
|
if (candidate && alg === 'EdDSA') {
|
|
candidate = jwk.crv === 'Ed25519' || jwk.crv === 'Ed448'
|
|
}
|
|
|
|
// filter out non-applicable EC curves
|
|
if (candidate) {
|
|
switch (alg) {
|
|
case 'ES256':
|
|
candidate = jwk.crv === 'P-256'
|
|
break
|
|
case 'ES256K':
|
|
candidate = jwk.crv === 'secp256k1'
|
|
break
|
|
case 'ES384':
|
|
candidate = jwk.crv === 'P-384'
|
|
break
|
|
case 'ES512':
|
|
candidate = jwk.crv === 'P-521'
|
|
break
|
|
}
|
|
}
|
|
|
|
return candidate
|
|
})
|
|
|
|
const { 0: jwk, length } = candidates
|
|
|
|
if (length === 0) {
|
|
throw new JWKSNoMatchingKey()
|
|
} else if (length !== 1) {
|
|
const error = new JWKSMultipleMatchingKeys()
|
|
|
|
const { _cached } = this
|
|
error[Symbol.asyncIterator] = async function* () {
|
|
for (const jwk of candidates) {
|
|
try {
|
|
yield await importWithAlgCache<T>(_cached, jwk, alg!)
|
|
} catch {
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
throw error
|
|
}
|
|
|
|
return importWithAlgCache<T>(this._cached, jwk, alg!)
|
|
}
|
|
}
|
|
|
|
async function importWithAlgCache<T extends KeyLike = KeyLike>(
|
|
cache: WeakMap<JWK, Cache<T>>,
|
|
jwk: JWK,
|
|
alg: string,
|
|
) {
|
|
const cached = cache.get(jwk) || cache.set(jwk, {}).get(jwk)!
|
|
if (cached[alg] === undefined) {
|
|
const key = await importJWK<T>({ ...jwk, ext: true }, alg)
|
|
|
|
if (key instanceof Uint8Array || key.type !== 'public') {
|
|
throw new JWKSInvalid('JSON Web Key Set members must be public keys')
|
|
}
|
|
|
|
cached[alg] = key
|
|
}
|
|
|
|
return cached[alg]
|
|
}
|
|
|
|
/**
|
|
* Returns a function that resolves to a key object from a locally stored, or otherwise available,
|
|
* JSON Web Key Set.
|
|
*
|
|
* It uses the "alg" (JWS Algorithm) Header Parameter to determine the right JWK "kty" (Key Type),
|
|
* then proceeds to match the JWK "kid" (Key ID) with one found in the JWS Header Parameters (if
|
|
* there is one) while also respecting the JWK "use" (Public Key Use) and JWK "key_ops" (Key
|
|
* Operations) Parameters (if they are present on the JWK).
|
|
*
|
|
* Only a single public key must match the selection process. As shown in the example below when
|
|
* multiple keys get matched it is possible to opt-in to iterate over the matched keys and attempt
|
|
* verification in an iterative manner.
|
|
*
|
|
* @param jwks JSON Web Key Set formatted object.
|
|
*/
|
|
export function createLocalJWKSet<T extends KeyLike = KeyLike>(jwks: JSONWebKeySet) {
|
|
const set = new LocalJWKSet<T>(jwks)
|
|
return async function (
|
|
protectedHeader?: JWSHeaderParameters,
|
|
token?: FlattenedJWSInput,
|
|
): Promise<T> {
|
|
return set.getKey(protectedHeader, token)
|
|
}
|
|
}
|