From b9257ccd07fdc54f4ea0953eb92a41a406ba2aca Mon Sep 17 00:00:00 2001 From: Samu Lang Date: Thu, 10 Sep 2026 13:47:02 +0100 Subject: [PATCH 1/4] Style --- src/DPoPTokenProvider.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/DPoPTokenProvider.ts b/src/DPoPTokenProvider.ts index 4e1b84c..cd3a39e 100644 --- a/src/DPoPTokenProvider.ts +++ b/src/DPoPTokenProvider.ts @@ -6,7 +6,11 @@ import type { AuthorizationServerProvider } from "./AuthorizationServerProvider. import { ClientProvider } from "./ClientProvider.js" import { supportsOfflineAccess } from "./supportsOfflineAccess.js" -type CacheEntry = { created: number, tokenResult: oauth.TokenEndpointResponse, dpopKey: CryptoKeyPair } +type CacheEntry = { + created: number, + tokenResult: oauth.TokenEndpointResponse, + dpopKey: CryptoKeyPair, +} export class DPoPTokenProvider implements TokenProvider { readonly #codeProvider: CodeProvider From 332064f9902213ae02d0d695cb6d89f5c1b119dd Mon Sep 17 00:00:00 2001 From: Samu Lang Date: Wed, 9 Sep 2026 12:19:27 +0100 Subject: [PATCH 2/4] Extract --- src/DPoPTokenProvider.ts | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/DPoPTokenProvider.ts b/src/DPoPTokenProvider.ts index cd3a39e..d040985 100644 --- a/src/DPoPTokenProvider.ts +++ b/src/DPoPTokenProvider.ts @@ -31,20 +31,29 @@ export class DPoPTokenProvider implements TokenProvider { } async upgrade(request: Request): Promise { + const {dpopKey, tokenResult: {access_token}} = await this.getCachedToken(request) + + const headers = new Headers(request.headers) + + headers.set("DPoP", await DPoP.generateProof(dpopKey, request.url, request.method, undefined, access_token)) + headers.set("Authorization", ["DPoP", access_token].join(" ")) + + return new Request(request, {headers}) + } + + private async getCachedToken(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) } - 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}) + return tokenData; } + private async obtainToken(request: Request): Promise { const authorizationServer = await this.#asProvider.getAuthorizationServer(request) From bff1eb8a6af1d22a294b3c35bc8fb44ea295b1a1 Mon Sep 17 00:00:00 2001 From: Samu Lang Date: Wed, 9 Sep 2026 12:26:47 +0100 Subject: [PATCH 3/4] Rearrange --- src/DPoPTokenProvider.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/DPoPTokenProvider.ts b/src/DPoPTokenProvider.ts index d040985..8197c58 100644 --- a/src/DPoPTokenProvider.ts +++ b/src/DPoPTokenProvider.ts @@ -43,15 +43,17 @@ export class DPoPTokenProvider implements TokenProvider { private async getCachedToken(request: Request): Promise { // TODO: More robust key via callback to support complex caching scenarios - let tokenData = this.#cache.get(request.url) + const cached = 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) + if (cached !== undefined && !isExpired(cached)) { + return cached } - return tokenData; + const fresh = await this.obtainToken(request) + this.#cache.set(request.url, fresh) + + return fresh } private async obtainToken(request: Request): Promise { From d74f833095dea0f97bd3224f030ed4efc73cdc81 Mon Sep 17 00:00:00 2001 From: Samu Lang Date: Thu, 10 Sep 2026 17:27:47 +0100 Subject: [PATCH 4/4] Refresh --- src/DPoPTokenProvider.ts | 41 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/src/DPoPTokenProvider.ts b/src/DPoPTokenProvider.ts index 8197c58..cc67506 100644 --- a/src/DPoPTokenProvider.ts +++ b/src/DPoPTokenProvider.ts @@ -10,6 +10,7 @@ type CacheEntry = { created: number, tokenResult: oauth.TokenEndpointResponse, dpopKey: CryptoKeyPair, + client: oauth.Client, } export class DPoPTokenProvider implements TokenProvider { @@ -50,6 +51,12 @@ export class DPoPTokenProvider implements TokenProvider { return cached } + const refreshed = await this.refreshToken(request) + if (refreshed !== undefined) { + this.#cache.set(request.url, refreshed) + return refreshed + } + const fresh = await this.obtainToken(request) this.#cache.set(request.url, fresh) @@ -121,7 +128,39 @@ export class DPoPTokenProvider implements TokenProvider { const tokenResult = await oauth.processAuthorizationCodeResponse(authorizationServer, clientRegistration, tokenResponse, {expectedNonce: this.nonceVerificationOverride(authorizationServer.issuer, nonce)}) - return {created: Date.now(), tokenResult, dpopKey} + return {created: Date.now(), tokenResult, dpopKey, client: clientRegistration} + } + + private async refreshToken(request: Request): Promise { + const cached = this.#cache.get(request.url) + if (cached === undefined) { + return undefined + } + + if (cached.tokenResult.refresh_token === undefined) { + return undefined + } + + const authorizationServer = await this.#asProvider.getAuthorizationServer(request) + const dpop = oauth.DPoP({}, cached.dpopKey) + const options = {DPoP: dpop, signal: request.signal} + + const tokenResponse = await oauth.refreshTokenGrantRequest(authorizationServer, cached.client, this.getClientAuth(authorizationServer.issuer, cached.client), cached.tokenResult.refresh_token, options) + + let tokenResult: oauth.TokenEndpointResponse + try { + tokenResult = await oauth.processRefreshTokenResponse(authorizationServer, cached.client, tokenResponse) + } catch (e) { + if (e instanceof oauth.ResponseBodyError && e.error === "invalid_grant") { + console.debug("Access token could not be refreshed") + + return undefined + } + + throw e + } + + return {created: Date.now(), tokenResult, dpopKey: cached.dpopKey, client: cached.client} } private getClientAuth(issuer: string, client: oauth.OmitSymbolProperties): oauth.ClientAuth {