[go: nahoru, domu]

Skip to content

Commit

Permalink
feat: add experimental support for edge compute runtimes JWKS caching
Browse files Browse the repository at this point in the history
Refs: #551
Refs: #661
Refs: #653
Refs: #415
  • Loading branch information
panva committed Jun 26, 2024
1 parent 15a61d4 commit ab166e2
Show file tree
Hide file tree
Showing 7 changed files with 223 additions and 2 deletions.
26 changes: 26 additions & 0 deletions docs/interfaces/jwks_remote.ExportedJWKSCache.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# 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).

---

## Table of contents

### Properties

- [jwks](jwks_remote.ExportedJWKSCache.md#jwks)
- [uat](jwks_remote.ExportedJWKSCache.md#uat)

## Properties

### jwks

**jwks**: [`JSONWebKeySet`](types.JSONWebKeySet.md)

___

### uat

**uat**: `number`
9 changes: 9 additions & 0 deletions docs/interfaces/jwks_remote.RemoteJWKSetOptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Options for the remote JSON Web Key Set.

### Properties

- [[experimental\_jwksCache]](jwks_remote.RemoteJWKSetOptions.md#[experimental_jwkscache])
- [agent](jwks_remote.RemoteJWKSetOptions.md#agent)
- [cacheMaxAge](jwks_remote.RemoteJWKSetOptions.md#cachemaxage)
- [cooldownDuration](jwks_remote.RemoteJWKSetOptions.md#cooldownduration)
Expand All @@ -20,6 +21,14 @@ Options for the remote JSON Web Key Set.

## Properties

### [experimental\_jwksCache]

`Optional` **[experimental\_jwksCache]**: [`JWKSCacheInput`](../types/jwks_remote.JWKSCacheInput.md)

See [experimental_jwksCache](../variables/jwks_remote.experimental_jwksCache.md).

___

### agent

`Optional` **agent**: `any`
Expand Down
9 changes: 9 additions & 0 deletions docs/modules/jwks_remote.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,13 @@ Support from the community to continue maintaining and improving this module is

### Interfaces

- [ExportedJWKSCache](../interfaces/jwks_remote.ExportedJWKSCache.md)
- [RemoteJWKSetOptions](../interfaces/jwks_remote.RemoteJWKSetOptions.md)

### Type Aliases

- [JWKSCacheInput](../types/jwks_remote.JWKSCacheInput.md)

### Variables

- [experimental\_jwksCache](../variables/jwks_remote.experimental_jwksCache.md)
9 changes: 9 additions & 0 deletions docs/types/jwks_remote.JWKSCacheInput.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Type alias: JWKSCacheInput

## [💗 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).

---

Ƭ **JWKSCacheInput**: [`ExportedJWKSCache`](../interfaces/jwks_remote.ExportedJWKSCache.md) \| [`Record`]( https://www.typescriptlang.org/docs/handbook/utility-types.html#recordkeys-type )\<`string`, `never`\>
64 changes: 64 additions & 0 deletions docs/variables/jwks_remote.experimental_jwksCache.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Variable: experimental\_jwksCache

## [💗 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).

---

`Const` **experimental\_jwksCache**: unique `symbol`

This is an experimental feature, it is not subject to semantic versioning rules. Non-backward
compatible changes or removal may occur in any future release.

DANGER ZONE - This option 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.

When passed to [createRemoteJWKSet](../functions/jwks_remote.createRemoteJWKSet.md) this allows the passed in
object to:

- Serve as an initial value for the JSON Web Key Set that the module would otherwise need to
trigger an HTTP request for
- Have the JSON Web Key Set the function optionally ended up triggering an HTTP request for
assigned to it as properties

The intended use pattern is:

- Before verifying with [createRemoteJWKSet](../functions/jwks_remote.createRemoteJWKSet.md) you pull the
previously cached object from a low-latency key-value store offered by the cloud computing
runtime it is executed on;
- Default to an empty object `{}` instead when there's no previously cached value;
- Pass it in as [[experimental_jwksCache]](../interfaces/jwks_remote.RemoteJWKSetOptions.md);
- Afterwards, update the key-value storage if the [`uat`](../interfaces/jwks_remote.ExportedJWKSCache.md#uat) property of
the object has changed.

**`Example`**

```ts
import * as jose from 'jose'

// Prerequisites
let url!: URL
let jwt!: string

// Load JSON Web Key Set cache
const jwksCache: jose.JWKSCacheInput = (await getPreviouslyCachedJWKS()) || {}
const { uat } = jwksCache

const JWKS = jose.createRemoteJWKSet(url, {
[jose.experimental_jwksCache]: jwksCache,
})

// Use JSON Web Key Set cache
await jose.jwtVerify(jwt, JWKS)

if (uat !== jwksCache.uat) {
// Update JSON Web Key Set cache
await storeNewJWKScache(jwksCache)
}
```
4 changes: 2 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ export { calculateJwkThumbprint, calculateJwkThumbprintUri } from './jwk/thumbpr
export { EmbeddedJWK } from './jwk/embedded.js'

export { createLocalJWKSet } from './jwks/local.js'
export { createRemoteJWKSet } from './jwks/remote.js'
export type { RemoteJWKSetOptions } from './jwks/remote.js'
export { createRemoteJWKSet, experimental_jwksCache } from './jwks/remote.js'
export type { RemoteJWKSetOptions, JWKSCacheInput, ExportedJWKSCache } from './jwks/remote.js'

export { UnsecuredJWT } from './jwt/unsecured.js'
export type { UnsecuredResult } from './jwt/unsecured.js'
Expand Down
104 changes: 104 additions & 0 deletions src/jwks/remote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { KeyLike, JWSHeaderParameters, FlattenedJWSInput, JSONWebKeySet } f
import { JWKSNoMatchingKey } from '../util/errors.js'

import { createLocalJWKSet } from './local.js'
import isObject from '../lib/is_object.js'

function isCloudflareWorkers() {
return (
Expand All @@ -27,6 +28,64 @@ if (typeof navigator === 'undefined' || !navigator.userAgent?.startsWith?.('Mozi
USER_AGENT = `${NAME}/${VERSION}`
}

/**
* This is an experimental feature, it is not subject to semantic versioning rules. Non-backward
* compatible changes or removal may occur in any future release.
*
* DANGER ZONE - This option 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.
*
* When passed to {@link jwks/remote.createRemoteJWKSet createRemoteJWKSet} this allows the passed in
* object to:
*
* - Serve as an initial value for the JSON Web Key Set that the module would otherwise need to
* trigger an HTTP request for
* - Have the JSON Web Key Set the function optionally ended up triggering an HTTP request for
* assigned to it as properties
*
* The intended use pattern is:
*
* - Before verifying with {@link jwks/remote.createRemoteJWKSet createRemoteJWKSet} you pull the
* previously cached object from a low-latency key-value store offered by the cloud computing
* runtime it is executed on;
* - Default to an empty object `{}` instead when there's no previously cached value;
* - Pass it in as {@link RemoteJWKSetOptions[experimental_jwksCache]};
* - Afterwards, update the key-value storage if the {@link ExportedJWKSCache.uat `uat`} property of
* the object has changed.
*
* @example
*
* ```ts
* import * as jose from 'jose'
*
* // Prerequisites
* let url!: URL
* let jwt!: string
*
* // Load JSON Web Key Set cache
* const jwksCache: jose.JWKSCacheInput = (await getPreviouslyCachedJWKS()) || {}
* const { uat } = jwksCache
*
* const JWKS = jose.createRemoteJWKSet(url, {
* [jose.experimental_jwksCache]: jwksCache,
* })
*
* // Use JSON Web Key Set cache
* await jose.jwtVerify(jwt, JWKS)
*
* if (uat !== jwksCache.uat) {
* // Update JSON Web Key Set cache
* await storeNewJWKScache(jwksCache)
* }
* ```
*/
export const experimental_jwksCache: unique symbol = Symbol()

/** Options for the remote JSON Web Key Set. */
export interface RemoteJWKSetOptions {
/**
Expand Down Expand Up @@ -63,6 +122,37 @@ export interface RemoteJWKSetOptions {
* configuration would cause an unnecessary CORS preflight request.
*/
headers?: Record<string, string>

/** See {@link experimental_jwksCache}. */
[experimental_jwksCache]?: JWKSCacheInput
}

export interface ExportedJWKSCache {
jwks: JSONWebKeySet
uat: number
}

export type JWKSCacheInput = ExportedJWKSCache | Record<string, never>

function isFreshJwksCache(input: unknown, cacheMaxAge: number): input is ExportedJWKSCache {
if (typeof input !== 'object' || input === null) {
return false
}

if (!('uat' in input) || typeof input.uat !== 'number' || Date.now() - input.uat >= cacheMaxAge) {
return false
}

if (
!('jwks' in input) ||
!isObject<JSONWebKeySet>(input.jwks) ||
!Array.isArray(input.jwks.keys) ||
!Array.prototype.every.call(input.jwks.keys, isObject)
) {
return false
}

return true
}

class RemoteJWKSet<KeyLikeType extends KeyLike = KeyLike> {
Expand All @@ -82,6 +172,8 @@ class RemoteJWKSet<KeyLikeType extends KeyLike = KeyLike> {

private _local!: ReturnType<typeof createLocalJWKSet<KeyLikeType>>

private _cache?: JWKSCacheInput

constructor(url: unknown, options?: RemoteJWKSetOptions) {
if (!(url instanceof URL)) {
throw new TypeError('url must be an instance of URL')
Expand All @@ -93,6 +185,14 @@ class RemoteJWKSet<KeyLikeType extends KeyLike = KeyLike> {
this._cooldownDuration =
typeof options?.cooldownDuration === 'number' ? options?.cooldownDuration : 30000
this._cacheMaxAge = typeof options?.cacheMaxAge === 'number' ? options?.cacheMaxAge : 600000

if (options?.[experimental_jwksCache] !== undefined) {
this._cache = options?.[experimental_jwksCache]
if (isFreshJwksCache(options?.[experimental_jwksCache], this._cacheMaxAge)) {
this._jwksTimestamp = this._cache.uat
this._local = createLocalJWKSet(this._cache.jwks)
}
}
}

coolingDown() {
Expand Down Expand Up @@ -144,6 +244,10 @@ class RemoteJWKSet<KeyLikeType extends KeyLike = KeyLike> {
this._pendingFetch ||= fetchJwks(this._url, this._timeoutDuration, this._options)
.then((json) => {
this._local = createLocalJWKSet(<JSONWebKeySet>(<unknown>json))
if (this._cache) {
this._cache.uat = Date.now()
this._cache.jwks = <JSONWebKeySet>(<unknown>json)
}
this._jwksTimestamp = Date.now()
this._pendingFetch = undefined
})
Expand Down

0 comments on commit ab166e2

Please sign in to comment.