mirror of
https://github.com/danbulant/jose
synced 2026-05-19 04:18:52 +00:00
fix: createRemoteJWKSet handles all JWS syntaxes
This commit is contained in:
parent
2e2b79d486
commit
aaba8f3000
5 changed files with 36 additions and 30 deletions
|
|
@ -103,23 +103,28 @@ class RemoteJWKSet {
|
|||
return Date.now() < this._cooldownStarted + this._cooldownDuration
|
||||
}
|
||||
|
||||
async getKey(protectedHeader: JWSHeaderParameters): Promise<KeyLike> {
|
||||
async getKey(protectedHeader: JWSHeaderParameters, token: FlattenedJWSInput): Promise<KeyLike> {
|
||||
const joseHeader = {
|
||||
...protectedHeader,
|
||||
...token.header,
|
||||
}
|
||||
|
||||
if (!this._jwks) {
|
||||
await this.reload()
|
||||
}
|
||||
|
||||
const candidates = this._jwks!.keys.filter((jwk) => {
|
||||
// filter keys based on the mapping of signature algorithms to Key Type
|
||||
let candidate = jwk.kty === getKtyFromAlg(protectedHeader.alg)
|
||||
let candidate = jwk.kty === getKtyFromAlg(joseHeader.alg)
|
||||
|
||||
// filter keys based on the JWK Key ID in the header
|
||||
if (candidate && typeof protectedHeader.kid === 'string') {
|
||||
candidate = protectedHeader.kid === jwk.kid
|
||||
if (candidate && typeof joseHeader.kid === 'string') {
|
||||
candidate = joseHeader.kid === jwk.kid
|
||||
}
|
||||
|
||||
// filter keys based on the key's declared Algorithm
|
||||
if (candidate && typeof jwk.alg === 'string') {
|
||||
candidate = protectedHeader.alg === jwk.alg
|
||||
candidate = joseHeader.alg === jwk.alg
|
||||
}
|
||||
|
||||
// filter keys based on the key's declared Public Key Use
|
||||
|
|
@ -133,13 +138,13 @@ class RemoteJWKSet {
|
|||
}
|
||||
|
||||
// filter out non-applicable OKP Sub Types
|
||||
if (candidate && protectedHeader.alg === 'EdDSA') {
|
||||
if (candidate && joseHeader.alg === 'EdDSA') {
|
||||
candidate = jwk.crv === 'Ed25519' || jwk.crv === 'Ed448'
|
||||
}
|
||||
|
||||
// filter out non-applicable EC curves
|
||||
if (candidate) {
|
||||
switch (protectedHeader.alg) {
|
||||
switch (joseHeader.alg) {
|
||||
case 'ES256':
|
||||
candidate = jwk.crv === 'P-256'
|
||||
break
|
||||
|
|
@ -164,7 +169,7 @@ class RemoteJWKSet {
|
|||
if (length === 0) {
|
||||
if (this.coolingDown() === false) {
|
||||
await this.reload()
|
||||
return this.getKey(protectedHeader)
|
||||
return this.getKey(joseHeader, token)
|
||||
}
|
||||
throw new JWKSNoMatchingKey()
|
||||
} else if (length !== 1) {
|
||||
|
|
@ -172,17 +177,17 @@ class RemoteJWKSet {
|
|||
}
|
||||
|
||||
const cached = this._cached.get(jwk) || this._cached.set(jwk, {}).get(jwk)!
|
||||
if (cached[protectedHeader.alg!] === undefined) {
|
||||
const keyObject = await importJWK({ ...jwk, ext: true }, protectedHeader.alg!)
|
||||
if (cached[joseHeader.alg!] === undefined) {
|
||||
const keyObject = await importJWK({ ...jwk, ext: true }, joseHeader.alg!)
|
||||
|
||||
if (keyObject instanceof Uint8Array || keyObject.type !== 'public') {
|
||||
throw new JWKSInvalid('JSON Web Key Set members must be public keys')
|
||||
}
|
||||
|
||||
cached[protectedHeader.alg!] = keyObject
|
||||
cached[joseHeader.alg!] = keyObject
|
||||
}
|
||||
|
||||
return cached[protectedHeader.alg!]
|
||||
return cached[joseHeader.alg!]
|
||||
}
|
||||
|
||||
async reload() {
|
||||
|
|
|
|||
|
|
@ -7,11 +7,11 @@ QUnit.test('fetches the JWKSet', async (assert) => {
|
|||
const { alg, kid } = response.keys[0]
|
||||
const jwks = createRemoteJWKSet(new URL(jwksUri))
|
||||
await assert.rejects(
|
||||
jwks({ alg: 'RS256' }),
|
||||
jwks({ alg: 'RS256' }, {}),
|
||||
'multiple matching keys found in the JSON Web Key Set',
|
||||
)
|
||||
await assert.rejects(
|
||||
jwks({ kid: 'foo', alg: 'RS256' }),
|
||||
jwks({ kid: 'foo', alg: 'RS256' }, {}),
|
||||
'no applicable key found in the JSON Web Key Set',
|
||||
)
|
||||
assert.ok(await jwks({ alg, kid }))
|
||||
|
|
|
|||
|
|
@ -346,13 +346,13 @@ test('createRemoteJWKSet', macro, async () => {
|
|||
const response = await fetch(jwksUri).then((r) => r.json())
|
||||
const { alg, kid } = response.keys[0]
|
||||
const jwks = jose.createRemoteJWKSet(new URL(jwksUri))
|
||||
await jwks({ alg, kid })
|
||||
await jwks({ alg, kid }, {})
|
||||
})
|
||||
|
||||
test('remote jwk set timeout', macro, async () => {
|
||||
const jwksUri = 'https://www.googleapis.com/oauth2/v3/certs'
|
||||
const jwks = jose.createRemoteJWKSet(new URL(jwksUri), { timeoutDuration: 0 })
|
||||
await jwks({ alg: 'RS256' }).then(
|
||||
await jwks({ alg: 'RS256' }, {}).then(
|
||||
() => {
|
||||
throw new Error('should fail')
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { assertThrowsAsync } from 'https://deno.land/std@0.109.0/testing/asserts.ts'
|
||||
|
||||
import { createRemoteJWKSet, errors } from '../dist/deno/index.ts'
|
||||
import type { FlattenedJWSInput } from '../dist/deno/index.ts'
|
||||
|
||||
const jwksUri = 'https://www.googleapis.com/oauth2/v3/certs'
|
||||
|
||||
|
|
@ -9,23 +10,23 @@ Deno.test('fetches the JWKSet', async () => {
|
|||
const { alg, kid } = response.keys[0]
|
||||
const jwks = createRemoteJWKSet(new URL(jwksUri))
|
||||
await assertThrowsAsync(
|
||||
() => jwks({ alg: 'RS256' }, <any>null),
|
||||
() => jwks({ alg: 'RS256' }, <FlattenedJWSInput>{}),
|
||||
errors.JWKSMultipleMatchingKeys,
|
||||
'multiple matching keys found in the JSON Web Key Set',
|
||||
)
|
||||
await assertThrowsAsync(
|
||||
() => jwks({ kid: 'foo', alg: 'RS256' }, <any>null),
|
||||
() => jwks({ kid: 'foo', alg: 'RS256' }, <FlattenedJWSInput>{}),
|
||||
errors.JWKSNoMatchingKey,
|
||||
'no applicable key found in the JSON Web Key Set',
|
||||
)
|
||||
await jwks({ alg, kid }, <any>null)
|
||||
await jwks({ alg, kid }, <FlattenedJWSInput>{})
|
||||
})
|
||||
|
||||
Deno.test('timeout', async () => {
|
||||
const server = Deno.listen({ port: 3000 })
|
||||
const jwks = createRemoteJWKSet(new URL('http://localhost:3000'), { timeoutDuration: 0 })
|
||||
await assertThrowsAsync(
|
||||
() => jwks({ alg: 'RS256' }, <any>null),
|
||||
() => jwks({ alg: 'RS256' }, <FlattenedJWSInput>{}),
|
||||
errors.JWKSTimeout,
|
||||
'request timed out',
|
||||
).finally(async () => {
|
||||
|
|
|
|||
|
|
@ -209,19 +209,19 @@ test.serial('throws on invalid JWKSet', async (t) => {
|
|||
|
||||
const url = new URL('https://as.example.com/jwks')
|
||||
const JWKS = createRemoteJWKSet(url)
|
||||
await t.throwsAsync(JWKS({ alg: 'RS256' }), {
|
||||
await t.throwsAsync(JWKS({ alg: 'RS256' }, {}), {
|
||||
code: 'ERR_JWKS_INVALID',
|
||||
message: 'JSON Web Key Set malformed',
|
||||
})
|
||||
|
||||
scope.get('/jwks').once().reply(200, {})
|
||||
await t.throwsAsync(JWKS({ alg: 'RS256' }), {
|
||||
await t.throwsAsync(JWKS({ alg: 'RS256' }, {}), {
|
||||
code: 'ERR_JWKS_INVALID',
|
||||
message: 'JSON Web Key Set malformed',
|
||||
})
|
||||
|
||||
scope.get('/jwks').once().reply(200, { keys: null })
|
||||
await t.throwsAsync(JWKS({ alg: 'RS256' }), {
|
||||
await t.throwsAsync(JWKS({ alg: 'RS256' }, {}), {
|
||||
code: 'ERR_JWKS_INVALID',
|
||||
message: 'JSON Web Key Set malformed',
|
||||
})
|
||||
|
|
@ -230,19 +230,19 @@ test.serial('throws on invalid JWKSet', async (t) => {
|
|||
.get('/jwks')
|
||||
.once()
|
||||
.reply(200, { keys: [null] })
|
||||
await t.throwsAsync(JWKS({ alg: 'RS256' }), {
|
||||
await t.throwsAsync(JWKS({ alg: 'RS256' }, {}), {
|
||||
code: 'ERR_JWKS_INVALID',
|
||||
message: 'JSON Web Key Set malformed',
|
||||
})
|
||||
|
||||
scope.get('/jwks').once().reply(404)
|
||||
await t.throwsAsync(JWKS({ alg: 'RS256' }), {
|
||||
await t.throwsAsync(JWKS({ alg: 'RS256' }, {}), {
|
||||
code: 'ERR_JOSE_GENERIC',
|
||||
message: 'Expected 200 OK from the JSON Web Key Set HTTP response',
|
||||
})
|
||||
|
||||
scope.get('/jwks').once().reply(200, '{')
|
||||
await t.throwsAsync(JWKS({ alg: 'RS256' }), {
|
||||
await t.throwsAsync(JWKS({ alg: 'RS256' }, {}), {
|
||||
code: 'ERR_JOSE_GENERIC',
|
||||
message: 'Failed to parse the JSON Web Key Set HTTP response as JSON',
|
||||
})
|
||||
|
|
@ -252,7 +252,7 @@ test('handles ENOTFOUND', async (t) => {
|
|||
nock.enableNetConnect()
|
||||
const url = new URL('https://op.example.com/jwks')
|
||||
const JWKS = createRemoteJWKSet(url)
|
||||
await t.throwsAsync(JWKS({ alg: 'RS256' }), {
|
||||
await t.throwsAsync(JWKS({ alg: 'RS256' }, {}), {
|
||||
code: 'ENOTFOUND',
|
||||
})
|
||||
})
|
||||
|
|
@ -261,7 +261,7 @@ test('handles ECONNREFUSED', async (t) => {
|
|||
nock.enableNetConnect()
|
||||
const url = new URL('http://localhost:3001/jwks')
|
||||
const JWKS = createRemoteJWKSet(url)
|
||||
await t.throwsAsync(JWKS({ alg: 'RS256' }), {
|
||||
await t.throwsAsync(JWKS({ alg: 'RS256' }, {}), {
|
||||
code: 'ECONNREFUSED',
|
||||
})
|
||||
})
|
||||
|
|
@ -273,7 +273,7 @@ test('handles ECONNRESET', async (t) => {
|
|||
socket.destroy()
|
||||
})
|
||||
const JWKS = createRemoteJWKSet(url)
|
||||
await t.throwsAsync(JWKS({ alg: 'RS256' }), {
|
||||
await t.throwsAsync(JWKS({ alg: 'RS256' }, {}), {
|
||||
code: 'ECONNRESET',
|
||||
})
|
||||
})
|
||||
|
|
@ -285,7 +285,7 @@ test('handles a timeout', async (t) => {
|
|||
const JWKS = createRemoteJWKSet(url, {
|
||||
timeoutDuration: 500,
|
||||
})
|
||||
await t.throwsAsync(JWKS({ alg: 'RS256' }), {
|
||||
await t.throwsAsync(JWKS({ alg: 'RS256' }, {}), {
|
||||
code: 'ERR_JWKS_TIMEOUT',
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in a new issue