UNPKG

@azure/msal-browser

Version:
1,347 lines (1,255 loc) 70.6 kB
/* * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ import { Constants, PersistentCacheKeys, StringUtils, CommonAuthorizationCodeRequest, ICrypto, AccountEntity, IdTokenEntity, AccessTokenEntity, RefreshTokenEntity, AppMetadataEntity, CacheManager, ServerTelemetryEntity, ThrottlingEntity, ProtocolUtils, Logger, AuthorityMetadataEntity, DEFAULT_CRYPTO_IMPLEMENTATION, AccountInfo, ActiveAccountFilters, CcsCredential, CcsCredentialType, AuthToken, ValidCredentialType, TokenKeys, CredentialType, CacheRecord, AuthenticationScheme, createClientAuthError, ClientAuthErrorCodes, PerformanceEvents, IPerformanceClient, StaticAuthorityOptions, CacheHelpers, StoreInCache, CacheError, } from "@azure/msal-common"; import { CacheOptions } from "../config/Configuration"; import { createBrowserAuthError, BrowserAuthErrorCodes, } from "../error/BrowserAuthError"; import { BrowserCacheLocation, InteractionType, TemporaryCacheKeys, InMemoryCacheKeys, StaticCacheKeys, } from "../utils/BrowserConstants"; import { BrowserStorage } from "./BrowserStorage"; import { MemoryStorage } from "./MemoryStorage"; import { IWindowStorage } from "./IWindowStorage"; import { extractBrowserRequestState } from "../utils/BrowserProtocolUtils"; import { NativeTokenRequest } from "../broker/nativeBroker/NativeRequest"; import { AuthenticationResult } from "../response/AuthenticationResult"; import { SilentRequest } from "../request/SilentRequest"; import { SsoSilentRequest } from "../request/SsoSilentRequest"; import { RedirectRequest } from "../request/RedirectRequest"; import { PopupRequest } from "../request/PopupRequest"; import { base64Decode } from "../encode/Base64Decode"; import { base64Encode } from "../encode/Base64Encode"; /** * 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>; // Logger instance protected logger: Logger; // Telemetry perf client protected performanceClient?: IPerformanceClient; // Cookie life calculation (hours * minutes * seconds * ms) protected readonly COOKIE_LIFE_MULTIPLIER = 24 * 60 * 60 * 1000; constructor( clientId: string, cacheConfig: Required<CacheOptions>, cryptoImpl: ICrypto, logger: Logger, staticAuthorityOptions?: StaticAuthorityOptions, performanceClient?: IPerformanceClient ) { super(clientId, cryptoImpl, logger, staticAuthorityOptions); this.cacheConfig = cacheConfig; this.logger = logger; this.internalStorage = new MemoryStorage(); this.browserStorage = this.setupBrowserStorage( this.cacheConfig.cacheLocation ); this.temporaryCacheStorage = this.setupTemporaryCacheStorage( this.cacheConfig.temporaryCacheLocation, this.cacheConfig.cacheLocation ); // Migrate cache entries from older versions of MSAL. if (cacheConfig.cacheMigrationEnabled) { this.migrateCacheEntries(); this.createKeyMaps(); } this.performanceClient = performanceClient; } /** * Returns a window storage class implementing the IWindowStorage interface that corresponds to the configured cacheLocation. * @param cacheLocation */ protected setupBrowserStorage( cacheLocation: BrowserCacheLocation | string ): IWindowStorage<string> { switch (cacheLocation) { case BrowserCacheLocation.LocalStorage: case BrowserCacheLocation.SessionStorage: try { return new BrowserStorage(cacheLocation); } catch (e) { this.logger.verbose(e as string); break; } case BrowserCacheLocation.MemoryStorage: default: break; } this.cacheConfig.cacheLocation = BrowserCacheLocation.MemoryStorage; return new MemoryStorage(); } /** * Returns a window storage class implementing the IWindowStorage interface that corresponds to the configured temporaryCacheLocation. * @param temporaryCacheLocation * @param cacheLocation */ protected setupTemporaryCacheStorage( temporaryCacheLocation: BrowserCacheLocation | string, cacheLocation: BrowserCacheLocation | string ): IWindowStorage<string> { switch (cacheLocation) { case BrowserCacheLocation.LocalStorage: case BrowserCacheLocation.SessionStorage: try { // Temporary cache items will always be stored in session storage to mitigate problems caused by multiple tabs return new BrowserStorage( temporaryCacheLocation || BrowserCacheLocation.SessionStorage ); } catch (e) { this.logger.verbose(e as string); return this.internalStorage; } case BrowserCacheLocation.MemoryStorage: default: return this.internalStorage; } } /** * Migrate all old cache entries to new schema. No rollback supported. * @param storeAuthStateInCookie */ protected migrateCacheEntries(): void { const idTokenKey = `${Constants.CACHE_PREFIX}.${PersistentCacheKeys.ID_TOKEN}`; const clientInfoKey = `${Constants.CACHE_PREFIX}.${PersistentCacheKeys.CLIENT_INFO}`; const errorKey = `${Constants.CACHE_PREFIX}.${PersistentCacheKeys.ERROR}`; const errorDescKey = `${Constants.CACHE_PREFIX}.${PersistentCacheKeys.ERROR_DESC}`; const idTokenValue = this.browserStorage.getItem(idTokenKey); const clientInfoValue = this.browserStorage.getItem(clientInfoKey); const errorValue = this.browserStorage.getItem(errorKey); const errorDescValue = this.browserStorage.getItem(errorDescKey); const values = [ idTokenValue, clientInfoValue, errorValue, errorDescValue, ]; const keysToMigrate = [ PersistentCacheKeys.ID_TOKEN, PersistentCacheKeys.CLIENT_INFO, PersistentCacheKeys.ERROR, PersistentCacheKeys.ERROR_DESC, ]; keysToMigrate.forEach((cacheKey: string, index: number) => { const value = values[index]; if (value) { this.setTemporaryCache(cacheKey, value, true); } }); } /** * Searches all cache entries for MSAL accounts and creates the account key map * This is used to migrate users from older versions of MSAL which did not create the map. * @returns */ private createKeyMaps(): void { this.logger.trace("BrowserCacheManager - createKeyMaps called."); const accountKeys = this.getItem(StaticCacheKeys.ACCOUNT_KEYS); const tokenKeys = this.getItem( `${StaticCacheKeys.TOKEN_KEYS}.${this.clientId}` ); if (accountKeys && tokenKeys) { this.logger.verbose( "BrowserCacheManager:createKeyMaps - account and token key maps already exist, skipping migration." ); // Key maps already exist, no need to iterate through cache return; } const allKeys = this.browserStorage.getKeys(); allKeys.forEach((key) => { if (this.isCredentialKey(key)) { // Get item, parse, validate and write key to map const value = this.getItem(key); if (value) { const credObj = this.validateAndParseJson(value); if (credObj && credObj.hasOwnProperty("credentialType")) { switch (credObj["credentialType"]) { case CredentialType.ID_TOKEN: if (CacheHelpers.isIdTokenEntity(credObj)) { this.logger.trace( "BrowserCacheManager:createKeyMaps - idToken found, saving key to token key map" ); this.logger.tracePii( `BrowserCacheManager:createKeyMaps - idToken with key: ${key} found, saving key to token key map` ); const idTokenEntity = credObj as IdTokenEntity; const newKey = this.updateCredentialCacheKey( key, idTokenEntity ); this.addTokenKey( newKey, CredentialType.ID_TOKEN ); return; } else { this.logger.trace( "BrowserCacheManager:createKeyMaps - key found matching idToken schema with value containing idToken credentialType field but value failed IdTokenEntity validation, skipping." ); this.logger.tracePii( `BrowserCacheManager:createKeyMaps - failed idToken validation on key: ${key}` ); } break; case CredentialType.ACCESS_TOKEN: case CredentialType.ACCESS_TOKEN_WITH_AUTH_SCHEME: if (CacheHelpers.isAccessTokenEntity(credObj)) { this.logger.trace( "BrowserCacheManager:createKeyMaps - accessToken found, saving key to token key map" ); this.logger.tracePii( `BrowserCacheManager:createKeyMaps - accessToken with key: ${key} found, saving key to token key map` ); const accessTokenEntity = credObj as AccessTokenEntity; const newKey = this.updateCredentialCacheKey( key, accessTokenEntity ); this.addTokenKey( newKey, CredentialType.ACCESS_TOKEN ); return; } else { this.logger.trace( "BrowserCacheManager:createKeyMaps - key found matching accessToken schema with value containing accessToken credentialType field but value failed AccessTokenEntity validation, skipping." ); this.logger.tracePii( `BrowserCacheManager:createKeyMaps - failed accessToken validation on key: ${key}` ); } break; case CredentialType.REFRESH_TOKEN: if ( CacheHelpers.isRefreshTokenEntity(credObj) ) { this.logger.trace( "BrowserCacheManager:createKeyMaps - refreshToken found, saving key to token key map" ); this.logger.tracePii( `BrowserCacheManager:createKeyMaps - refreshToken with key: ${key} found, saving key to token key map` ); const refreshTokenEntity = credObj as RefreshTokenEntity; const newKey = this.updateCredentialCacheKey( key, refreshTokenEntity ); this.addTokenKey( newKey, CredentialType.REFRESH_TOKEN ); return; } else { this.logger.trace( "BrowserCacheManager:createKeyMaps - key found matching refreshToken schema with value containing refreshToken credentialType field but value failed RefreshTokenEntity validation, skipping." ); this.logger.tracePii( `BrowserCacheManager:createKeyMaps - failed refreshToken validation on key: ${key}` ); } break; default: // If credentialType isn't one of our predefined ones, it may not be an MSAL cache value. Ignore. } } } } if (this.isAccountKey(key)) { const value = this.getItem(key); if (value) { const accountObj = this.validateAndParseJson(value); if ( accountObj && AccountEntity.isAccountEntity(accountObj) ) { this.logger.trace( "BrowserCacheManager:createKeyMaps - account found, saving key to account key map" ); this.logger.tracePii( `BrowserCacheManager:createKeyMaps - account with key: ${key} found, saving key to account key map` ); this.addAccountKeyToMap(key); } } } }); } /** * 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; } } /** * fetches the entry from the browser storage based off the key * @param key */ getItem(key: string): string | null { return this.browserStorage.getItem(key); } /** * sets the entry in the browser storage * @param key * @param value */ setItem(key: string, value: string): void { this.browserStorage.setItem(key, value); } /** * fetch the account entity from the platform cache * @param accountKey */ getAccount(accountKey: string, logger?: Logger): AccountEntity | null { this.logger.trace("BrowserCacheManager.getAccount called"); const accountEntity = this.getCachedAccountEntity(accountKey); return this.updateOutdatedCachedAccount( accountKey, accountEntity, logger ); } /** * 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 */ getCachedAccountEntity(accountKey: string): AccountEntity | null { const serializedAccount = this.getItem(accountKey); if (!serializedAccount) { this.removeAccountKeyFromMap(accountKey); return null; } const parsedAccount = this.validateAndParseJson(serializedAccount); if (!parsedAccount || !AccountEntity.isAccountEntity(parsedAccount)) { this.removeAccountKeyFromMap(accountKey); return null; } return CacheManager.toObject<AccountEntity>( new AccountEntity(), parsedAccount ); } /** * set account entity in the platform cache * @param account */ setAccount(account: AccountEntity): void { this.logger.trace("BrowserCacheManager.setAccount called"); const key = account.generateAccountKey(); this.setItem(key, JSON.stringify(account)); this.addAccountKeyToMap(key); } /** * Returns the array of account keys currently cached * @returns */ getAccountKeys(): Array<string> { this.logger.trace("BrowserCacheManager.getAccountKeys called"); const accountKeys = this.getItem(StaticCacheKeys.ACCOUNT_KEYS); if (accountKeys) { return JSON.parse(accountKeys); } this.logger.verbose( "BrowserCacheManager.getAccountKeys - No account keys found" ); return []; } /** * Add a new account to the key map * @param key */ addAccountKeyToMap(key: string): void { 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) ); this.logger.verbose( "BrowserCacheManager.addAccountKeyToMap account key added" ); } else { this.logger.verbose( "BrowserCacheManager.addAccountKeyToMap account key already exists in map" ); } } /** * Remove an account from the key map * @param key */ removeAccountKeyFromMap(key: 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); this.setItem( StaticCacheKeys.ACCOUNT_KEYS, JSON.stringify(accountKeys) ); 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 */ async removeAccount(key: string): Promise<void> { void super.removeAccount(key); this.removeAccountKeyFromMap(key); } /** * Remove account entity from the platform cache if it's outdated * @param accountKey */ removeOutdatedAccount(accountKey: string): void { this.removeItem(accountKey); this.removeAccountKeyFromMap(accountKey); } /** * Removes given idToken from the cache and from the key map * @param key */ removeIdToken(key: string): void { super.removeIdToken(key); this.removeTokenKey(key, CredentialType.ID_TOKEN); } /** * Removes given accessToken from the cache and from the key map * @param key */ async removeAccessToken(key: string): Promise<void> { void super.removeAccessToken(key); this.removeTokenKey(key, CredentialType.ACCESS_TOKEN); } /** * Removes given refreshToken from the cache and from the key map * @param key */ removeRefreshToken(key: string): void { super.removeRefreshToken(key); this.removeTokenKey(key, CredentialType.REFRESH_TOKEN); } /** * Gets the keys for the cached tokens associated with this clientId * @returns */ getTokenKeys(): TokenKeys { this.logger.trace("BrowserCacheManager.getTokenKeys called"); const item = this.getItem( `${StaticCacheKeys.TOKEN_KEYS}.${this.clientId}` ); if (item) { const tokenKeys = this.validateAndParseJson(item); if ( tokenKeys && tokenKeys.hasOwnProperty("idToken") && tokenKeys.hasOwnProperty("accessToken") && tokenKeys.hasOwnProperty("refreshToken") ) { return tokenKeys as TokenKeys; } else { this.logger.error( "BrowserCacheManager.getTokenKeys - Token keys found but in an unknown format. Returning empty key map." ); } } else { this.logger.verbose( "BrowserCacheManager.getTokenKeys - No token keys found" ); } return { idToken: [], accessToken: [], refreshToken: [], }; } /** * Adds the given key to the token key map * @param key * @param type */ addTokenKey(key: string, type: CredentialType): void { this.logger.trace("BrowserCacheManager addTokenKey called"); const tokenKeys = this.getTokenKeys(); switch (type) { case CredentialType.ID_TOKEN: if (tokenKeys.idToken.indexOf(key) === -1) { this.logger.info( "BrowserCacheManager: addTokenKey - idToken added to map" ); tokenKeys.idToken.push(key); } break; case CredentialType.ACCESS_TOKEN: if (tokenKeys.accessToken.indexOf(key) === -1) { this.logger.info( "BrowserCacheManager: addTokenKey - accessToken added to map" ); tokenKeys.accessToken.push(key); } break; case CredentialType.REFRESH_TOKEN: if (tokenKeys.refreshToken.indexOf(key) === -1) { this.logger.info( "BrowserCacheManager: addTokenKey - refreshToken added to map" ); tokenKeys.refreshToken.push(key); } break; default: this.logger.error( `BrowserCacheManager:addTokenKey - CredentialType provided invalid. CredentialType: ${type}` ); throw createClientAuthError( ClientAuthErrorCodes.unexpectedCredentialType ); } this.setItem( `${StaticCacheKeys.TOKEN_KEYS}.${this.clientId}`, JSON.stringify(tokenKeys) ); } /** * Removes the given key from the token key map * @param key * @param type */ removeTokenKey(key: string, type: CredentialType): void { this.logger.trace("BrowserCacheManager removeTokenKey called"); const tokenKeys = this.getTokenKeys(); switch (type) { case CredentialType.ID_TOKEN: this.logger.infoPii( `BrowserCacheManager: removeTokenKey - attempting to remove idToken with key: ${key} from map` ); const idRemoval = tokenKeys.idToken.indexOf(key); if (idRemoval > -1) { this.logger.info( "BrowserCacheManager: removeTokenKey - idToken removed from map" ); tokenKeys.idToken.splice(idRemoval, 1); } else { this.logger.info( "BrowserCacheManager: removeTokenKey - idToken does not exist in map. Either it was previously removed or it was never added." ); } break; case CredentialType.ACCESS_TOKEN: this.logger.infoPii( `BrowserCacheManager: removeTokenKey - attempting to remove accessToken with key: ${key} from map` ); const accessRemoval = tokenKeys.accessToken.indexOf(key); if (accessRemoval > -1) { this.logger.info( "BrowserCacheManager: removeTokenKey - accessToken removed from map" ); tokenKeys.accessToken.splice(accessRemoval, 1); } else { this.logger.info( "BrowserCacheManager: removeTokenKey - accessToken does not exist in map. Either it was previously removed or it was never added." ); } break; case CredentialType.REFRESH_TOKEN: this.logger.infoPii( `BrowserCacheManager: removeTokenKey - attempting to remove refreshToken with key: ${key} from map` ); const refreshRemoval = tokenKeys.refreshToken.indexOf(key); if (refreshRemoval > -1) { this.logger.info( "BrowserCacheManager: removeTokenKey - refreshToken removed from map" ); tokenKeys.refreshToken.splice(refreshRemoval, 1); } else { this.logger.info( "BrowserCacheManager: removeTokenKey - refreshToken does not exist in map. Either it was previously removed or it was never added." ); } break; default: this.logger.error( `BrowserCacheManager:removeTokenKey - CredentialType provided invalid. CredentialType: ${type}` ); throw createClientAuthError( ClientAuthErrorCodes.unexpectedCredentialType ); } this.setItem( `${StaticCacheKeys.TOKEN_KEYS}.${this.clientId}`, JSON.stringify(tokenKeys) ); } /** * generates idToken entity from a string * @param idTokenKey */ getIdTokenCredential(idTokenKey: string): IdTokenEntity | null { const value = this.getItem(idTokenKey); if (!value) { this.logger.trace( "BrowserCacheManager.getIdTokenCredential: called, no cache hit" ); this.removeTokenKey(idTokenKey, CredentialType.ID_TOKEN); return null; } const parsedIdToken = this.validateAndParseJson(value); if (!parsedIdToken || !CacheHelpers.isIdTokenEntity(parsedIdToken)) { this.logger.trace( "BrowserCacheManager.getIdTokenCredential: called, no cache hit" ); this.removeTokenKey(idTokenKey, CredentialType.ID_TOKEN); return null; } this.logger.trace( "BrowserCacheManager.getIdTokenCredential: cache hit" ); return parsedIdToken as IdTokenEntity; } /** * set IdToken credential to the platform cache * @param idToken */ setIdTokenCredential(idToken: IdTokenEntity): void { this.logger.trace("BrowserCacheManager.setIdTokenCredential called"); const idTokenKey = CacheHelpers.generateCredentialKey(idToken); this.setItem(idTokenKey, JSON.stringify(idToken)); this.addTokenKey(idTokenKey, CredentialType.ID_TOKEN); } /** * generates accessToken entity from a string * @param key */ getAccessTokenCredential(accessTokenKey: string): AccessTokenEntity | null { const value = this.getItem(accessTokenKey); if (!value) { this.logger.trace( "BrowserCacheManager.getAccessTokenCredential: called, no cache hit" ); this.removeTokenKey(accessTokenKey, CredentialType.ACCESS_TOKEN); return null; } const parsedAccessToken = this.validateAndParseJson(value); if ( !parsedAccessToken || !CacheHelpers.isAccessTokenEntity(parsedAccessToken) ) { this.logger.trace( "BrowserCacheManager.getAccessTokenCredential: called, no cache hit" ); this.removeTokenKey(accessTokenKey, CredentialType.ACCESS_TOKEN); return null; } this.logger.trace( "BrowserCacheManager.getAccessTokenCredential: cache hit" ); return parsedAccessToken as AccessTokenEntity; } /** * set accessToken credential to the platform cache * @param accessToken */ setAccessTokenCredential(accessToken: AccessTokenEntity): void { this.logger.trace( "BrowserCacheManager.setAccessTokenCredential called" ); const accessTokenKey = CacheHelpers.generateCredentialKey(accessToken); this.setItem(accessTokenKey, JSON.stringify(accessToken)); this.addTokenKey(accessTokenKey, CredentialType.ACCESS_TOKEN); } /** * generates refreshToken entity from a string * @param refreshTokenKey */ getRefreshTokenCredential( refreshTokenKey: string ): RefreshTokenEntity | null { const value = this.getItem(refreshTokenKey); if (!value) { this.logger.trace( "BrowserCacheManager.getRefreshTokenCredential: called, no cache hit" ); this.removeTokenKey(refreshTokenKey, CredentialType.REFRESH_TOKEN); return null; } const parsedRefreshToken = this.validateAndParseJson(value); if ( !parsedRefreshToken || !CacheHelpers.isRefreshTokenEntity(parsedRefreshToken) ) { this.logger.trace( "BrowserCacheManager.getRefreshTokenCredential: called, no cache hit" ); this.removeTokenKey(refreshTokenKey, CredentialType.REFRESH_TOKEN); return null; } this.logger.trace( "BrowserCacheManager.getRefreshTokenCredential: cache hit" ); return parsedRefreshToken as RefreshTokenEntity; } /** * set refreshToken credential to the platform cache * @param refreshToken */ setRefreshTokenCredential(refreshToken: RefreshTokenEntity): void { this.logger.trace( "BrowserCacheManager.setRefreshTokenCredential called" ); const refreshTokenKey = CacheHelpers.generateCredentialKey(refreshToken); this.setItem(refreshTokenKey, JSON.stringify(refreshToken)); this.addTokenKey(refreshTokenKey, CredentialType.REFRESH_TOKEN); } /** * fetch appMetadata entity from the platform cache * @param appMetadataKey */ getAppMetadata(appMetadataKey: string): AppMetadataEntity | null { const value = this.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): void { this.logger.trace("BrowserCacheManager.setAppMetadata called"); const appMetadataKey = CacheHelpers.generateAppMetadataKey(appMetadata); this.setItem(appMetadataKey, JSON.stringify(appMetadata)); } /** * fetch server telemetry entity from the platform cache * @param serverTelemetryKey */ getServerTelemetry( serverTelemetryKey: string ): ServerTelemetryEntity | null { const value = this.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 ): void { this.logger.trace("BrowserCacheManager.setServerTelemetry called"); this.setItem(serverTelemetryKey, JSON.stringify(serverTelemetry)); } /** * */ 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(): AccountInfo | null { const activeAccountKeyFilters = this.generateCacheKey( PersistentCacheKeys.ACTIVE_ACCOUNT_FILTERS ); const activeAccountValueFilters = this.getItem(activeAccountKeyFilters); if (!activeAccountValueFilters) { // if new active account cache type isn't found, it's an old version, so look for that instead this.logger.trace( "BrowserCacheManager.getActiveAccount: No active account filters cache schema found, looking for legacy schema" ); const activeAccountKeyLocal = this.generateCacheKey( PersistentCacheKeys.ACTIVE_ACCOUNT ); const activeAccountValueLocal = this.getItem(activeAccountKeyLocal); if (!activeAccountValueLocal) { this.logger.trace( "BrowserCacheManager.getActiveAccount: No active account found" ); return null; } const activeAccount = this.getAccountInfoFilteredBy({ localAccountId: activeAccountValueLocal, }); if (activeAccount) { this.logger.trace( "BrowserCacheManager.getActiveAccount: Legacy active account cache schema found" ); this.logger.trace( "BrowserCacheManager.getActiveAccount: Adding active account filters cache schema" ); this.setActiveAccount(activeAccount); return activeAccount; } 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, }); } 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): void { const activeAccountKey = this.generateCacheKey( PersistentCacheKeys.ACTIVE_ACCOUNT_FILTERS ); const activeAccountKeyLocal = this.generateCacheKey( PersistentCacheKeys.ACTIVE_ACCOUNT ); if (account) { this.logger.verbose("setActiveAccount: Active account set"); const activeAccountValue: ActiveAccountFilters = { homeAccountId: account.homeAccountId, localAccountId: account.localAccountId, tenantId: account.tenantId, }; this.browserStorage.setItem( activeAccountKey, JSON.stringify(activeAccountValue) ); this.browserStorage.setItem( activeAccountKeyLocal, account.localAccountId ); } else { this.logger.verbose( "setActiveAccount: No account passed, active account not set" ); this.browserStorage.removeItem(activeAccountKey); this.browserStorage.removeItem(activeAccountKeyLocal); } } /** * fetch throttling entity from the platform cache * @param throttlingCacheKey */ getThrottlingCache(throttlingCacheKey: string): ThrottlingEntity | null { const value = this.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 ): void { this.logger.trace("BrowserCacheManager.setThrottlingCache called"); this.setItem(throttlingCacheKey, JSON.stringify(throttlingCache)); } /** * 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.getItemCookie(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.setItemCookie(key, value); } } /** * 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.clearItemCookie(key); } } /** * Gets all keys in window. */ getKeys(): string[] { return this.browserStorage.getKeys(); } /** * Clears all cache entries created by MSAL. */ async clear(): Promise<void> { // Removes all accounts and their credentials await this.removeAllAccounts(); this.removeAppMetadata(); // 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} * @returns */ async clearTokensAndKeysWithClaims( performanceClient: IPerformanceClient ): Promise<void> { performanceClient.addQueueMeasurement( PerformanceEvents.ClearTokensAndKeysWithClaims ); const tokenKeys = this.getTokenKeys(); const removedAccessTokens: Array<Promise<void>> = []; 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); if ( credential?.requestedClaimsHash && key.includes(credential.requestedClaimsHash.toLowerCase()) ) { removedAccessTokens.push(this.removeAccessToken(key)); } }); await Promise.all(removedAccessTokens); // warn if any access tokens are removed if (removedAccessTokens.length > 0) { this.logger.warning( `${removedAccessTokens.length} access tokens with claims in the cache keys have been removed from the cache.` ); } } /** * Add value to cookies * @param cookieName * @param cookieValue * @param expires * @deprecated */ setItemCookie( cookieName: string, cookieValue: string, expires?: number ): void { let cookieStr = `${encodeURIComponent(cookieName)}=${encodeURIComponent( cookieValue )};path=/;SameSite=Lax;`; if (expires) { const expireTime = this.getCookieExpirationTime(expires); cookieStr += `expires=${expireTime};`; } if (this.cacheConfig.secureCookies) { cookieStr += "Secure;"; } document.cookie = cookieStr; } /** * Get one item by key from cookies * @param cookieName * @deprecated */ getItemCookie(cookieName: string): string { const name = `${encodeURIComponent(cookieName)}=`; const cookieList = document.cookie.split(";"); for (let i: number = 0; i < cookieList.length; i++) { let cookie = cookieList[i]; while (cookie.charAt(0) === " ") { cookie = cookie.substring(1); } if (cookie.indexOf(name) === 0) { return decodeURIComponent( cookie.substring(name.length, cookie.length) ); } } return Constants.EMPTY_STRI