diff --git a/src/Cache.ts b/src/Cache.ts new file mode 100644 index 0000000..f94a0a9 --- /dev/null +++ b/src/Cache.ts @@ -0,0 +1,5 @@ +export interface Cache> { + getItem(key: string): Promise + setItem(key: string, value: T): Promise + removeItem(key: string): Promise +} diff --git a/src/CachingAuthorizationServerProvider.ts b/src/CachingAuthorizationServerProvider.ts index 267db11..abc1931 100644 --- a/src/CachingAuthorizationServerProvider.ts +++ b/src/CachingAuthorizationServerProvider.ts @@ -1,8 +1,10 @@ +import type { Cache } from "./Cache.js" +import { MemoryCache } from "./MemoryCache.js" import type { AuthorizationServerProvider } from "./AuthorizationServerProvider.js" import type * as oauth from "oauth4webapi" export class CachingAuthorizationServerProvider implements AuthorizationServerProvider { - readonly #cache = new Map // TODO: Take cache from caller + readonly #cache: Cache = new MemoryCache // TODO: Take cache from caller readonly #original: AuthorizationServerProvider constructor(original: AuthorizationServerProvider) { @@ -10,13 +12,13 @@ export class CachingAuthorizationServerProvider implements AuthorizationServerPr } async getAuthorizationServer(request: Request): Promise { - const cached = this.#cache.get(request.url) - if (cached !== undefined) { + const cached = await this.#cache.getItem(request.url) + if (cached !== null) { return cached } const fresh = await this.#original.getAuthorizationServer(request) - this.#cache.set(request.url, fresh) + await this.#cache.setItem(request.url, fresh) return fresh } } diff --git a/src/CachingClientProvider.ts b/src/CachingClientProvider.ts index ddda929..fbd0f09 100644 --- a/src/CachingClientProvider.ts +++ b/src/CachingClientProvider.ts @@ -1,8 +1,10 @@ +import type { Cache } from "./Cache.js" +import { MemoryCache } from "./MemoryCache.js" import type { ClientProvider } from "./ClientProvider.js" import type * as oauth from "oauth4webapi" export class CachingClientProvider implements ClientProvider { - readonly #cache = new Map // TODO: Take cache from caller + readonly #cache: Cache = new MemoryCache // TODO: Take cache from caller readonly #original: ClientProvider constructor(original: ClientProvider) { @@ -10,13 +12,13 @@ export class CachingClientProvider implements ClientProvider { } async getClient(as: oauth.AuthorizationServer, redirectUri: string, signal: AbortSignal): Promise { - const cached = this.#cache.get(as.issuer) - if (cached !== undefined) { + const cached = await this.#cache.getItem(as.issuer) + if (cached !== null) { return cached } const fresh = await this.#original.getClient(as, redirectUri, signal) - this.#cache.set(as.issuer, fresh) + await this.#cache.setItem(as.issuer, fresh) return fresh } } diff --git a/src/CachingIssuerProvider.ts b/src/CachingIssuerProvider.ts index 992342f..64efb4d 100644 --- a/src/CachingIssuerProvider.ts +++ b/src/CachingIssuerProvider.ts @@ -1,7 +1,9 @@ +import type { Cache } from "./Cache.js" +import { MemoryCache } from "./MemoryCache.js" import { IssuerProvider } from "./IssuerProvider.js" export class CachingIssuerProvider implements IssuerProvider { - readonly #cache = new Map // TODO: Take cache from caller + readonly #cache: Cache = new MemoryCache // TODO: Take cache from caller readonly #original: IssuerProvider constructor(original: IssuerProvider) { @@ -9,13 +11,13 @@ export class CachingIssuerProvider implements IssuerProvider { } async getIssuer(request: Request): Promise { - const cached = this.#cache.get(request.url) - if (cached !== undefined) { + const cached = await this.#cache.getItem(request.url) + if (cached !== null) { return cached } const fresh = await this.#original.getIssuer(request) - this.#cache.set(request.url, fresh) + await this.#cache.setItem(request.url, fresh) return fresh } } diff --git a/src/DPoPTokenProvider.ts b/src/DPoPTokenProvider.ts index d577955..574f552 100644 --- a/src/DPoPTokenProvider.ts +++ b/src/DPoPTokenProvider.ts @@ -5,6 +5,8 @@ import type { TokenProvider } from "./TokenProvider.js" import type { AuthorizationServerProvider } from "./AuthorizationServerProvider.js" import { ClientProvider } from "./ClientProvider.js" import { supportsOfflineAccess } from "./supportsOfflineAccess.js" +import type { Cache } from "./Cache.js" +import { MemoryCache } from "./MemoryCache.js" type CacheEntry = { created: number, @@ -20,7 +22,7 @@ export class DPoPTokenProvider implements TokenProvider { // TODO: Take cache from caller // TODO: Once cache is externalized, document that it should not be shared between clients (which would lead to impersonation) - readonly #cache = new Map + readonly #cache: Cache = new MemoryCache readonly #asProvider: AuthorizationServerProvider readonly #clientProvider: ClientProvider @@ -52,23 +54,23 @@ export class DPoPTokenProvider implements TokenProvider { private async getCachedToken(request: Request): Promise { // TODO: More robust key via callback to support complex caching scenarios - const cached = this.#cache.get(request.url) + const cached = await this.#cache.getItem(request.url) // TODO: Support actively refreshing the token - if (cached !== undefined) { + if (cached !== null) { if (!isExpired(cached)) { return cached } const refreshed = await this.refreshToken(cached, request) if (refreshed !== undefined) { - this.#cache.set(request.url, refreshed) + await this.#cache.setItem(request.url, refreshed) return refreshed } } const fresh = await this.obtainToken(request) - this.#cache.set(request.url, fresh) + await this.#cache.setItem(request.url, fresh) return fresh } @@ -155,7 +157,7 @@ export class DPoPTokenProvider implements TokenProvider { const tokenResponse = await oauth.refreshTokenGrantRequest(cached.authorizationServer, cached.client, this.getClientAuth(cached.authorizationServer.issuer, cached.client), cached.tokenResult.refresh_token, options) tokenResult = await oauth.processRefreshTokenResponse(cached.authorizationServer, cached.client, tokenResponse) } catch (e) { - this.#cache.delete(request.url) + await this.#cache.removeItem(request.url) if (e instanceof oauth.ResponseBodyError && e.error === "invalid_grant") { console.debug("Access token could not be refreshed") diff --git a/src/MemoryCache.ts b/src/MemoryCache.ts new file mode 100644 index 0000000..4c5203b --- /dev/null +++ b/src/MemoryCache.ts @@ -0,0 +1,17 @@ +import type { Cache } from "./Cache.js" + +export class MemoryCache> implements Cache { + readonly #values = new Map + + async getItem(key: string): Promise { + return this.#values.get(key) ?? null + } + + async setItem(key: string, value: T): Promise { + this.#values.set(key, value) + } + + async removeItem(key: string): Promise { + this.#values.delete(key) + } +}