UNPKG

@auth0/auth0-spa-js

Version:

Auth0 SDK for Single Page Applications using Authorization Code Grant Flow with PKCE

211 lines (172 loc) 5.74 kB
import { DEFAULT_NOW_PROVIDER } from '../constants'; import { CacheKeyManifest } from './key-manifest'; import { CacheEntry, ICache, CacheKey, CACHE_KEY_PREFIX, WrappedCacheEntry, DecodedToken, CACHE_KEY_ID_TOKEN_SUFFIX, IdTokenEntry } from './shared'; const DEFAULT_EXPIRY_ADJUSTMENT_SECONDS = 0; export class CacheManager { private nowProvider: () => number | Promise<number>; constructor( private cache: ICache, private keyManifest?: CacheKeyManifest, nowProvider?: () => number | Promise<number> ) { this.nowProvider = nowProvider || DEFAULT_NOW_PROVIDER; } async setIdToken( clientId: string, idToken: string, decodedToken: DecodedToken ): Promise<void> { const cacheKey = this.getIdTokenCacheKey(clientId); await this.cache.set(cacheKey, { id_token: idToken, decodedToken }); await this.keyManifest?.add(cacheKey); } async getIdToken(cacheKey: CacheKey): Promise<IdTokenEntry | undefined> { const entry = await this.cache.get<IdTokenEntry>( this.getIdTokenCacheKey(cacheKey.clientId) ); if (!entry && cacheKey.scope && cacheKey.audience) { const entryByScope = await this.get(cacheKey); if (!entryByScope) { return; } if (!entryByScope.id_token || !entryByScope.decodedToken) { return; } return { id_token: entryByScope.id_token, decodedToken: entryByScope.decodedToken }; } if (!entry) { return; } return { id_token: entry.id_token, decodedToken: entry.decodedToken }; } async get( cacheKey: CacheKey, expiryAdjustmentSeconds = DEFAULT_EXPIRY_ADJUSTMENT_SECONDS ): Promise<Partial<CacheEntry> | undefined> { let wrappedEntry = await this.cache.get<WrappedCacheEntry>( cacheKey.toKey() ); if (!wrappedEntry) { const keys = await this.getCacheKeys(); if (!keys) return; const matchedKey = this.matchExistingCacheKey(cacheKey, keys); if (matchedKey) { wrappedEntry = await this.cache.get<WrappedCacheEntry>(matchedKey); } } // If we still don't have an entry, exit. if (!wrappedEntry) { return; } const now = await this.nowProvider(); const nowSeconds = Math.floor(now / 1000); if (wrappedEntry.expiresAt - expiryAdjustmentSeconds < nowSeconds) { if (wrappedEntry.body.refresh_token) { wrappedEntry.body = { refresh_token: wrappedEntry.body.refresh_token }; await this.cache.set(cacheKey.toKey(), wrappedEntry); return wrappedEntry.body; } await this.cache.remove(cacheKey.toKey()); await this.keyManifest?.remove(cacheKey.toKey()); return; } return wrappedEntry.body; } async set(entry: CacheEntry): Promise<void> { const cacheKey = new CacheKey({ clientId: entry.client_id, scope: entry.scope, audience: entry.audience }); const wrappedEntry = await this.wrapCacheEntry(entry); await this.cache.set(cacheKey.toKey(), wrappedEntry); await this.keyManifest?.add(cacheKey.toKey()); } async clear(clientId?: string): Promise<void> { const keys = await this.getCacheKeys(); /* c8 ignore next */ if (!keys) return; await keys .filter(key => (clientId ? key.includes(clientId) : true)) .reduce(async (memo, key) => { await memo; await this.cache.remove(key); }, Promise.resolve()); await this.keyManifest?.clear(); } private async wrapCacheEntry(entry: CacheEntry): Promise<WrappedCacheEntry> { const now = await this.nowProvider(); const expiresInTime = Math.floor(now / 1000) + entry.expires_in; return { body: entry, expiresAt: expiresInTime }; } private async getCacheKeys(): Promise<string[] | undefined> { if (this.keyManifest) { return (await this.keyManifest.get())?.keys; } else if (this.cache.allKeys) { return this.cache.allKeys(); } } /** * Returns the cache key to be used to store the id token * @param clientId The client id used to link to the id token * @returns The constructed cache key, as a string, to store the id token */ private getIdTokenCacheKey(clientId: string) { return new CacheKey( { clientId }, CACHE_KEY_PREFIX, CACHE_KEY_ID_TOKEN_SUFFIX ).toKey(); } /** * Finds the corresponding key in the cache based on the provided cache key. * The keys inside the cache are in the format {prefix}::{clientId}::{audience}::{scope}. * The first key in the cache that satisfies the following conditions is returned * - `prefix` is strict equal to Auth0's internally configured `keyPrefix` * - `clientId` is strict equal to the `cacheKey.clientId` * - `audience` is strict equal to the `cacheKey.audience` * - `scope` contains at least all the `cacheKey.scope` values * * * @param keyToMatch The provided cache key * @param allKeys A list of existing cache keys */ private matchExistingCacheKey(keyToMatch: CacheKey, allKeys: Array<string>) { return allKeys.filter(key => { const cacheKey = CacheKey.fromKey(key); const scopeSet = new Set(cacheKey.scope && cacheKey.scope.split(' ')); const scopesToMatch = keyToMatch.scope?.split(' ') || []; const hasAllScopes = cacheKey.scope && scopesToMatch.reduce( (acc, current) => acc && scopeSet.has(current), true ); return ( cacheKey.prefix === CACHE_KEY_PREFIX && cacheKey.clientId === keyToMatch.clientId && cacheKey.audience === keyToMatch.audience && hasAllScopes ); })[0]; } }