Skip to content

Commit

Permalink
fix: createRemoteJWKSet handles all JWS syntaxes
Browse files Browse the repository at this point in the history
  • Loading branch information
panva committed Nov 11, 2021
1 parent 2e2b79d commit aaba8f3
Show file tree
Hide file tree
Showing 5 changed files with 36 additions and 30 deletions.
29 changes: 17 additions & 12 deletions src/jwks/remote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -164,25 +169,25 @@ 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) {
throw new JWKSMultipleMatchingKeys()
}

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() {
Expand Down
4 changes: 2 additions & 2 deletions test-browser/jwks.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 }))
Expand Down
4 changes: 2 additions & 2 deletions test-cloudflare-workers/cloudflare.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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')
},
Expand Down
9 changes: 5 additions & 4 deletions test-deno/jwks.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { assertThrowsAsync } from 'https://deno.land/[email protected]/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'

Expand All @@ -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:https://localhost:3000'), { timeoutDuration: 0 })
await assertThrowsAsync(
() => jwks({ alg: 'RS256' }, <any>null),
() => jwks({ alg: 'RS256' }, <FlattenedJWSInput>{}),
errors.JWKSTimeout,
'request timed out',
).finally(async () => {
Expand Down
20 changes: 10 additions & 10 deletions test/jwks/remote.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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',
})
Expand All @@ -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',
})
Expand All @@ -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',
})
})
Expand All @@ -261,7 +261,7 @@ test('handles ECONNREFUSED', async (t) => {
nock.enableNetConnect()
const url = new URL('http:https://localhost:3001/jwks')
const JWKS = createRemoteJWKSet(url)
await t.throwsAsync(JWKS({ alg: 'RS256' }), {
await t.throwsAsync(JWKS({ alg: 'RS256' }, {}), {
code: 'ECONNREFUSED',
})
})
Expand All @@ -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',
})
})
Expand All @@ -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',
})
})

0 comments on commit aaba8f3

Please sign in to comment.