-
Notifications
You must be signed in to change notification settings - Fork 2
Caching and expiry in DPoP token provider #30
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -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<string, CacheEntry> // TODO: Take cache from caller | ||||||||
|
|
||||||||
| constructor(callbackUri: string, getCodeCallback: GetCodeCallback, getIssuerCallback: GetIssuerCallback) { | ||||||||
| this.#getCode = getCodeCallback | ||||||||
|
|
@@ -20,6 +23,23 @@ export class DPoPTokenProvider implements TokenProvider { | |||||||
| } | ||||||||
|
|
||||||||
| async upgrade(request: Request): Promise<Request> { | ||||||||
| // TODO: More robust key via callback to support complex caching scenarios | ||||||||
| let tokenData = this.#cache.get(request.url) | ||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The request url is not necessarily the right cache key. I'd suggest a callback to map from request to key. For instance, we will often want this token to be shareable across all URLs in one or multiple storages.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||||
| // TODO: Support actively refreshing the token | ||||||||
| if (tokenData === undefined || isExpired(tokenData)) { | ||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||||
| 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<CacheEntry> { | ||||||||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All upgrade logic except caching is unchanged in this extracted method. |
||||||||
| const issuer = await this.#getIssuer(request) | ||||||||
|
|
||||||||
| const discoveryResponse = await oauth.discoveryRequest(issuer, {signal: request.signal}) | ||||||||
|
|
@@ -83,12 +103,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} | ||||||||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the newly extracted method we don't actually upgrade the request. |
||||||||
| } | ||||||||
|
|
||||||||
| private getClientAuth(issuer: string, client: oauth.OmitSymbolProperties<oauth.Client>): oauth.ClientAuth { | ||||||||
|
|
@@ -144,3 +159,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; | ||||||||
| } | ||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Locally represents what we cache.
Will move out of here once cache also moves out.