Skip to content

Commit

Permalink
feat: add JWKS Cache management for use in non-persistent runtimes
Browse files Browse the repository at this point in the history
  • Loading branch information
panva committed Oct 17, 2024
1 parent 2cd11f2 commit cda4b53
Show file tree
Hide file tree
Showing 8 changed files with 250 additions and 5 deletions.
5 changes: 5 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ Support from the community to continue maintaining and improving this module is
- [enableDecryptingResponses](functions/enableDecryptingResponses.md)
- [enableDetachedSignatureResponseChecks](functions/enableDetachedSignatureResponseChecks.md)
- [enableNonRepudiationChecks](functions/enableNonRepudiationChecks.md)
- [getJwksCache](functions/getJwksCache.md)
- [setJwksCache](functions/setJwksCache.md)
- [useCodeIdTokenResponseType](functions/useCodeIdTokenResponseType.md)
- [useJwtResponseMode](functions/useJwtResponseMode.md)

Expand Down Expand Up @@ -103,9 +105,12 @@ Support from the community to continue maintaining and improving this module is
- [DiscoveryRequestOptions](interfaces/DiscoveryRequestOptions.md)
- [DPoPHandle](interfaces/DPoPHandle.md)
- [DPoPOptions](interfaces/DPoPOptions.md)
- [ExportedJWKSCache](interfaces/ExportedJWKSCache.md)
- [GenerateKeyPairOptions](interfaces/GenerateKeyPairOptions.md)
- [IDToken](interfaces/IDToken.md)
- [IntrospectionResponse](interfaces/IntrospectionResponse.md)
- [JWK](interfaces/JWK.md)
- [JWKS](interfaces/JWKS.md)
- [ModifyAssertionFunction](interfaces/ModifyAssertionFunction.md)
- [ModifyAssertionOptions](interfaces/ModifyAssertionOptions.md)
- [MTLSEndpointAliases](interfaces/MTLSEndpointAliases.md)
Expand Down
32 changes: 32 additions & 0 deletions docs/functions/getJwksCache.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Function: getJwksCache()

