Skip to content

Commit

Permalink
feat: allow setting expected JWT algorithms in validateJwtAccessToken
Browse files Browse the repository at this point in the history
  • Loading branch information
panva committed Sep 24, 2024
1 parent 1edb1be commit e75e641
Show file tree
Hide file tree
Showing 3 changed files with 72 additions and 25 deletions.
9 changes: 9 additions & 0 deletions docs/interfaces/ValidateJWTAccessTokenOptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,12 @@ A 5000ms timeout AbortSignal for every request
```js
const signal = () => AbortSignal.timeout(5_000) // Note: AbortSignal.timeout may not yet be available in all runtimes.
```

***

### signingAlgorithms?

`optional` **signingAlgorithms**: `string`[]

Supported (or expected) JWT "alg" header parameter values for the JWT Access Token (and DPoP
Proof JWTs). Default is [JWSAlgorithm](../type-aliases/JWSAlgorithm.md)
58 changes: 43 additions & 15 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2998,6 +2998,7 @@ export async function processUserInfoResponse(
undefined,
client.userinfo_signed_response_alg,
as.userinfo_signing_alg_values_supported,
undefined,
),
noSignatureCheck,
getClockSkew(client),
Expand Down Expand Up @@ -3429,6 +3430,7 @@ async function processGenericAccessTokenResponse(
undefined,
client.id_token_signed_response_alg,
as.id_token_signing_alg_values_supported,
'RS256',
),
noSignatureCheck,
getClockSkew(client),
Expand Down Expand Up @@ -4288,6 +4290,7 @@ export async function processIntrospectionResponse(
undefined,
client.introspection_signed_response_alg,
as.introspection_signing_alg_values_supported,
'RS256',
),
noSignatureCheck,
getClockSkew(client),
Expand Down Expand Up @@ -4679,6 +4682,7 @@ export async function validateJwtAuthResponse(
undefined,
client.authorization_signed_response_alg,
as.authorization_signing_alg_values_supported,
'RS256',
),
getPublicSigKeyFromIssuerJwksUri.bind(undefined, as, options),
getClockSkew(client),
Expand Down Expand Up @@ -4872,6 +4876,7 @@ export async function validateDetachedSignatureResponse(
undefined,
client.id_token_signed_response_alg,
as.id_token_signing_alg_values_supported,
'RS256',
),
getPublicSigKeyFromIssuerJwksUri.bind(undefined, as, options),
getClockSkew(client),
Expand Down Expand Up @@ -4964,18 +4969,19 @@ export async function validateDetachedSignatureResponse(
return result
}

