From 372435ca4aebfc577eebbdd3a2c5e9b71cb9961c Mon Sep 17 00:00:00 2001 From: Samu Lang Date: Wed, 19 Aug 2026 16:14:02 +0200 Subject: [PATCH 1/2] Caching and expiry in DPoP token provider --- src/DPoPTokenProvider.ts | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/src/DPoPTokenProvider.ts b/src/DPoPTokenProvider.ts index 4760ae2..2a13ef3 100644 --- a/src/DPoPTokenProvider.ts +++ b/src/DPoPTokenProvider.ts @@ -4,10 +4,13 @@ import type { GetCodeCallback } from "./GetCodeCallback.js" import type { TokenProvider } from "./TokenProvider.js" import type { GetIssuerCallback } from "./GetIssuerCallback.js" +type CacheEntry = { created: number, tokenResult: oauth.TokenEndpointResponse, dpopKey: CryptoKeyPair } + export class DPoPTokenProvider implements TokenProvider { readonly #getCode: GetCodeCallback readonly #callbackUri: string readonly #getIssuer: GetIssuerCallback + readonly #cache = new Map // TODO: Take cache from caller constructor(callbackUri: string, getCodeCallback: GetCodeCallback, getIssuerCallback: GetIssuerCallback) { this.#getCode = getCodeCallback @@ -20,6 +23,21 @@ export class DPoPTokenProvider implements TokenProvider { } async upgrade(request: Request): Promise { + let tokenData = this.#cache.get(request.url) + if (tokenData === undefined || isExpired(tokenData)) { + tokenData = await this.obtainToken(request) + this.#cache.set(request.url, tokenData) + } + + const headers = new Headers(request.headers) + + headers.set("DPoP", await DPoP.generateProof(tokenData.dpopKey, request.url, request.method, undefined, tokenData.tokenResult.access_token)) + headers.set("Authorization", ["DPoP", tokenData.tokenResult.access_token].join(" ")) + + return new Request(request, {headers}) + } + + private async obtainToken(request: Request): Promise { const issuer = await this.#getIssuer(request) const discoveryResponse = await oauth.discoveryRequest(issuer, {signal: request.signal}) @@ -83,12 +101,7 @@ export class DPoPTokenProvider implements TokenProvider { const tokenResult = await oauth.processAuthorizationCodeResponse(authorizationServer, clientRegistration, tokenResponse, {expectedNonce: this.nonceVerificationOverride(authorizationServer.issuer, nonce)}) - const headers = new Headers(request.headers) - - headers.set("DPoP", await DPoP.generateProof(dpopKey, request.url, request.method, undefined, tokenResult.access_token)) - headers.set("Authorization", ["DPoP", tokenResult.access_token].join(" ")) - - return new Request(request, {headers}) + return {created: Date.now(), tokenResult, dpopKey} } private getClientAuth(issuer: string, client: oauth.OmitSymbolProperties): oauth.ClientAuth { @@ -144,3 +157,9 @@ function clientSecretBasicFor(issuer: string): (clientSecret: string) => oauth.C return oauth.ClientSecretBasic } + +function isExpired(tokenData: CacheEntry) { + // TODO: Add some headroom (expire a bit before limit) + // TODO: What to do when `expires_in` is Missing? (optional in https://datatracker.ietf.org/doc/html/rfc6749#section-4.2.2) + return Date.now() - tokenData.created > tokenData.tokenResult.expires_in! * 1_000; +} From e8cd81976c29a76bac1fd7494d3e9689c77f2bf3 Mon Sep 17 00:00:00 2001 From: Samu Lang Date: Thu, 20 Aug 2026 16:31:32 +0200 Subject: [PATCH 2/2] Reminders Co-authored-by: Jesse Wright <63333554+jeswr@users.noreply.github.com> --- src/DPoPTokenProvider.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/DPoPTokenProvider.ts b/src/DPoPTokenProvider.ts index 2a13ef3..dea27d5 100644 --- a/src/DPoPTokenProvider.ts +++ b/src/DPoPTokenProvider.ts @@ -23,7 +23,9 @@ export class DPoPTokenProvider implements TokenProvider { } async upgrade(request: Request): Promise { + // TODO: More robust key via callback to support complex caching scenarios let tokenData = this.#cache.get(request.url) + // TODO: Support actively refreshing the token if (tokenData === undefined || isExpired(tokenData)) { tokenData = await this.obtainToken(request) this.#cache.set(request.url, tokenData)