[💗 Help the project](https://github.com/sponsors/panva)

Support from the community to continue maintaining and improving this module is welcome. If you find the module useful, please consider supporting the project by [becoming a sponsor](https://github.com/sponsors/panva).

***

**getJwksCache**(`config`): [`ExportedJWKSCache`](../interfaces/ExportedJWKSCache.md) \| `undefined`

This function can be used to export the JSON Web Key Set and the timestamp at
which it was last fetched if the client used the
[authorization server's JWK Set](../interfaces/ServerMetadata.md#jwks_uri) to validate
digital signatures.

This function is intended for cloud computing runtimes that cannot keep an in
memory cache between their code's invocations. Use in runtimes where an in
memory cache between requests is available is not desirable.

Note: the client only uses the authorization server's JWK Set when
[enableNonRepudiationChecks](enableNonRepudiationChecks.md), [useJwtResponseMode](useJwtResponseMode.md), or
[useCodeIdTokenResponseType](useCodeIdTokenResponseType.md) is used.

## Parameters

| Parameter | Type |
| ------ | ------ |
| `config` | [`Configuration`](../classes/Configuration.md) |

## Returns

[`ExportedJWKSCache`](../interfaces/ExportedJWKSCache.md) \| `undefined`
28 changes: 28 additions & 0 deletions docs/functions/setJwksCache.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Function: setJwksCache()

[💗 Help the project](https://github.com/sponsors/panva)

Support from the community to continue maintaining and improving this module is welcome. If you find the module useful, please consider supporting the project by [becoming a sponsor](https://github.com/sponsors/panva).

***

**setJwksCache**(`config`, `jwksCache`): `void`

DANGER ZONE - Use of this function has security implications that must be
understood, assessed for applicability, and accepted before use. It is
critical that the JSON Web Key Set cache only be writable by your own code.

This option is intended for cloud computing runtimes that cannot keep an in
memory cache between their code's invocations. Use in runtimes where an in
memory cache between requests is available is not desirable.

## Parameters

| Parameter | Type | Description |
| ------ | ------ | ------ |
| `config` | [`Configuration`](../classes/Configuration.md) | - |
| `jwksCache` | [`ExportedJWKSCache`](../interfaces/ExportedJWKSCache.md) | JWKS Cache previously obtained from [getJwksCache](getJwksCache.md) |

## Returns

`void`
19 changes: 19 additions & 0 deletions docs/interfaces/ExportedJWKSCache.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Interface: ExportedJWKSCache

[💗 Help the project](https://github.com/sponsors/panva)

Support from the community to continue maintaining and improving this module is welcome. If you find the module useful, please consider supporting the project by [becoming a sponsor](https://github.com/sponsors/panva).

***

## Properties

### jwks

**jwks**: [`JWKS`](JWKS.md)

***

### uat

**uat**: `number`
71 changes: 71 additions & 0 deletions docs/interfaces/JWK.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Interface: JWK

[💗 Help the project](https://github.com/sponsors/panva)

Support from the community to continue maintaining and improving this module is welcome. If you find the module useful, please consider supporting the project by [becoming a sponsor](https://github.com/sponsors/panva).

***

## Indexable

\[`parameter`: `string`\]: [`JsonValue`](../type-aliases/JsonValue.md) \| `undefined`

## Properties

### alg?

`readonly` `optional` **alg**: `string`

***

### crv?

`readonly` `optional` **crv**: `string`

***

### e?

`readonly` `optional` **e**: `string`

***

### key\_ops?

`readonly` `optional` **key\_ops**: `string`[]

***

### kid?

`readonly` `optional` **kid**: `string`

***

### kty?

`readonly` `optional` **kty**: `string`

***

### n?

`readonly` `optional` **n**: `string`

***

### use?

`readonly` `optional` **use**: `string`

***

### x?

`readonly` `optional` **x**: `string`

***

### y?

`readonly` `optional` **y**: `string`
13 changes: 13 additions & 0 deletions docs/interfaces/JWKS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Interface: JWKS

[💗 Help the project](https://github.com/sponsors/panva)

Support from the community to continue maintaining and improving this module is welcome. If you find the module useful, please consider supporting the project by [becoming a sponsor](https://github.com/sponsors/panva).

***

## Properties

### keys

`readonly` **keys**: [`JWK`](JWK.md)[]
60 changes: 57 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ interface Internal {
hybrid?: HybridImplementation
nonRepudiation?: NonRepudiationImplementation
decrypt?: oauth.JweDecryptFunction
jwksCache: oauth.JWKSCacheInput
}

const int = (config: Configuration) => {
Expand All @@ -57,13 +58,16 @@ export {
type AuthorizationDetails,
type ConfirmationClaims,
type DeviceAuthorizationResponse,
type ExportedJWKSCache,
type GenerateKeyPairOptions,
type IDToken,
type IntrospectionResponse,
type JsonArray,
type JsonObject,
type JsonPrimitive,
type JsonValue,
type JWK,
type JWKS,
type JWSAlgorithm,
type ModifyAssertionFunction,
type ModifyAssertionOptions,
Expand Down Expand Up @@ -1640,6 +1644,7 @@ export class Configuration
c,
auth,
tlsOnly: true,
jwksCache: {},
})
}

Expand Down Expand Up @@ -2060,6 +2065,52 @@ export function allowInsecureRequests(config: Configuration) {
int(config).tlsOnly = false
}

/**
* DANGER ZONE - Use of this function has security implications that must be
* understood, assessed for applicability, and accepted before use. It is
* critical that the JSON Web Key Set cache only be writable by your own code.
*
* This option is intended for cloud computing runtimes that cannot keep an in
* memory cache between their code's invocations. Use in runtimes where an in
* memory cache between requests is available is not desirable.
*
* @param jwksCache JWKS Cache previously obtained from {@link getJwksCache}
*
* @group Advanced Configuration
*/
export function setJwksCache(
config: Configuration,
jwksCache: oauth.ExportedJWKSCache,
) {
int(config).jwksCache = structuredClone(jwksCache)
}

/**
* This function can be used to export the JSON Web Key Set and the timestamp at
* which it was last fetched if the client used the
* {@link ServerMetadata.jwks_uri authorization server's JWK Set} to validate
* digital signatures.
*
* This function is intended for cloud computing runtimes that cannot keep an in
* memory cache between their code's invocations. Use in runtimes where an in
* memory cache between requests is available is not desirable.
*
* Note: the client only uses the authorization server's JWK Set when
* {@link enableNonRepudiationChecks}, {@link useJwtResponseMode}, or
* {@link useCodeIdTokenResponseType} is used.
*
* @group Advanced Configuration
*/
export function getJwksCache(
config: Configuration,
): oauth.ExportedJWKSCache | undefined {
const cache = int(config).jwksCache
if (cache.uat) {
return cache as oauth.ExportedJWKSCache
}
return undefined
}

/**
* Enables validating the JWS Signature of either a JWT {@link !Response.body} or
* {@link TokenEndpointResponse.id_token} of a processed {@link !Response} such as
Expand Down Expand Up @@ -2116,13 +2167,14 @@ export function enableNonRepudiationChecks(config: Configuration) {
checkConfig(config)

int(config).nonRepudiation = (response) => {
const { as, fetch, tlsOnly, timeout } = int(config)
const { as, fetch, tlsOnly, timeout, jwksCache } = int(config)
return oauth
.validateApplicationLevelSignature(as, response, {
[oauth.customFetch]: fetch,
[oauth.allowInsecureRequests]: !tlsOnly,
headers: new Headers(headers),
signal: signal(timeout),
[oauth.jwksCache]: jwksCache,
})
.catch(errorHandler)
}
Expand Down Expand Up @@ -2565,14 +2617,15 @@ async function validateJARMResponse(
authorizationResponse: URL,
expectedState: string | typeof skipStateCheck | undefined,
): Promise<URLSearchParams> {
const { as, c, fetch, tlsOnly, timeout, decrypt } = int(config)
const { as, c, fetch, tlsOnly, timeout, decrypt, jwksCache } = int(config)
return oauth
.validateJwtAuthResponse(as, c, authorizationResponse, expectedState, {
[oauth.customFetch]: fetch,
[oauth.allowInsecureRequests]: !tlsOnly,
headers: new Headers(headers),
signal: signal(timeout),
[oauth.jweDecrypt]: decrypt,
[oauth.jwksCache]: jwksCache,
})
.catch(errorHandler)
}
Expand All @@ -2599,7 +2652,7 @@ async function validateCodeIdTokenResponse(
)
}

const { as, c, fetch, tlsOnly, timeout, decrypt } = int(config)
const { as, c, fetch, tlsOnly, timeout, decrypt, jwksCache } = int(config)

return (
fapi
Expand All @@ -2611,6 +2664,7 @@ async function validateCodeIdTokenResponse(
headers: new Headers(headers),
signal: signal(timeout),
[oauth.jweDecrypt]: decrypt,
[oauth.jwksCache]: jwksCache,
}).catch(errorHandler)
}

Expand Down
27 changes: 25 additions & 2 deletions tap/end2end.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export default (QUnit: QUnit) => {
| 'dpop'
| 'jwtUserinfo'
| 'hybrid'
| 'nonrepudiation'
| 'encryption'
| 'login'
>
Expand All @@ -31,6 +32,7 @@ export default (QUnit: QUnit) => {
hybrid: false,
login: false,
encryption: false,
nonrepudiation: false,
}
for (const flag of flags) {
conf[flag] = true
Expand All @@ -40,6 +42,8 @@ export default (QUnit: QUnit) => {

const testCases = [
options(),
options('nonrepudiation'),
options('nonrepudiation', 'encryption'),
options('par'),
options('jar'),
options('dpop'),
Expand All @@ -55,7 +59,16 @@ export default (QUnit: QUnit) => {
]

for (const config of testCases) {
const { jarm, par, jar, dpop, jwtUserinfo, hybrid, encryption } = config
const {
jarm,
par,
jar,
dpop,
jwtUserinfo,
hybrid,
encryption,
nonrepudiation,
} = config

function label(config: Record<string, boolean>) {
const keys = Object.keys(
Expand Down Expand Up @@ -89,10 +102,12 @@ export default (QUnit: QUnit) => {
)

const execute: Array<(config: lib.Configuration) => void> = [
lib.enableNonRepudiationChecks,
lib.allowInsecureRequests,
]

if (nonrepudiation) {
execute.push(lib.enableNonRepudiationChecks)
}
if (jarm) {
execute.push(lib.useJwtResponseMode)
}
Expand Down Expand Up @@ -222,6 +237,14 @@ export default (QUnit: QUnit) => {
DPoP,
})

if (jarm || hybrid || nonrepudiation) {
const cache = lib.getJwksCache(client)
t.ok(cache?.uat)
t.ok(cache?.jwks)
} else {
t.notOk(lib.getJwksCache(client))
}

t.ok(1)
})
}
Expand Down

0 comments on commit cda4b53

Please sign in to comment.