// TODO: only do default for ID Tokens or specs that actually define RS256 as the default
/**
* If configured must be the configured one (client) if not configured must be signalled by the
* issuer to be supported (issuer) if not signalled must be fallback
* If configured must be the configured one (client), if not configured must be signalled by the
* issuer to be supported (issuer), if not signalled may be a default fallback, otherwise its a
* failure
*/
function checkSigningAlgorithm(
client: string | undefined,
client: string | string[] | undefined,
issuer: string[] | undefined,
fallback: string | string[] | undefined,
header: CompactJWSHeaderParameters,
) {
if (client !== undefined) {
if (header.alg !== client) {
if (typeof client === 'string' ? header.alg !== client : !client.includes(header.alg)) {
throw new OPE('unexpected JWT "alg" header parameter', {
code: INVALID_RESPONSE,
cause: { header, expected: client, reason: 'client configuration' },
Expand All @@ -4994,12 +5000,20 @@ function checkSigningAlgorithm(
return
}

if (header.alg !== 'RS256') {
throw new OPE('unexpected JWT "alg" header parameter', {
code: INVALID_RESPONSE,
cause: { header, expected: 'RS256', reason: 'default value' },
})
if (fallback !== undefined) {
if (typeof fallback === 'string' ? header.alg !== fallback : !fallback.includes(header.alg)) {
throw new OPE('unexpected JWT "alg" header parameter', {
code: INVALID_RESPONSE,
cause: { header, expected: fallback, reason: 'default value' },
})
}
return
}

throw new OPE(
'missing client or server configuration to verify used JWT "alg" header parameter',
{ cause: { client, issuer, fallback } },
)
}

/**
Expand Down Expand Up @@ -5473,6 +5487,12 @@ export interface ValidateJWTAccessTokenOptions extends HttpRequestOptions<'GET'>
* See {@link clockTolerance}.
*/
[clockTolerance]?: number

/**
* Supported (or expected) JWT "alg" header parameter values for the JWT Access Token (and DPoP
* Proof JWTs). Default is {@link JWSAlgorithm}
*/
signingAlgorithms?: string[]
}

function normalizeHtu(htu: string) {
Expand All @@ -5483,11 +5503,13 @@ function normalizeHtu(htu: string) {
}

async function validateDPoP(
as: AuthorizationServer,
request: Request,
accessToken: string,
accessTokenClaims: JWTPayload,
options?: Pick<ValidateJWTAccessTokenOptions, typeof clockSkew | typeof clockTolerance>,
options?: Pick<
ValidateJWTAccessTokenOptions,
typeof clockSkew | typeof clockTolerance | 'signingAlgorithms'
>,
) {
const headerValue = request.headers.get('dpop')
if (headerValue === null) {
Expand Down Expand Up @@ -5522,8 +5544,9 @@ async function validateDPoP(
headerValue,
checkSigningAlgorithm.bind(
undefined,
options?.signingAlgorithms,
undefined,
as?.dpop_signing_alg_values_supported || SUPPORTED_JWS_ALGS,
SUPPORTED_JWS_ALGS,
),
async (header) => {
const { jwk, alg } = header
Expand Down Expand Up @@ -5717,7 +5740,12 @@ export async function validateJwtAccessToken(

const { claims } = await validateJwt(
accessToken,
checkSigningAlgorithm.bind(undefined, undefined, SUPPORTED_JWS_ALGS),
checkSigningAlgorithm.bind(
undefined,
options?.signingAlgorithms,
undefined,
SUPPORTED_JWS_ALGS,
),
getPublicSigKeyFromIssuerJwksUri.bind(undefined, as, options),
getClockSkew(options),
getClockTolerance(options),
Expand Down Expand Up @@ -5768,7 +5796,7 @@ export async function validateJwtAccessToken(
claims.cnf?.jkt !== undefined ||
request.headers.has('dpop')
) {
await validateDPoP(as, request, accessToken, claims, options)
await validateDPoP(request, accessToken, claims, options)
}

return claims as JWTAccessTokenClaims
Expand Down
30 changes: 20 additions & 10 deletions test/userinfo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,15 +240,20 @@ test('processUserInfoResponse() - jwt (alg default)', async (t) => {
}
const kp = t.context.RS256

await t.notThrowsAsync(async () => {
const response = getResponse(
await new jose.SignJWT({ sub: 'urn:example:subject' })
.setProtectedHeader({ alg: 'RS256' })
.sign(kp.privateKey),
{ headers: new Headers({ 'content-type': 'application/jwt' }) },
)
await lib.processUserInfoResponse(tIssuer, client, 'urn:example:subject', response)
})
await t.throwsAsync(
async () => {
const response = getResponse(
await new jose.SignJWT({ sub: 'urn:example:subject' })
.setProtectedHeader({ alg: 'RS256' })
.sign(kp.privateKey),
{ headers: new Headers({ 'content-type': 'application/jwt' }) },
)
await lib.processUserInfoResponse(tIssuer, client, 'urn:example:subject', response)
},
{
message: 'missing client or server configuration to verify used JWT "alg" header parameter',
},
)
})

test('processUserInfoResponse() - alg mismatches', async (t) => {
Expand All @@ -265,7 +270,12 @@ test('processUserInfoResponse() - alg mismatches', async (t) => {
.sign(t.context.ES256.privateKey),
{ headers: new Headers({ 'content-type': 'application/jwt' }) },
)
await lib.processUserInfoResponse(tIssuer, client, 'urn:example:subject', response)
await lib.processUserInfoResponse(
tIssuer,
{ ...client, userinfo_signed_response_alg: 'PS256' },
'urn:example:subject',
response,
)
},
{ message: 'unexpected JWT "alg" header parameter' },
)
Expand Down

0 comments on commit e75e641

Please sign in to comment.