UNPKG

@azure/msal-browser

Version:
1,088 lines (1,087 loc) 53.7 kB
/*! @azure/msal-browser v4.21.0 2025-08-19 */ 'use strict'; import { CacheManager, CacheHelpers, TimeUtils, DEFAULT_TOKEN_RENEWAL_OFFSET_SEC, createCacheError, CacheErrorCodes, invokeAsync, PerformanceEvents, AccountEntity, Constants, PersistentCacheKeys, StringUtils, AuthenticationScheme, CacheError, DEFAULT_CRYPTO_IMPLEMENTATION, CredentialType } from '@azure/msal-common/browser'; import { createBrowserAuthError } from '../error/BrowserAuthError.mjs'; import { BrowserCacheLocation, InMemoryCacheKeys, TemporaryCacheKeys, INTERACTION_TYPE } from '../utils/BrowserConstants.mjs'; import { ACCOUNT_SCHEMA_VERSION, CREDENTIAL_SCHEMA_VERSION, getAccountKeysCacheKey, PREFIX, CACHE_KEY_SEPARATOR, VERSION_CACHE_KEY, getTokenKeysCacheKey } from './CacheKeys.mjs'; import { LocalStorage } from './LocalStorage.mjs'; import { SessionStorage } from './SessionStorage.mjs'; import { MemoryStorage } from './MemoryStorage.mjs'; import { base64Decode } from '../encode/Base64Decode.mjs'; import { base64Encode } from '../encode/Base64Encode.mjs'; import { CookieStorage } from './CookieStorage.mjs'; import { getAccountKeys, getTokenKeys } from './CacheHelpers.mjs'; import { EventType } from '../event/EventType.mjs'; import { clearHash } from '../utils/BrowserUtils.mjs'; import { version } from '../packageMetadata.mjs'; import { removeElementFromArray } from '../utils/Helpers.mjs'; import { isEncrypted } from './EncryptedData.mjs'; import { noTokenRequestCacheError, unableToParseTokenRequestCacheError, interactionInProgress } from '../error/BrowserAuthErrorCodes.mjs'; /* * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ /** * 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. */ class BrowserCacheManager extends CacheManager { constructor(clientId, cacheConfig, cryptoImpl, logger, performanceClient, eventHandler, 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) { this.performanceClient.addFields({ cacheLocation: this.cacheConfig.cacheLocation, cacheRetentionDays: this.cacheConfig.cacheRetentionDays, }, correlationId); await this.browserStorage.initialize(correlationId); await this.migrateExistingCache(correlationId); this.trackVersionChanges(correlationId); } /** * Migrates any existing cache data from previous versions of MSAL.js into the current cache structure. */ async migrateExistingCache(correlationId) { const accountKeys0 = getAccountKeys(this.browserStorage, 0); const tokenKeys0 = getTokenKeys(this.clientId, this.browserStorage, 0); this.performanceClient.addFields({ oldAccountCount: accountKeys0.length, oldAccessCount: tokenKeys0.accessToken.length, oldIdCount: tokenKeys0.idToken.length, oldRefreshCount: tokenKeys0.refreshToken.length, }, correlationId); const accountKeys1 = getAccountKeys(this.browserStorage, 1); const tokenKeys1 = getTokenKeys(this.clientId, this.browserStorage, 1); this.performanceClient.addFields({ currAccountCount: accountKeys1.length, currAccessCount: tokenKeys1.accessToken.length, currIdCount: tokenKeys1.idToken.length, currRefreshCount: tokenKeys1.refreshToken.length, }, correlationId); await Promise.all([ this.updateV0ToCurrent(ACCOUNT_SCHEMA_VERSION, accountKeys0, accountKeys1, correlationId), this.updateV0ToCurrent(CREDENTIAL_SCHEMA_VERSION, tokenKeys0.idToken, tokenKeys1.idToken, correlationId), this.updateV0ToCurrent(CREDENTIAL_SCHEMA_VERSION, tokenKeys0.accessToken, tokenKeys1.accessToken, correlationId), this.updateV0ToCurrent(CREDENTIAL_SCHEMA_VERSION, tokenKeys0.refreshToken, tokenKeys1.refreshToken, correlationId), ]); if (accountKeys0.length > 0) { this.browserStorage.setItem(getAccountKeysCacheKey(0), JSON.stringify(accountKeys0)); } else { this.browserStorage.removeItem(getAccountKeysCacheKey(0)); } if (accountKeys1.length > 0) { this.browserStorage.setItem(getAccountKeysCacheKey(1), JSON.stringify(accountKeys1)); } else { this.browserStorage.removeItem(getAccountKeysCacheKey(1)); } this.setTokenKeys(tokenKeys0, correlationId, 0); this.setTokenKeys(tokenKeys1, correlationId, 1); } async updateV0ToCurrent(currentSchema, v0Keys, v1Keys, correlationId) { const upgradePromises = []; for (const v0Key of [...v0Keys]) { const rawV0Value = this.browserStorage.getItem(v0Key); const parsedV0Value = this.validateAndParseJson(rawV0Value || ""); if (!parsedV0Value) { removeElementFromArray(v0Keys, v0Key); continue; } if (!parsedV0Value.lastUpdatedAt) { // Add lastUpdatedAt to the existing v0 entry if it doesnt exist so we know when it's safe to remove it parsedV0Value.lastUpdatedAt = Date.now().toString(); this.setItem(v0Key, JSON.stringify(parsedV0Value), correlationId); } const decryptedData = isEncrypted(parsedV0Value) ? await this.browserStorage.decryptData(v0Key, parsedV0Value, correlationId) : parsedV0Value; let expirationTime; if (decryptedData) { if (CacheHelpers.isAccessTokenEntity(decryptedData)) { expirationTime = decryptedData.expiresOn; } else if (CacheHelpers.isRefreshTokenEntity(decryptedData)) { expirationTime = decryptedData.expiresOn; } } if (!decryptedData || TimeUtils.isCacheExpired(parsedV0Value.lastUpdatedAt, this.cacheConfig.cacheRetentionDays) || (expirationTime && TimeUtils.isTokenExpired(expirationTime, DEFAULT_TOKEN_RENEWAL_OFFSET_SEC))) { this.browserStorage.removeItem(v0Key); removeElementFromArray(v0Keys, v0Key); this.performanceClient.incrementFields({ expiredCacheRemovedCount: 1 }, correlationId); continue; } if (this.cacheConfig.cacheLocation !== BrowserCacheLocation.LocalStorage || isEncrypted(parsedV0Value)) { const v1Key = `${PREFIX}.${currentSchema}${CACHE_KEY_SEPARATOR}${v0Key}`; const rawV1Entry = this.browserStorage.getItem(v1Key); if (!rawV1Entry) { upgradePromises.push(this.setUserData(v1Key, JSON.stringify(decryptedData), correlationId, parsedV0Value.lastUpdatedAt).then(() => { v1Keys.push(v1Key); this.performanceClient.incrementFields({ upgradedCacheCount: 1 }, correlationId); })); continue; } else { const parsedV1Entry = this.validateAndParseJson(rawV1Entry); // If the entry already exists but is older than the v0 entry, replace it if (Number(parsedV0Value.lastUpdatedAt) > Number(parsedV1Entry.lastUpdatedAt)) { upgradePromises.push(this.setUserData(v1Key, JSON.stringify(decryptedData), correlationId, parsedV0Value.lastUpdatedAt).then(() => { this.performanceClient.incrementFields({ updatedCacheFromV0Count: 1 }, correlationId); })); continue; } } } /* * Note: If we reach here for unencrypted localStorage data, we continue without migrating * as we can't migrate unencrypted localStorage data right now since we can't guarantee KMSI=no */ } return Promise.all(upgradePromises); } /** * Tracks upgrades and downgrades for telemetry and debugging purposes */ trackVersionChanges(correlationId) { const previousVersion = this.browserStorage.getItem(VERSION_CACHE_KEY); if (previousVersion) { this.logger.info(`MSAL.js was last initialized by version: ${previousVersion}`); this.performanceClient.addFields({ previousLibraryVersion: previousVersion }, correlationId); } if (previousVersion !== version) { this.setItem(VERSION_CACHE_KEY, version, correlationId); } } /** * Parses passed value as JSON object, JSON.parse() will throw an error. * @param input */ validateAndParseJson(jsonValue) { if (!jsonValue) { return 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, value, correlationId) { let tokenKeysV0Count = 0; let accessTokenKeys = []; 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 if (i <= tokenKeysV0Count) { this.removeAccessTokenKeys(accessTokenKeys.slice(0, i), correlationId, 0); } else { this.removeAccessTokenKeys(accessTokenKeys.slice(0, tokenKeysV0Count), correlationId, 0); this.removeAccessTokenKeys(accessTokenKeys.slice(tokenKeysV0Count, 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 we are currently trying to set the token keys, use the value we're trying to set const tokenKeys0 = key === getTokenKeysCacheKey(this.clientId, 0) ? JSON.parse(value).accessToken : this.getTokenKeys(0).accessToken; const tokenKeys1 = key === getTokenKeysCacheKey(this.clientId) ? JSON.parse(value).accessToken : this.getTokenKeys().accessToken; accessTokenKeys = [...tokenKeys0, ...tokenKeys1]; tokenKeysV0Count = tokenKeys0.length; } 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, value, correlationId, timestamp) { let tokenKeysV0Count = 0; let accessTokenKeys = []; 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 if (i <= tokenKeysV0Count) { this.removeAccessTokenKeys(accessTokenKeys.slice(0, i), correlationId, 0); } else { this.removeAccessTokenKeys(accessTokenKeys.slice(0, tokenKeysV0Count), correlationId, 0); this.removeAccessTokenKeys(accessTokenKeys.slice(tokenKeysV0Count, 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) { const tokenKeys0 = this.getTokenKeys(0).accessToken; const tokenKeys1 = this.getTokenKeys().accessToken; accessTokenKeys = [...tokenKeys0, ...tokenKeys1]; tokenKeysV0Count = tokenKeys0.length; } 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, correlationId) { 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(new AccountEntity(), parsedAccount); } /** * set account entity in the platform cache * @param account */ async setAccount(account, correlationId) { this.logger.trace("BrowserCacheManager.setAccount called"); const key = this.generateAccountKey(account.getAccountInfo()); 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() { return getAccountKeys(this.browserStorage); } /** * Add a new account to the key map * @param key */ addAccountKeyToMap(key, correlationId) { 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(getAccountKeysCacheKey(), 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, correlationId) { 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(getAccountKeysCacheKey()); return; } else { this.setItem(getAccountKeysCacheKey(), 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(account, correlationId) { const activeAccount = this.getActiveAccount(correlationId); if (activeAccount?.homeAccountId === account.homeAccountId && activeAccount?.environment === account.environment) { this.setActiveAccount(null, correlationId); } super.removeAccount(account, correlationId); this.removeAccountKeyFromMap(this.generateAccountKey(account), correlationId); // Remove all other associated cache items this.browserStorage.getKeys().forEach((key) => { if (key.includes(account.homeAccountId) && key.includes(account.environment)) { this.browserStorage.removeItem(key); } }); /** * @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); } } /** * Removes given idToken from the cache and from the key map * @param key */ removeIdToken(key, correlationId) { 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, correlationId, updateTokenKeys = true) { 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, correlationId, schemaVersion = CREDENTIAL_SCHEMA_VERSION) { this.logger.trace("removeAccessTokenKey called"); const tokenKeys = this.getTokenKeys(schemaVersion); 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, schemaVersion); return; } } /** * Removes given refreshToken from the cache and from the key map * @param key */ removeRefreshToken(key, correlationId) { 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(schemaVersion = CREDENTIAL_SCHEMA_VERSION) { return getTokenKeys(this.clientId, this.browserStorage, schemaVersion); } /** * Stores the token keys in the cache * @param tokenKeys * @param correlationId * @returns */ setTokenKeys(tokenKeys, correlationId, schemaVersion = CREDENTIAL_SCHEMA_VERSION) { if (tokenKeys.idToken.length === 0 && tokenKeys.accessToken.length === 0 && tokenKeys.refreshToken.length === 0) { // If no keys left, remove the map this.removeItem(getTokenKeysCacheKey(this.clientId, schemaVersion)); return; } else { this.setItem(getTokenKeysCacheKey(this.clientId, schemaVersion), JSON.stringify(tokenKeys), correlationId); } } /** * generates idToken entity from a string * @param idTokenKey */ getIdTokenCredential(idTokenKey, correlationId) { 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; } /** * set IdToken credential to the platform cache * @param idToken */ async setIdTokenCredential(idToken, correlationId) { this.logger.trace("BrowserCacheManager.setIdTokenCredential called"); const idTokenKey = this.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, correlationId) { 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; } /** * set accessToken credential to the platform cache * @param accessToken */ async setAccessTokenCredential(accessToken, correlationId) { this.logger.trace("BrowserCacheManager.setAccessTokenCredential called"); const accessTokenKey = this.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, correlationId) { 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; } /** * set refreshToken credential to the platform cache * @param refreshToken */ async setRefreshTokenCredential(refreshToken, correlationId) { this.logger.trace("BrowserCacheManager.setRefreshTokenCredential called"); const refreshTokenKey = this.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) { 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; } /** * set appMetadata entity to the platform cache * @param appMetadata */ setAppMetadata(appMetadata, correlationId) { 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) { 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; } /** * set server telemetry entity to the platform cache * @param serverTelemetryKey * @param serverTelemetry */ setServerTelemetry(serverTelemetryKey, serverTelemetry, correlationId) { this.logger.trace("BrowserCacheManager.setServerTelemetry called"); this.setItem(serverTelemetryKey, JSON.stringify(serverTelemetry), correlationId); } /** * */ getAuthorityMetadata(key) { 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; } return null; } /** * */ getAuthorityMetadataKeys() { const allKeys = this.internalStorage.getKeys(); return allKeys.filter((key) => { return this.isAuthorityMetadata(key); }); } /** * Sets wrapper metadata in memory * @param wrapperSKU * @param wrapperVersion */ setWrapperMetadata(wrapperSKU, wrapperVersion) { this.internalStorage.setItem(InMemoryCacheKeys.WRAPPER_SKU, wrapperSKU); this.internalStorage.setItem(InMemoryCacheKeys.WRAPPER_VER, wrapperVersion); } /** * Returns wrapper metadata from in-memory storage */ getWrapperMetadata() { 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, entity) { this.logger.trace("BrowserCacheManager.setAuthorityMetadata called"); this.internalStorage.setItem(key, JSON.stringify(entity)); } /** * Gets the active account */ getActiveAccount(correlationId) { 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); 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, correlationId) { const activeAccountKey = this.generateCacheKey(PersistentCacheKeys.ACTIVE_ACCOUNT_FILTERS); if (account) { this.logger.verbose("setActiveAccount: Active account set"); const activeAccountValue = { 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) { 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; } /** * set throttling entity to the platform cache * @param throttlingCacheKey * @param throttlingCache */ setThrottlingCache(throttlingCacheKey, throttlingCache, correlationId) { 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, generateKey) { 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, value, generateKey) { 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) { 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) { 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() { return this.browserStorage.getKeys(); } /** * Clears all cache entries created by MSAL. */ clear(correlationId) { // 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) => { if (cacheKey.indexOf(PREFIX) !== -1 || cacheKey.indexOf(this.clientId) !== -1) { this.removeTemporaryItem(cacheKey); } }); // Removes all remaining MSAL cache items this.browserStorage.getKeys().forEach((cacheKey) => { if (cacheKey.indexOf(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) { this.performanceClient.addQueueMeasurement(PerformanceEvents.ClearTokensAndKeysWithClaims, correlationId); const tokenKeys = this.getTokenKeys(); let removedAccessTokens = 0; tokenKeys.accessToken.forEach((key) => { // 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 * @param key * @param addInstanceId */ generateCacheKey(key) { if (StringUtils.startsWith(key, PREFIX)) { return key; } return `${PREFIX}.${this.clientId}.${key}`; } /** * Cache Key: msal.<schema_version>-<home_account_id>-<environment>-<credential_type>-<client_id or familyId>-<realm>-<scopes>-<claims hash>-<scheme> * IdToken Example: uid.utid-login.microsoftonline.com-idtoken-app_client_id-contoso.com * AccessToken Example: uid.utid-login.microsoftonline.com-accesstoken-app_client_id-contoso.com-scope1 scope2--pop * RefreshToken Example: uid.utid-login.microsoftonline.com-refreshtoken-1-contoso.com * @param credentialEntity * @returns */ generateCredentialKey(credential) { const familyId = (credential.credentialType === CredentialType.REFRESH_TOKEN && credential.familyId) || credential.clientId; const scheme = credential.tokenType && credential.tokenType.toLowerCase() !== AuthenticationScheme.BEARER.toLowerCase() ? credential.tokenType.toLowerCase() : ""; const credentialKey = [ `${PREFIX}.${CREDENTIAL_SCHEMA_VERSION}`, credential.homeAccountId, credential.environment, credential.credentialType, familyId, credential.realm || "", credential.target || "", credential.requestedClaimsHash || "", scheme, ]; return credentialKey.join(CACHE_KEY_SEPARATOR).toLowerCase(); } /** * Cache Key: msal.<schema_version>.<home_account_id>.<environment>.<tenant_id> * @param account * @returns */ generateAccountKey(account) { const homeTenantId = account.homeAccountId.split(".")[1]; const accountKey = [ `${PREFIX}.${ACCOUNT_SCHEMA_VERSION}`, account.homeAccountId, account.environment, homeTenantId || account.tenantId || "", ]; return accountKey.join(CACHE_KEY_SEPARATOR).toLowerCase(); } /** * Reset all temporary cache items * @param state */ resetRequestCache() { 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, codeVerifier) { 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() { 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(noTokenRequestCacheError); } const encodedVerifier = this.getTemporaryCache(TemporaryCacheKeys.VERIFIER, true); let parsedRequest; 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(unableToParseTokenRequestCacheError); } return [parsedRequest, verifier]; } /** * Gets cached native request for redirect flows */ getCachedNativeRequest() { 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); if (!parsedRequest) { this.logger.error("BrowserCacheManager.getCachedNativeRequest: Unable to parse native request"); return null; } return parsedRequest; } isInteractionInProgress(matchClientId) { const clientId = this.getInteractionInProgress()?.clientId; if (matchClientId) { return clientId === this.clientId; } else { return !!clientId; } } getInteractionInProgress() { const key = `${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, type = INTERACTION_TYPE.SIGNIN) { // Ensure we don't overwrite interaction in progress for a different clientId const key = `${PREFIX}.${TemporaryCacheKeys.INTERACTION_STATUS_KEY}`; if (inProgress) { if (this.getInteractionInProgress()) { throw createBrowserAuthError(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, request) { 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 store