UNPKG

@azure/msal-browser

Version:
1,484 lines (1,384 loc) 51.9 kB
/* * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ import { AccessTokenEntity, AccountEntity, AccountInfo, ActiveAccountFilters, AppMetadataEntity, AuthenticationScheme, AuthorityMetadataEntity, CacheError, CacheErrorCodes, CacheHelpers, CacheManager, CacheRecord, CommonAuthorizationUrlRequest, Constants, createCacheError, DEFAULT_CRYPTO_IMPLEMENTATION, ICrypto, IdTokenEntity, invokeAsync, IPerformanceClient, Logger, PerformanceEvents, PersistentCacheKeys, RefreshTokenEntity, ServerTelemetryEntity, StaticAuthorityOptions, StoreInCache, StringUtils, ThrottlingEntity, TimeUtils, TokenKeys, } from "@azure/msal-common/browser"; import { CacheOptions } from "../config/Configuration.js"; import { BrowserAuthErrorCodes, createBrowserAuthError, } from "../error/BrowserAuthError.js"; import { BrowserCacheLocation, InMemoryCacheKeys, INTERACTION_TYPE, StaticCacheKeys, TemporaryCacheKeys, } from "../utils/BrowserConstants.js"; import { LocalStorage } from "./LocalStorage.js"; import { SessionStorage } from "./SessionStorage.js"; import { MemoryStorage } from "./MemoryStorage.js"; import { IWindowStorage } from "./IWindowStorage.js"; import { PlatformAuthRequest } from "../broker/nativeBroker/PlatformAuthRequest.js"; import { AuthenticationResult } from "../response/AuthenticationResult.js"; import { SilentRequest } from "../request/SilentRequest.js"; import { SsoSilentRequest } from "../request/SsoSilentRequest.js"; import { RedirectRequest } from "../request/RedirectRequest.js"; import { PopupRequest } from "../request/PopupRequest.js"; import { base64Decode } from "../encode/Base64Decode.js"; import { base64Encode } from "../encode/Base64Encode.js"; import { CookieStorage } from "./CookieStorage.js"; import { getAccountKeys, getTokenKeys } from "./CacheHelpers.js"; import { EventType } from "../event/EventType.js"; import { EventHandler } from "../event/EventHandler.js"; import { clearHash } from "../utils/BrowserUtils.js"; import { version } from "../packageMetadata.js"; /** * This class implements the cache storage interface for MSAL through browser local or session storage. * Cookies are only used if storeAuthStateInCookie is true, and are only used for * parameters such as state and nonce, generally. */ export class BrowserCacheManager extends CacheManager { // Cache configuration, either set by user or default values. protected cacheConfig: Required<CacheOptions>; // Window storage object (either local or sessionStorage) protected browserStorage: IWindowStorage<string>; // Internal in-memory storage object used for data used by msal that does not need to persist across page loads protected internalStorage: MemoryStorage<string>; // Temporary cache protected temporaryCacheStorage: IWindowStorage<string>; // Cookie storage protected cookieStorage: CookieStorage; // Logger instance protected logger: Logger; // Event Handler private eventHandler: EventHandler; constructor( clientId: string, cacheConfig: Required<CacheOptions>, cryptoImpl: ICrypto, logger: Logger, performanceClient: IPerformanceClient, eventHandler: EventHandler, staticAuthorityOptions?: StaticAuthorityOptions ) { super( clientId, cryptoImpl, logger, performanceClient, staticAuthorityOptions ); this.cacheConfig = cacheConfig; this.logger = logger; this.internalStorage = new MemoryStorage(); this.browserStorage = getStorageImplementation( clientId, cacheConfig.cacheLocation, logger, performanceClient ); this.temporaryCacheStorage = getStorageImplementation( clientId, cacheConfig.temporaryCacheLocation, logger, performanceClient ); this.cookieStorage = new CookieStorage(); this.eventHandler = eventHandler; } async initialize(correlationId: string): Promise<void> { await this.browserStorage.initialize(correlationId); this.trackVersionChanges(correlationId); } /** * Tracks upgrades and downgrades for telemetry and debugging purposes */ private trackVersionChanges(correlationId: string): void { const previousVersion = this.browserStorage.getItem( StaticCacheKeys.VERSION ); if (previousVersion) { this.logger.info( `MSAL.js was last initialized by version: ${previousVersion}` ); this.performanceClient.addFields( { previousLibraryVersion: previousVersion }, correlationId ); } if (previousVersion !== version) { this.setItem(StaticCacheKeys.VERSION, version, correlationId); } } /** * Parses passed value as JSON object, JSON.parse() will throw an error. * @param input */ protected validateAndParseJson(jsonValue: string): object | null { try { const parsedJson = JSON.parse(jsonValue); /** * There are edge cases in which JSON.parse will successfully parse a non-valid JSON object * (e.g. JSON.parse will parse an escaped string into an unescaped string), so adding a type check * of the parsed value is necessary in order to be certain that the string represents a valid JSON object. * */ return parsedJson && typeof parsedJson === "object" ? parsedJson : null; } catch (error) { return null; } } /** * Helper to setItem in browser storage, with cleanup in case of quota errors * @param key * @param value */ setItem(key: string, value: string, correlationId: string): void { let accessTokenKeys: Array<string> = []; const maxRetries = 20; for (let i = 0; i <= maxRetries; i++) { try { this.browserStorage.setItem(key, value); if (i > 0) { // Finally update the token keys array with the tokens removed this.removeAccessTokenKeys( accessTokenKeys.slice(0, i), correlationId ); } break; // If setItem succeeds, exit the loop } catch (e) { const cacheError = createCacheError(e); if ( cacheError.errorCode === CacheErrorCodes.cacheQuotaExceeded && i < maxRetries ) { if (!accessTokenKeys.length) { if ( key === `${StaticCacheKeys.TOKEN_KEYS}.${this.clientId}` ) { // If we are currently trying to set the token keys, use the value we're trying to set accessTokenKeys = (JSON.parse(value) as TokenKeys) .accessToken; } else { // If token keys have not been initialized, get them accessTokenKeys = this.getTokenKeys().accessToken; } } if (accessTokenKeys.length <= i) { // Nothing to remove, rethrow the error throw cacheError; } // When cache quota is exceeded, start removing access tokens until we can successfully set the item this.removeAccessToken( accessTokenKeys[i], correlationId, false // Don't save token keys yet, do it at the end ); } else { // If the error is not a quota exceeded error, rethrow it throw cacheError; } } } } /** * Helper to setUserData in browser storage, with cleanup in case of quota errors * @param key * @param value * @param correlationId */ async setUserData( key: string, value: string, correlationId: string, timestamp: string ): Promise<void> { let accessTokenKeys: Array<string> = []; const maxRetries = 20; for (let i = 0; i <= maxRetries; i++) { try { await invokeAsync( this.browserStorage.setUserData.bind(this.browserStorage), PerformanceEvents.SetUserData, this.logger, this.performanceClient )(key, value, correlationId, timestamp); if (i > 0) { // Finally update the token keys array with the tokens removed this.removeAccessTokenKeys( accessTokenKeys.slice(0, i), correlationId ); } break; // If setItem succeeds, exit the loop } catch (e) { const cacheError = createCacheError(e); if ( cacheError.errorCode === CacheErrorCodes.cacheQuotaExceeded && i < maxRetries ) { if (!accessTokenKeys.length) { accessTokenKeys = this.getTokenKeys().accessToken; } if (accessTokenKeys.length <= i) { // Nothing left to remove, rethrow the error throw cacheError; } // When cache quota is exceeded, start removing access tokens until we can successfully set the item this.removeAccessToken( accessTokenKeys[i], correlationId, false // Don't save token keys yet, do it at the end ); } else { // If the error is not a quota exceeded error, rethrow it throw cacheError; } } } } /** * Reads account from cache, deserializes it into an account entity and returns it. * If account is not found from the key, returns null and removes key from map. * @param accountKey * @returns */ getAccount( accountKey: string, correlationId: string ): AccountEntity | null { this.logger.trace("BrowserCacheManager.getAccount called"); const serializedAccount = this.browserStorage.getUserData(accountKey); if (!serializedAccount) { this.removeAccountKeyFromMap(accountKey, correlationId); return null; } const parsedAccount = this.validateAndParseJson(serializedAccount); if (!parsedAccount || !AccountEntity.isAccountEntity(parsedAccount)) { return null; } return CacheManager.toObject<AccountEntity>( new AccountEntity(), parsedAccount ); } /** * set account entity in the platform cache * @param account */ async setAccount( account: AccountEntity, correlationId: string ): Promise<void> { this.logger.trace("BrowserCacheManager.setAccount called"); const key = account.generateAccountKey(); const timestamp = Date.now().toString(); account.lastUpdatedAt = timestamp; await this.setUserData( key, JSON.stringify(account), correlationId, timestamp ); const wasAdded = this.addAccountKeyToMap(key, correlationId); /** * @deprecated - Remove this in next major version in favor of more consistent LOGIN event */ if ( this.cacheConfig.cacheLocation === BrowserCacheLocation.LocalStorage && wasAdded ) { this.eventHandler.emitEvent( EventType.ACCOUNT_ADDED, undefined, account.getAccountInfo() ); } } /** * Returns the array of account keys currently cached * @returns */ getAccountKeys(): Array<string> { return getAccountKeys(this.browserStorage); } /** * Add a new account to the key map * @param key */ addAccountKeyToMap(key: string, correlationId: string): boolean { this.logger.trace("BrowserCacheManager.addAccountKeyToMap called"); this.logger.tracePii( `BrowserCacheManager.addAccountKeyToMap called with key: ${key}` ); const accountKeys = this.getAccountKeys(); if (accountKeys.indexOf(key) === -1) { // Only add key if it does not already exist in the map accountKeys.push(key); this.setItem( StaticCacheKeys.ACCOUNT_KEYS, JSON.stringify(accountKeys), correlationId ); this.logger.verbose( "BrowserCacheManager.addAccountKeyToMap account key added" ); return true; } else { this.logger.verbose( "BrowserCacheManager.addAccountKeyToMap account key already exists in map" ); return false; } } /** * Remove an account from the key map * @param key */ removeAccountKeyFromMap(key: string, correlationId: string): void { this.logger.trace("BrowserCacheManager.removeAccountKeyFromMap called"); this.logger.tracePii( `BrowserCacheManager.removeAccountKeyFromMap called with key: ${key}` ); const accountKeys = this.getAccountKeys(); const removalIndex = accountKeys.indexOf(key); if (removalIndex > -1) { accountKeys.splice(removalIndex, 1); if (accountKeys.length === 0) { // If no keys left, remove the map this.removeItem(StaticCacheKeys.ACCOUNT_KEYS); return; } else { this.setItem( StaticCacheKeys.ACCOUNT_KEYS, JSON.stringify(accountKeys), correlationId ); } this.logger.trace( "BrowserCacheManager.removeAccountKeyFromMap account key removed" ); } else { this.logger.trace( "BrowserCacheManager.removeAccountKeyFromMap key not found in existing map" ); } } /** * Extends inherited removeAccount function to include removal of the account key from the map * @param key */ removeAccount(key: string, correlationId: string): void { super.removeAccount(key, correlationId); this.removeAccountKeyFromMap(key, correlationId); } /** * Removes credentials associated with the provided account * @param account */ removeAccountContext(account: AccountEntity, correlationId: string): void { super.removeAccountContext(account, correlationId); /** * @deprecated - Remove this in next major version in favor of more consistent LOGOUT event */ if ( this.cacheConfig.cacheLocation === BrowserCacheLocation.LocalStorage ) { this.eventHandler.emitEvent( EventType.ACCOUNT_REMOVED, undefined, account.getAccountInfo() ); } } /** * Removes given idToken from the cache and from the key map * @param key */ removeIdToken(key: string, correlationId: string): void { super.removeIdToken(key, correlationId); const tokenKeys = this.getTokenKeys(); const idRemoval = tokenKeys.idToken.indexOf(key); if (idRemoval > -1) { this.logger.info("idToken removed from tokenKeys map"); tokenKeys.idToken.splice(idRemoval, 1); this.setTokenKeys(tokenKeys, correlationId); } } /** * Removes given accessToken from the cache and from the key map * @param key */ removeAccessToken( key: string, correlationId: string, updateTokenKeys: boolean = true ): void { super.removeAccessToken(key, correlationId); updateTokenKeys && this.removeAccessTokenKeys([key], correlationId); } /** * Remove access token key from the key map * @param key * @param correlationId * @param tokenKeys */ removeAccessTokenKeys(keys: Array<string>, correlationId: string): void { this.logger.trace("removeAccessTokenKey called"); const tokenKeys = this.getTokenKeys(); let keysRemoved = 0; keys.forEach((key) => { const accessRemoval = tokenKeys.accessToken.indexOf(key); if (accessRemoval > -1) { tokenKeys.accessToken.splice(accessRemoval, 1); keysRemoved++; } }); if (keysRemoved > 0) { this.logger.info( `removed ${keysRemoved} accessToken keys from tokenKeys map` ); this.setTokenKeys(tokenKeys, correlationId); return; } } /** * Removes given refreshToken from the cache and from the key map * @param key */ removeRefreshToken(key: string, correlationId: string): void { super.removeRefreshToken(key, correlationId); const tokenKeys = this.getTokenKeys(); const refreshRemoval = tokenKeys.refreshToken.indexOf(key); if (refreshRemoval > -1) { this.logger.info("refreshToken removed from tokenKeys map"); tokenKeys.refreshToken.splice(refreshRemoval, 1); this.setTokenKeys(tokenKeys, correlationId); } } /** * Gets the keys for the cached tokens associated with this clientId * @returns */ getTokenKeys(): TokenKeys { return getTokenKeys(this.clientId, this.browserStorage); } /** * Stores the token keys in the cache * @param tokenKeys * @param correlationId * @returns */ setTokenKeys(tokenKeys: TokenKeys, correlationId: string): void { if ( tokenKeys.idToken.length === 0 && tokenKeys.accessToken.length === 0 && tokenKeys.refreshToken.length === 0 ) { // If no keys left, remove the map this.removeItem(`${StaticCacheKeys.TOKEN_KEYS}.${this.clientId}`); return; } else { this.setItem( `${StaticCacheKeys.TOKEN_KEYS}.${this.clientId}`, JSON.stringify(tokenKeys), correlationId ); } } /** * generates idToken entity from a string * @param idTokenKey */ getIdTokenCredential( idTokenKey: string, correlationId: string ): IdTokenEntity | null { const value = this.browserStorage.getUserData(idTokenKey); if (!value) { this.logger.trace( "BrowserCacheManager.getIdTokenCredential: called, no cache hit" ); this.removeIdToken(idTokenKey, correlationId); return null; } const parsedIdToken = this.validateAndParseJson(value); if (!parsedIdToken || !CacheHelpers.isIdTokenEntity(parsedIdToken)) { this.logger.trace( "BrowserCacheManager.getIdTokenCredential: called, no cache hit" ); return null; } this.logger.trace( "BrowserCacheManager.getIdTokenCredential: cache hit" ); return parsedIdToken as IdTokenEntity; } /** * set IdToken credential to the platform cache * @param idToken */ async setIdTokenCredential( idToken: IdTokenEntity, correlationId: string ): Promise<void> { this.logger.trace("BrowserCacheManager.setIdTokenCredential called"); const idTokenKey = CacheHelpers.generateCredentialKey(idToken); const timestamp = Date.now().toString(); idToken.lastUpdatedAt = timestamp; await this.setUserData( idTokenKey, JSON.stringify(idToken), correlationId, timestamp ); const tokenKeys = this.getTokenKeys(); if (tokenKeys.idToken.indexOf(idTokenKey) === -1) { this.logger.info( "BrowserCacheManager: addTokenKey - idToken added to map" ); tokenKeys.idToken.push(idTokenKey); this.setTokenKeys(tokenKeys, correlationId); } } /** * generates accessToken entity from a string * @param key */ getAccessTokenCredential( accessTokenKey: string, correlationId: string ): AccessTokenEntity | null { const value = this.browserStorage.getUserData(accessTokenKey); if (!value) { this.logger.trace( "BrowserCacheManager.getAccessTokenCredential: called, no cache hit" ); this.removeAccessTokenKeys([accessTokenKey], correlationId); return null; } const parsedAccessToken = this.validateAndParseJson(value); if ( !parsedAccessToken || !CacheHelpers.isAccessTokenEntity(parsedAccessToken) ) { this.logger.trace( "BrowserCacheManager.getAccessTokenCredential: called, no cache hit" ); return null; } this.logger.trace( "BrowserCacheManager.getAccessTokenCredential: cache hit" ); return parsedAccessToken as AccessTokenEntity; } /** * set accessToken credential to the platform cache * @param accessToken */ async setAccessTokenCredential( accessToken: AccessTokenEntity, correlationId: string ): Promise<void> { this.logger.trace( "BrowserCacheManager.setAccessTokenCredential called" ); const accessTokenKey = CacheHelpers.generateCredentialKey(accessToken); const timestamp = Date.now().toString(); accessToken.lastUpdatedAt = timestamp; await this.setUserData( accessTokenKey, JSON.stringify(accessToken), correlationId, timestamp ); const tokenKeys = this.getTokenKeys(); const index = tokenKeys.accessToken.indexOf(accessTokenKey); if (index !== -1) { tokenKeys.accessToken.splice(index, 1); // Remove existing key before pushing to the end } this.logger.trace( `access token ${index === -1 ? "added to" : "updated in"} map` ); tokenKeys.accessToken.push(accessTokenKey); this.setTokenKeys(tokenKeys, correlationId); } /** * generates refreshToken entity from a string * @param refreshTokenKey */ getRefreshTokenCredential( refreshTokenKey: string, correlationId: string ): RefreshTokenEntity | null { const value = this.browserStorage.getUserData(refreshTokenKey); if (!value) { this.logger.trace( "BrowserCacheManager.getRefreshTokenCredential: called, no cache hit" ); this.removeRefreshToken(refreshTokenKey, correlationId); return null; } const parsedRefreshToken = this.validateAndParseJson(value); if ( !parsedRefreshToken || !CacheHelpers.isRefreshTokenEntity(parsedRefreshToken) ) { this.logger.trace( "BrowserCacheManager.getRefreshTokenCredential: called, no cache hit" ); return null; } this.logger.trace( "BrowserCacheManager.getRefreshTokenCredential: cache hit" ); return parsedRefreshToken as RefreshTokenEntity; } /** * set refreshToken credential to the platform cache * @param refreshToken */ async setRefreshTokenCredential( refreshToken: RefreshTokenEntity, correlationId: string ): Promise<void> { this.logger.trace( "BrowserCacheManager.setRefreshTokenCredential called" ); const refreshTokenKey = CacheHelpers.generateCredentialKey(refreshToken); const timestamp = Date.now().toString(); refreshToken.lastUpdatedAt = timestamp; await this.setUserData( refreshTokenKey, JSON.stringify(refreshToken), correlationId, timestamp ); const tokenKeys = this.getTokenKeys(); if (tokenKeys.refreshToken.indexOf(refreshTokenKey) === -1) { this.logger.info( "BrowserCacheManager: addTokenKey - refreshToken added to map" ); tokenKeys.refreshToken.push(refreshTokenKey); this.setTokenKeys(tokenKeys, correlationId); } } /** * fetch appMetadata entity from the platform cache * @param appMetadataKey */ getAppMetadata(appMetadataKey: string): AppMetadataEntity | null { const value = this.browserStorage.getItem(appMetadataKey); if (!value) { this.logger.trace( "BrowserCacheManager.getAppMetadata: called, no cache hit" ); return null; } const parsedMetadata = this.validateAndParseJson(value); if ( !parsedMetadata || !CacheHelpers.isAppMetadataEntity(appMetadataKey, parsedMetadata) ) { this.logger.trace( "BrowserCacheManager.getAppMetadata: called, no cache hit" ); return null; } this.logger.trace("BrowserCacheManager.getAppMetadata: cache hit"); return parsedMetadata as AppMetadataEntity; } /** * set appMetadata entity to the platform cache * @param appMetadata */ setAppMetadata( appMetadata: AppMetadataEntity, correlationId: string ): void { this.logger.trace("BrowserCacheManager.setAppMetadata called"); const appMetadataKey = CacheHelpers.generateAppMetadataKey(appMetadata); this.setItem( appMetadataKey, JSON.stringify(appMetadata), correlationId ); } /** * fetch server telemetry entity from the platform cache * @param serverTelemetryKey */ getServerTelemetry( serverTelemetryKey: string ): ServerTelemetryEntity | null { const value = this.browserStorage.getItem(serverTelemetryKey); if (!value) { this.logger.trace( "BrowserCacheManager.getServerTelemetry: called, no cache hit" ); return null; } const parsedEntity = this.validateAndParseJson(value); if ( !parsedEntity || !CacheHelpers.isServerTelemetryEntity( serverTelemetryKey, parsedEntity ) ) { this.logger.trace( "BrowserCacheManager.getServerTelemetry: called, no cache hit" ); return null; } this.logger.trace("BrowserCacheManager.getServerTelemetry: cache hit"); return parsedEntity as ServerTelemetryEntity; } /** * set server telemetry entity to the platform cache * @param serverTelemetryKey * @param serverTelemetry */ setServerTelemetry( serverTelemetryKey: string, serverTelemetry: ServerTelemetryEntity, correlationId: string ): void { this.logger.trace("BrowserCacheManager.setServerTelemetry called"); this.setItem( serverTelemetryKey, JSON.stringify(serverTelemetry), correlationId ); } /** * */ getAuthorityMetadata(key: string): AuthorityMetadataEntity | null { const value = this.internalStorage.getItem(key); if (!value) { this.logger.trace( "BrowserCacheManager.getAuthorityMetadata: called, no cache hit" ); return null; } const parsedMetadata = this.validateAndParseJson(value); if ( parsedMetadata && CacheHelpers.isAuthorityMetadataEntity(key, parsedMetadata) ) { this.logger.trace( "BrowserCacheManager.getAuthorityMetadata: cache hit" ); return parsedMetadata as AuthorityMetadataEntity; } return null; } /** * */ getAuthorityMetadataKeys(): Array<string> { const allKeys = this.internalStorage.getKeys(); return allKeys.filter((key) => { return this.isAuthorityMetadata(key); }); } /** * Sets wrapper metadata in memory * @param wrapperSKU * @param wrapperVersion */ setWrapperMetadata(wrapperSKU: string, wrapperVersion: string): void { this.internalStorage.setItem(InMemoryCacheKeys.WRAPPER_SKU, wrapperSKU); this.internalStorage.setItem( InMemoryCacheKeys.WRAPPER_VER, wrapperVersion ); } /** * Returns wrapper metadata from in-memory storage */ getWrapperMetadata(): [string, string] { const sku = this.internalStorage.getItem(InMemoryCacheKeys.WRAPPER_SKU) || Constants.EMPTY_STRING; const version = this.internalStorage.getItem(InMemoryCacheKeys.WRAPPER_VER) || Constants.EMPTY_STRING; return [sku, version]; } /** * * @param entity */ setAuthorityMetadata(key: string, entity: AuthorityMetadataEntity): void { this.logger.trace("BrowserCacheManager.setAuthorityMetadata called"); this.internalStorage.setItem(key, JSON.stringify(entity)); } /** * Gets the active account */ getActiveAccount(correlationId: string): AccountInfo | null { const activeAccountKeyFilters = this.generateCacheKey( PersistentCacheKeys.ACTIVE_ACCOUNT_FILTERS ); const activeAccountValueFilters = this.browserStorage.getItem( activeAccountKeyFilters ); if (!activeAccountValueFilters) { this.logger.trace( "BrowserCacheManager.getActiveAccount: No active account filters found" ); return null; } const activeAccountValueObj = this.validateAndParseJson( activeAccountValueFilters ) as AccountInfo; if (activeAccountValueObj) { this.logger.trace( "BrowserCacheManager.getActiveAccount: Active account filters schema found" ); return this.getAccountInfoFilteredBy( { homeAccountId: activeAccountValueObj.homeAccountId, localAccountId: activeAccountValueObj.localAccountId, tenantId: activeAccountValueObj.tenantId, }, correlationId ); } this.logger.trace( "BrowserCacheManager.getActiveAccount: No active account found" ); return null; } /** * Sets the active account's localAccountId in cache * @param account */ setActiveAccount(account: AccountInfo | null, correlationId: string): void { const activeAccountKey = this.generateCacheKey( PersistentCacheKeys.ACTIVE_ACCOUNT_FILTERS ); if (account) { this.logger.verbose("setActiveAccount: Active account set"); const activeAccountValue: ActiveAccountFilters = { homeAccountId: account.homeAccountId, localAccountId: account.localAccountId, tenantId: account.tenantId, lastUpdatedAt: TimeUtils.nowSeconds().toString(), }; this.setItem( activeAccountKey, JSON.stringify(activeAccountValue), correlationId ); } else { this.logger.verbose( "setActiveAccount: No account passed, active account not set" ); this.browserStorage.removeItem(activeAccountKey); } this.eventHandler.emitEvent(EventType.ACTIVE_ACCOUNT_CHANGED); } /** * fetch throttling entity from the platform cache * @param throttlingCacheKey */ getThrottlingCache(throttlingCacheKey: string): ThrottlingEntity | null { const value = this.browserStorage.getItem(throttlingCacheKey); if (!value) { this.logger.trace( "BrowserCacheManager.getThrottlingCache: called, no cache hit" ); return null; } const parsedThrottlingCache = this.validateAndParseJson(value); if ( !parsedThrottlingCache || !CacheHelpers.isThrottlingEntity( throttlingCacheKey, parsedThrottlingCache ) ) { this.logger.trace( "BrowserCacheManager.getThrottlingCache: called, no cache hit" ); return null; } this.logger.trace("BrowserCacheManager.getThrottlingCache: cache hit"); return parsedThrottlingCache as ThrottlingEntity; } /** * set throttling entity to the platform cache * @param throttlingCacheKey * @param throttlingCache */ setThrottlingCache( throttlingCacheKey: string, throttlingCache: ThrottlingEntity, correlationId: string ): void { this.logger.trace("BrowserCacheManager.setThrottlingCache called"); this.setItem( throttlingCacheKey, JSON.stringify(throttlingCache), correlationId ); } /** * Gets cache item with given key. * Will retrieve from cookies if storeAuthStateInCookie is set to true. * @param key */ getTemporaryCache(cacheKey: string, generateKey?: boolean): string | null { const key = generateKey ? this.generateCacheKey(cacheKey) : cacheKey; if (this.cacheConfig.storeAuthStateInCookie) { const itemCookie = this.cookieStorage.getItem(key); if (itemCookie) { this.logger.trace( "BrowserCacheManager.getTemporaryCache: storeAuthStateInCookies set to true, retrieving from cookies" ); return itemCookie; } } const value = this.temporaryCacheStorage.getItem(key); if (!value) { // If temp cache item not found in session/memory, check local storage for items set by old versions if ( this.cacheConfig.cacheLocation === BrowserCacheLocation.LocalStorage ) { const item = this.browserStorage.getItem(key); if (item) { this.logger.trace( "BrowserCacheManager.getTemporaryCache: Temporary cache item found in local storage" ); return item; } } this.logger.trace( "BrowserCacheManager.getTemporaryCache: No cache item found in local storage" ); return null; } this.logger.trace( "BrowserCacheManager.getTemporaryCache: Temporary cache item returned" ); return value; } /** * Sets the cache item with the key and value given. * Stores in cookie if storeAuthStateInCookie is set to true. * This can cause cookie overflow if used incorrectly. * @param key * @param value */ setTemporaryCache( cacheKey: string, value: string, generateKey?: boolean ): void { const key = generateKey ? this.generateCacheKey(cacheKey) : cacheKey; this.temporaryCacheStorage.setItem(key, value); if (this.cacheConfig.storeAuthStateInCookie) { this.logger.trace( "BrowserCacheManager.setTemporaryCache: storeAuthStateInCookie set to true, setting item cookie" ); this.cookieStorage.setItem( key, value, undefined, this.cacheConfig.secureCookies ); } } /** * Removes the cache item with the given key. * @param key */ removeItem(key: string): void { this.browserStorage.removeItem(key); } /** * Removes the temporary cache item with the given key. * Will also clear the cookie item if storeAuthStateInCookie is set to true. * @param key */ removeTemporaryItem(key: string): void { this.temporaryCacheStorage.removeItem(key); if (this.cacheConfig.storeAuthStateInCookie) { this.logger.trace( "BrowserCacheManager.removeItem: storeAuthStateInCookie is true, clearing item cookie" ); this.cookieStorage.removeItem(key); } } /** * Gets all keys in window. */ getKeys(): string[] { return this.browserStorage.getKeys(); } /** * Clears all cache entries created by MSAL. */ clear(correlationId: string): void { // Removes all accounts and their credentials this.removeAllAccounts(correlationId); this.removeAppMetadata(correlationId); // Remove temp storage first to make sure any cookies are cleared this.temporaryCacheStorage.getKeys().forEach((cacheKey: string) => { if ( cacheKey.indexOf(Constants.CACHE_PREFIX) !== -1 || cacheKey.indexOf(this.clientId) !== -1 ) { this.removeTemporaryItem(cacheKey); } }); // Removes all remaining MSAL cache items this.browserStorage.getKeys().forEach((cacheKey: string) => { if ( cacheKey.indexOf(Constants.CACHE_PREFIX) !== -1 || cacheKey.indexOf(this.clientId) !== -1 ) { this.browserStorage.removeItem(cacheKey); } }); this.internalStorage.clear(); } /** * Clears all access tokes that have claims prior to saving the current one * @param performanceClient {IPerformanceClient} * @param correlationId {string} correlation id * @returns */ clearTokensAndKeysWithClaims(correlationId: string): void { this.performanceClient.addQueueMeasurement( PerformanceEvents.ClearTokensAndKeysWithClaims, correlationId ); const tokenKeys = this.getTokenKeys(); let removedAccessTokens = 0; tokenKeys.accessToken.forEach((key: string) => { // if the access token has claims in its key, remove the token key and the token const credential = this.getAccessTokenCredential( key, correlationId ); if ( credential?.requestedClaimsHash && key.includes(credential.requestedClaimsHash.toLowerCase()) ) { this.removeAccessToken(key, correlationId); removedAccessTokens++; } }); // warn if any access tokens are removed if (removedAccessTokens > 0) { this.logger.warning( `${removedAccessTokens} access tokens with claims in the cache keys have been removed from the cache.` ); } } /** * Prepend msal.<client-id> to each key; Skip for any JSON object as Key (defined schemas do not need the key appended: AccessToken Keys or the upcoming schema) * @param key * @param addInstanceId */ generateCacheKey(key: string): string { const generatedKey = this.validateAndParseJson(key); if (!generatedKey) { if (StringUtils.startsWith(key, Constants.CACHE_PREFIX)) { return key; } return `${Constants.CACHE_PREFIX}.${this.clientId}.${key}`; } return JSON.stringify(key); } /** * Reset all temporary cache items * @param state */ resetRequestCache(): void { this.logger.trace("BrowserCacheManager.resetRequestCache called"); this.removeTemporaryItem( this.generateCacheKey(TemporaryCacheKeys.REQUEST_PARAMS) ); this.removeTemporaryItem( this.generateCacheKey(TemporaryCacheKeys.VERIFIER) ); this.removeTemporaryItem( this.generateCacheKey(TemporaryCacheKeys.ORIGIN_URI) ); this.removeTemporaryItem( this.generateCacheKey(TemporaryCacheKeys.URL_HASH) ); this.removeTemporaryItem( this.generateCacheKey(TemporaryCacheKeys.NATIVE_REQUEST) ); this.setInteractionInProgress(false); } cacheAuthorizeRequest( authCodeRequest: CommonAuthorizationUrlRequest, codeVerifier?: string ): void { this.logger.trace("BrowserCacheManager.cacheAuthorizeRequest called"); const encodedValue = base64Encode(JSON.stringify(authCodeRequest)); this.setTemporaryCache( TemporaryCacheKeys.REQUEST_PARAMS, encodedValue, true ); if (codeVerifier) { const encodedVerifier = base64Encode(codeVerifier); this.setTemporaryCache( TemporaryCacheKeys.VERIFIER, encodedVerifier, true ); } } /** * Gets the token exchange parameters from the cache. Throws an error if nothing is found. */ getCachedRequest(): [CommonAuthorizationUrlRequest, string] { this.logger.trace("BrowserCacheManager.getCachedRequest called"); // Get token request from cache and parse as TokenExchangeParameters. const encodedTokenRequest = this.getTemporaryCache( TemporaryCacheKeys.REQUEST_PARAMS, true ); if (!encodedTokenRequest) { throw createBrowserAuthError( BrowserAuthErrorCodes.noTokenRequestCacheError ); } const encodedVerifier = this.getTemporaryCache( TemporaryCacheKeys.VERIFIER, true ); let parsedRequest: CommonAuthorizationUrlRequest; let verifier = ""; try { parsedRequest = JSON.parse(base64Decode(encodedTokenRequest)); if (encodedVerifier) { verifier = base64Decode(encodedVerifier); } } catch (e) { this.logger.errorPii(`Attempted to parse: ${encodedTokenRequest}`); this.logger.error( `Parsing cached token request threw with error: ${e}` ); throw createBrowserAuthError( BrowserAuthErrorCodes.unableToParseTokenRequestCacheError ); } return [parsedRequest, verifier]; } /** * Gets cached native request for redirect flows */ getCachedNativeRequest(): PlatformAuthRequest | null { this.logger.trace("BrowserCacheManager.getCachedNativeRequest called"); const cachedRequest = this.getTemporaryCache( TemporaryCacheKeys.NATIVE_REQUEST, true ); if (!cachedRequest) { this.logger.trace( "BrowserCacheManager.getCachedNativeRequest: No cached native request found" ); return null; } const parsedRequest = this.validateAndParseJson( cachedRequest ) as PlatformAuthRequest; if (!parsedRequest) { this.logger.error( "BrowserCacheManager.getCachedNativeRequest: Unable to parse native request" ); return null; } return parsedRequest; } isInteractionInProgress(matchClientId?: boolean): boolean { const clientId = this.getInteractionInProgress()?.clientId; if (matchClientId) { return clientId === this.clientId; } else { return !!clientId; } } getInteractionInProgress(): { clientId: string; type: INTERACTION_TYPE; } | null { const key = `${Constants.CACHE_PREFIX}.${TemporaryCacheKeys.INTERACTION_STATUS_KEY}`; const value = this.getTemporaryCache(key, false); try { return value ? JSON.parse(value) : null; } catch (e) { // Remove interaction and other temp keys if interaction status can't be parsed this.logger.error( `Cannot parse interaction status. Removing temporary cache items and clearing url hash. Retrying interaction should fix the error` ); this.removeTemporaryItem(key); this.resetRequestCache(); clearHash(window); return null; } } setInteractionInProgress( inProgress: boolean, type: INTERACTION_TYPE = INTERACTION_TYPE.SIGNIN ): void { // Ensure we don't overwrite interaction in progress for a different clientId const key = `${Constants.CACHE_PREFIX}.${TemporaryCacheKeys.INTERACTION_STATUS_KEY}`; if (inProgress) { if (this.getInteractionInProgress()) { throw createBrowserAuthError( BrowserAuthErrorCodes.interactionInProgress ); } else { // No interaction is in progress this.setTemporaryCache( key, JSON.stringify({ clientId: this.clientId, type }), false ); } } else if ( !inProgress && this.getInteractionInProgress()?.clientId === this.clientId ) { this.removeTemporaryItem(key); } } /** * Builds credential entities from AuthenticationResult object and saves the resulting credentials to the cache * @param result * @param request */ async hydrateCache( result: AuthenticationResult, request: | SilentRequest | SsoSilentRequest | RedirectRequest | PopupRequest ): Promise<void> { const idTokenEntity = CacheHelpers.createIdTokenEntity( result.account?.homeAccountId, result.account?.environment, result.idToken, this.clientId, result.tenantId ); let claimsHash; if (request.claims) { claimsHash = await this.cryptoImpl.hashString(request.claims); } /** * meta data for cache stores time in seconds from epoch * AuthenticationResult returns expiresOn and extExpiresOn in milliseconds (as a Date object which is in ms) * We need to map these for the cache when building tokens from AuthenticationResult * * The next MSAL VFuture should map these both to same value if possible */ const accessTokenEntity = CacheHelpers.createAccessTokenEntity( result.account?.homeAccountId, result.account.environment, result.accessToken, this.clientId, result.tenantId, result.scopes.join(" "), // Access token expiresOn stored in seconds, converting from AuthenticationResult expiresOn stored as Date result.expiresOn ? TimeUtils.toSecondsFromDate(result.expiresOn) : 0, result.extExpiresOn ? TimeUtils.toSecondsFromDate(result.extExpiresOn) : 0, base64Decode, undefined, // refreshOn result.tokenType as AuthenticationScheme, undefined, // userAssertionHash request.sshKid, request.claims, claimsHash ); const cacheRecord = { idToken: idTokenEntity, accessToken: accessTokenEntity, }; return this.saveCacheRecord(cacheRecord, result.correlationId); } /** * saves a cache record * @param cacheRecord {CacheRecord} * @param storeInCache {?StoreInCache} * @param correlationId {?string} correlation id */ async saveCacheRecord( cacheRecord: CacheRecord, correlationId: string, storeInCache?: StoreInCache ): Promise<void> { try { await super.saveCacheRecord( cacheRecord, correlationId, storeInCache ); } catch (e) { if ( e instanceof CacheError && this.performanceClient && correlationId ) { try { const tokenKeys = this.getTokenKeys(); this.performanceClient.addFields( {