1380 lines
62 KiB
JavaScript
1380 lines
62 KiB
JavaScript
/*! @azure/msal-browser v3.21.0 2024-08-13 */
|
|
'use strict';
|
|
import { CacheManager, CredentialType, CacheHelpers, AccountEntity, createClientAuthError, ClientAuthErrorCodes, Constants, PersistentCacheKeys, PerformanceEvents, StringUtils, ProtocolUtils, AuthToken, CacheError, DEFAULT_CRYPTO_IMPLEMENTATION, CcsCredentialType } from '@azure/msal-common';
|
|
import { createBrowserAuthError } from '../error/BrowserAuthError.mjs';
|
|
import { BrowserCacheLocation, StaticCacheKeys, InMemoryCacheKeys, TemporaryCacheKeys } from '../utils/BrowserConstants.mjs';
|
|
import { BrowserStorage } from './BrowserStorage.mjs';
|
|
import { MemoryStorage } from './MemoryStorage.mjs';
|
|
import { extractBrowserRequestState } from '../utils/BrowserProtocolUtils.mjs';
|
|
import { base64Decode } from '../encode/Base64Decode.mjs';
|
|
import { base64Encode } from '../encode/Base64Encode.mjs';
|
|
import { noTokenRequestCacheError, unableToParseTokenRequestCacheError, noCachedAuthorityError, 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, staticAuthorityOptions, performanceClient) {
|
|
super(clientId, cryptoImpl, logger, staticAuthorityOptions);
|
|
// Cookie life calculation (hours * minutes * seconds * ms)
|
|
this.COOKIE_LIFE_MULTIPLIER = 24 * 60 * 60 * 1000;
|
|
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
|
|
*/
|
|
setupBrowserStorage(cacheLocation) {
|
|
switch (cacheLocation) {
|
|
case BrowserCacheLocation.LocalStorage:
|
|
case BrowserCacheLocation.SessionStorage:
|
|
try {
|
|
return new BrowserStorage(cacheLocation);
|
|
}
|
|
catch (e) {
|
|
this.logger.verbose(e);
|
|
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
|
|
*/
|
|
setupTemporaryCacheStorage(temporaryCacheLocation, cacheLocation) {
|
|
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);
|
|
return this.internalStorage;
|
|
}
|
|
case BrowserCacheLocation.MemoryStorage:
|
|
default:
|
|
return this.internalStorage;
|
|
}
|
|
}
|
|
/**
|
|
* Migrate all old cache entries to new schema. No rollback supported.
|
|
* @param storeAuthStateInCookie
|
|
*/
|
|
migrateCacheEntries() {
|
|
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, index) => {
|
|
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
|
|
*/
|
|
createKeyMaps() {
|
|
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;
|
|
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;
|
|
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;
|
|
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;
|
|
// 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
|
|
*/
|
|
validateAndParseJson(jsonValue) {
|
|
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) {
|
|
return this.browserStorage.getItem(key);
|
|
}
|
|
/**
|
|
* sets the entry in the browser storage
|
|
* @param key
|
|
* @param value
|
|
*/
|
|
setItem(key, value) {
|
|
this.browserStorage.setItem(key, value);
|
|
}
|
|
/**
|
|
* fetch the account entity from the platform cache
|
|
* @param accountKey
|
|
*/
|
|
getAccount(accountKey, logger) {
|
|
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) {
|
|
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(new AccountEntity(), parsedAccount);
|
|
}
|
|
/**
|
|
* set account entity in the platform cache
|
|
* @param account
|
|
*/
|
|
setAccount(account) {
|
|
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() {
|
|
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) {
|
|
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) {
|
|
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) {
|
|
void super.removeAccount(key);
|
|
this.removeAccountKeyFromMap(key);
|
|
}
|
|
/**
|
|
* Remove account entity from the platform cache if it's outdated
|
|
* @param accountKey
|
|
*/
|
|
removeOutdatedAccount(accountKey) {
|
|
this.removeItem(accountKey);
|
|
this.removeAccountKeyFromMap(accountKey);
|
|
}
|
|
/**
|
|
* Removes given idToken from the cache and from the key map
|
|
* @param key
|
|
*/
|
|
removeIdToken(key) {
|
|
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) {
|
|
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) {
|
|
super.removeRefreshToken(key);
|
|
this.removeTokenKey(key, CredentialType.REFRESH_TOKEN);
|
|
}
|
|
/**
|
|
* Gets the keys for the cached tokens associated with this clientId
|
|
* @returns
|
|
*/
|
|
getTokenKeys() {
|
|
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;
|
|
}
|
|
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, type) {
|
|
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, type) {
|
|
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) {
|
|
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;
|
|
}
|
|
/**
|
|
* set IdToken credential to the platform cache
|
|
* @param idToken
|
|
*/
|
|
setIdTokenCredential(idToken) {
|
|
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) {
|
|
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;
|
|
}
|
|
/**
|
|
* set accessToken credential to the platform cache
|
|
* @param accessToken
|
|
*/
|
|
setAccessTokenCredential(accessToken) {
|
|
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) {
|
|
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;
|
|
}
|
|
/**
|
|
* set refreshToken credential to the platform cache
|
|
* @param refreshToken
|
|
*/
|
|
setRefreshTokenCredential(refreshToken) {
|
|
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) {
|
|
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;
|
|
}
|
|
/**
|
|
* set appMetadata entity to the platform cache
|
|
* @param appMetadata
|
|
*/
|
|
setAppMetadata(appMetadata) {
|
|
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) {
|
|
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;
|
|
}
|
|
/**
|
|
* set server telemetry entity to the platform cache
|
|
* @param serverTelemetryKey
|
|
* @param serverTelemetry
|
|
*/
|
|
setServerTelemetry(serverTelemetryKey, serverTelemetry) {
|
|
this.logger.trace("BrowserCacheManager.setServerTelemetry called");
|
|
this.setItem(serverTelemetryKey, JSON.stringify(serverTelemetry));
|
|
}
|
|
/**
|
|
*
|
|
*/
|
|
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() {
|
|
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);
|
|
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) {
|
|
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 = {
|
|
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) {
|
|
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;
|
|
}
|
|
/**
|
|
* set throttling entity to the platform cache
|
|
* @param throttlingCacheKey
|
|
* @param throttlingCache
|
|
*/
|
|
setThrottlingCache(throttlingCacheKey, throttlingCache) {
|
|
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, generateKey) {
|
|
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, 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.setItemCookie(key, value);
|
|
}
|
|
}
|
|
/**
|
|
* 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.clearItemCookie(key);
|
|
}
|
|
}
|
|
/**
|
|
* Gets all keys in window.
|
|
*/
|
|
getKeys() {
|
|
return this.browserStorage.getKeys();
|
|
}
|
|
/**
|
|
* Clears all cache entries created by MSAL.
|
|
*/
|
|
async clear() {
|
|
// 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) => {
|
|
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) => {
|
|
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
|
|
*/
|
|
async clearTokensAndKeysWithClaims(performanceClient, correlationId) {
|
|
performanceClient.addQueueMeasurement(PerformanceEvents.ClearTokensAndKeysWithClaims, correlationId);
|
|
const tokenKeys = this.getTokenKeys();
|
|
const removedAccessTokens = [];
|
|
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);
|
|
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, cookieValue, expires) {
|
|
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) {
|
|
const name = `${encodeURIComponent(cookieName)}=`;
|
|
const cookieList = document.cookie.split(";");
|
|
for (let i = 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_STRING;
|
|
}
|
|
/**
|
|
* Clear all msal-related cookies currently set in the browser. Should only be used to clear temporary cache items.
|
|
* @deprecated
|
|
*/
|
|
clearMsalCookies() {
|
|
const cookiePrefix = `${Constants.CACHE_PREFIX}.${this.clientId}`;
|
|
const cookieList = document.cookie.split(";");
|
|
cookieList.forEach((cookie) => {
|
|
while (cookie.charAt(0) === " ") {
|
|
// eslint-disable-next-line no-param-reassign
|
|
cookie = cookie.substring(1);
|
|
}
|
|
if (cookie.indexOf(cookiePrefix) === 0) {
|
|
const cookieKey = cookie.split("=")[0];
|
|
this.clearItemCookie(cookieKey);
|
|
}
|
|
});
|
|
}
|
|
/**
|
|
* Clear an item in the cookies by key
|
|
* @param cookieName
|
|
* @deprecated
|
|
*/
|
|
clearItemCookie(cookieName) {
|
|
this.setItemCookie(cookieName, Constants.EMPTY_STRING, -1);
|
|
}
|
|
/**
|
|
* Get cookie expiration time
|
|
* @param cookieLifeDays
|
|
* @deprecated
|
|
*/
|
|
getCookieExpirationTime(cookieLifeDays) {
|
|
const today = new Date();
|
|
const expr = new Date(today.getTime() + cookieLifeDays * this.COOKIE_LIFE_MULTIPLIER);
|
|
return expr.toUTCString();
|
|
}
|
|
/**
|
|
* 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) {
|
|
const generatedKey = this.validateAndParseJson(key);
|
|
if (!generatedKey) {
|
|
if (StringUtils.startsWith(key, Constants.CACHE_PREFIX) ||
|
|
StringUtils.startsWith(key, PersistentCacheKeys.ADAL_ID_TOKEN)) {
|
|
return key;
|
|
}
|
|
return `${Constants.CACHE_PREFIX}.${this.clientId}.${key}`;
|
|
}
|
|
return JSON.stringify(key);
|
|
}
|
|
/**
|
|
* Create authorityKey to cache authority
|
|
* @param state
|
|
*/
|
|
generateAuthorityKey(stateString) {
|
|
const { libraryState: { id: stateId }, } = ProtocolUtils.parseRequestState(this.cryptoImpl, stateString);
|
|
return this.generateCacheKey(`${TemporaryCacheKeys.AUTHORITY}.${stateId}`);
|
|
}
|
|
/**
|
|
* Create Nonce key to cache nonce
|
|
* @param state
|
|
*/
|
|
generateNonceKey(stateString) {
|
|
const { libraryState: { id: stateId }, } = ProtocolUtils.parseRequestState(this.cryptoImpl, stateString);
|
|
return this.generateCacheKey(`${TemporaryCacheKeys.NONCE_IDTOKEN}.${stateId}`);
|
|
}
|
|
/**
|
|
* Creates full cache key for the request state
|
|
* @param stateString State string for the request
|
|
*/
|
|
generateStateKey(stateString) {
|
|
// Use the library state id to key temp storage for uniqueness for multiple concurrent requests
|
|
const { libraryState: { id: stateId }, } = ProtocolUtils.parseRequestState(this.cryptoImpl, stateString);
|
|
return this.generateCacheKey(`${TemporaryCacheKeys.REQUEST_STATE}.${stateId}`);
|
|
}
|
|
/**
|
|
* Gets the cached authority based on the cached state. Returns empty if no cached state found.
|
|
*/
|
|
getCachedAuthority(cachedState) {
|
|
const stateCacheKey = this.generateStateKey(cachedState);
|
|
const state = this.getTemporaryCache(stateCacheKey);
|
|
if (!state) {
|
|
return null;
|
|
}
|
|
const authorityCacheKey = this.generateAuthorityKey(state);
|
|
return this.getTemporaryCache(authorityCacheKey);
|
|
}
|
|
/**
|
|
* Updates account, authority, and state in cache
|
|
* @param serverAuthenticationRequest
|
|
* @param account
|
|
*/
|
|
updateCacheEntries(state, nonce, authorityInstance, loginHint, account) {
|
|
this.logger.trace("BrowserCacheManager.updateCacheEntries called");
|
|
// Cache the request state
|
|
const stateCacheKey = this.generateStateKey(state);
|
|
this.setTemporaryCache(stateCacheKey, state, false);
|
|
// Cache the nonce
|
|
const nonceCacheKey = this.generateNonceKey(state);
|
|
this.setTemporaryCache(nonceCacheKey, nonce, false);
|
|
// Cache authorityKey
|
|
const authorityCacheKey = this.generateAuthorityKey(state);
|
|
this.setTemporaryCache(authorityCacheKey, authorityInstance, false);
|
|
if (account) {
|
|
const ccsCredential = {
|
|
credential: account.homeAccountId,
|
|
type: CcsCredentialType.HOME_ACCOUNT_ID,
|
|
};
|
|
this.setTemporaryCache(TemporaryCacheKeys.CCS_CREDENTIAL, JSON.stringify(ccsCredential), true);
|
|
}
|
|
else if (loginHint) {
|
|
const ccsCredential = {
|
|
credential: loginHint,
|
|
type: CcsCredentialType.UPN,
|
|
};
|
|
this.setTemporaryCache(TemporaryCacheKeys.CCS_CREDENTIAL, JSON.stringify(ccsCredential), true);
|
|
}
|
|
}
|
|
/**
|
|
* Reset all temporary cache items
|
|
* @param state
|
|
*/
|
|
resetRequestCache(state) {
|
|
this.logger.trace("BrowserCacheManager.resetRequestCache called");
|
|
// check state and remove associated cache items
|
|
if (state) {
|
|
this.temporaryCacheStorage.getKeys().forEach((key) => {
|
|
if (key.indexOf(state) !== -1) {
|
|
this.removeTemporaryItem(key);
|
|
}
|
|
});
|
|
// delete generic interactive request parameters
|
|
this.removeTemporaryItem(this.generateStateKey(state));
|
|
this.removeTemporaryItem(this.generateNonceKey(state));
|
|
this.removeTemporaryItem(this.generateAuthorityKey(state));
|
|
}
|
|
this.removeTemporaryItem(this.generateCacheKey(TemporaryCacheKeys.REQUEST_PARAMS));
|
|
this.removeTemporaryItem(this.generateCacheKey(TemporaryCacheKeys.ORIGIN_URI));
|
|
this.removeTemporaryItem(this.generateCacheKey(TemporaryCacheKeys.URL_HASH));
|
|
this.removeTemporaryItem(this.generateCacheKey(TemporaryCacheKeys.CORRELATION_ID));
|
|
this.removeTemporaryItem(this.generateCacheKey(TemporaryCacheKeys.CCS_CREDENTIAL));
|
|
this.removeTemporaryItem(this.generateCacheKey(TemporaryCacheKeys.NATIVE_REQUEST));
|
|
this.removeTemporaryItem(this.generateCacheKey(TemporaryCacheKeys.REDIRECT_REQUEST));
|
|
this.setInteractionInProgress(false);
|
|
}
|
|
/**
|
|
* Removes temporary cache for the provided state
|
|
* @param stateString
|
|
*/
|
|
cleanRequestByState(stateString) {
|
|
this.logger.trace("BrowserCacheManager.cleanRequestByState called");
|
|
// Interaction is completed - remove interaction status.
|
|
if (stateString) {
|
|
const stateKey = this.generateStateKey(stateString);
|
|
const cachedState = this.temporaryCacheStorage.getItem(stateKey);
|
|
this.logger.infoPii(`BrowserCacheManager.cleanRequestByState: Removing temporary cache items for state: ${cachedState}`);
|
|
this.resetRequestCache(cachedState || Constants.EMPTY_STRING);
|
|
}
|
|
this.clearMsalCookies();
|
|
}
|
|
/**
|
|
* Looks in temporary cache for any state values with the provided interactionType and removes all temporary cache items for that state
|
|
* Used in scenarios where temp cache needs to be cleaned but state is not known, such as clicking browser back button.
|
|
* @param interactionType
|
|
*/
|
|
cleanRequestByInteractionType(interactionType) {
|
|
this.logger.trace("BrowserCacheManager.cleanRequestByInteractionType called");
|
|
// Loop through all keys to find state key
|
|
this.temporaryCacheStorage.getKeys().forEach((key) => {
|
|
// If this key is not the state key, move on
|
|
if (key.indexOf(TemporaryCacheKeys.REQUEST_STATE) === -1) {
|
|
return;
|
|
}
|
|
// Retrieve state value, return if not a valid value
|
|
const stateValue = this.temporaryCacheStorage.getItem(key);
|
|
if (!stateValue) {
|
|
return;
|
|
}
|
|
// Extract state and ensure it matches given InteractionType, then clean request cache
|
|
const parsedState = extractBrowserRequestState(this.cryptoImpl, stateValue);
|
|
if (parsedState &&
|
|
parsedState.interactionType === interactionType) {
|
|
this.logger.infoPii(`BrowserCacheManager.cleanRequestByInteractionType: Removing temporary cache items for state: ${stateValue}`);
|
|
this.resetRequestCache(stateValue);
|
|
}
|
|
});
|
|
this.clearMsalCookies();
|
|
this.setInteractionInProgress(false);
|
|
}
|
|
/**
|
|
* Create request retry key to cache retry status
|
|
*/
|
|
generateRequestRetriedKey() {
|
|
return `${Constants.CACHE_PREFIX}.${TemporaryCacheKeys.REQUEST_RETRY}.${this.clientId}`;
|
|
}
|
|
/**
|
|
* Gets the request retry value from the cache
|
|
*/
|
|
getRequestRetried() {
|
|
const requestRetriedKey = this.generateRequestRetriedKey();
|
|
const cachedRetryNumber = this.getTemporaryCache(requestRetriedKey);
|
|
if (!cachedRetryNumber) {
|
|
return null;
|
|
}
|
|
return parseInt(cachedRetryNumber);
|
|
}
|
|
/**
|
|
* Sets the request retry value to "retried" in the cache
|
|
*/
|
|
setRequestRetried() {
|
|
this.logger.trace("BrowserCacheManager.setRequestRetried called");
|
|
const requestRetriedKey = this.generateRequestRetriedKey();
|
|
this.setTemporaryCache(requestRetriedKey, "1", false);
|
|
}
|
|
/**
|
|
* Removes all request retry values in the cache
|
|
*/
|
|
removeRequestRetried() {
|
|
const requestRetriedKey = this.generateRequestRetriedKey();
|
|
this.removeTemporaryItem(requestRetriedKey);
|
|
}
|
|
/**
|
|
* Caches the redirectRequest in the cache
|
|
* @param redirectRequest
|
|
*/
|
|
cacheRedirectRequest(redirectRequest) {
|
|
this.logger.trace("BrowserCacheManager.cacheRedirectRequest called");
|
|
const { ...restParams } = redirectRequest;
|
|
delete restParams.onRedirectNavigate;
|
|
const encodedValue = JSON.stringify(restParams);
|
|
this.setTemporaryCache(TemporaryCacheKeys.REDIRECT_REQUEST, encodedValue, true);
|
|
}
|
|
/**
|
|
* Gets redirect request from the cache. Logs an error and returns undefined if nothing is found.
|
|
*/
|
|
getCachedRedirectRequest() {
|
|
this.logger.trace("BrowserCacheManager.getCachedRedirectRequest called");
|
|
const cachedRedirectRequest = this.getTemporaryCache(TemporaryCacheKeys.REDIRECT_REQUEST, true);
|
|
if (!cachedRedirectRequest) {
|
|
this.logger.error(`No cached redirect request found.`);
|
|
}
|
|
else {
|
|
this.removeTemporaryItem(this.generateCacheKey(TemporaryCacheKeys.REDIRECT_REQUEST));
|
|
let parsedRequest;
|
|
try {
|
|
parsedRequest = JSON.parse(cachedRedirectRequest);
|
|
}
|
|
catch (e) {
|
|
this.logger.errorPii(`Attempted to parse: ${cachedRedirectRequest}`);
|
|
this.logger.error(`Parsing cached redirect request threw with error: ${e}`);
|
|
return;
|
|
}
|
|
if (parsedRequest) {
|
|
return parsedRequest;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
cacheCodeRequest(authCodeRequest) {
|
|
this.logger.trace("BrowserCacheManager.cacheCodeRequest called");
|
|
const encodedValue = base64Encode(JSON.stringify(authCodeRequest));
|
|
this.setTemporaryCache(TemporaryCacheKeys.REQUEST_PARAMS, encodedValue, true);
|
|
}
|
|
/**
|
|
* Gets the token exchange parameters from the cache. Throws an error if nothing is found.
|
|
*/
|
|
getCachedRequest(state) {
|
|
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);
|
|
}
|
|
let parsedRequest;
|
|
try {
|
|
parsedRequest = JSON.parse(base64Decode(encodedTokenRequest));
|
|
}
|
|
catch (e) {
|
|
this.logger.errorPii(`Attempted to parse: ${encodedTokenRequest}`);
|
|
this.logger.error(`Parsing cached token request threw with error: ${e}`);
|
|
throw createBrowserAuthError(unableToParseTokenRequestCacheError);
|
|
}
|
|
this.removeTemporaryItem(this.generateCacheKey(TemporaryCacheKeys.REQUEST_PARAMS));
|
|
// Get cached authority and use if no authority is cached with request.
|
|
if (!parsedRequest.authority) {
|
|
const authorityCacheKey = this.generateAuthorityKey(state);
|
|
const cachedAuthority = this.getTemporaryCache(authorityCacheKey);
|
|
if (!cachedAuthority) {
|
|
throw createBrowserAuthError(noCachedAuthorityError);
|
|
}
|
|
parsedRequest.authority = cachedAuthority;
|
|
}
|
|
return parsedRequest;
|
|
}
|
|
/**
|
|
* 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();
|
|
if (matchClientId) {
|
|
return clientId === this.clientId;
|
|
}
|
|
else {
|
|
return !!clientId;
|
|
}
|
|
}
|
|
getInteractionInProgress() {
|
|
const key = `${Constants.CACHE_PREFIX}.${TemporaryCacheKeys.INTERACTION_STATUS_KEY}`;
|
|
return this.getTemporaryCache(key, false);
|
|
}
|
|
setInteractionInProgress(inProgress) {
|
|
// 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(interactionInProgress);
|
|
}
|
|
else {
|
|
// No interaction is in progress
|
|
this.setTemporaryCache(key, this.clientId, false);
|
|
}
|
|
}
|
|
else if (!inProgress &&
|
|
this.getInteractionInProgress() === this.clientId) {
|
|
this.removeTemporaryItem(key);
|
|
}
|
|
}
|
|
/**
|
|
* Returns username retrieved from ADAL or MSAL v1 idToken
|
|
* @deprecated
|
|
*/
|
|
getLegacyLoginHint() {
|
|
// Only check for adal/msal token if no SSO params are being used
|
|
const adalIdTokenString = this.getTemporaryCache(PersistentCacheKeys.ADAL_ID_TOKEN);
|
|
if (adalIdTokenString) {
|
|
this.browserStorage.removeItem(PersistentCacheKeys.ADAL_ID_TOKEN);
|
|
this.logger.verbose("Cached ADAL id token retrieved.");
|
|
}
|
|
// Check for cached MSAL v1 id token
|
|
const msalIdTokenString = this.getTemporaryCache(PersistentCacheKeys.ID_TOKEN, true);
|
|
if (msalIdTokenString) {
|
|
this.browserStorage.removeItem(this.generateCacheKey(PersistentCacheKeys.ID_TOKEN));
|
|
this.logger.verbose("Cached MSAL.js v1 id token retrieved");
|
|
}
|
|
const cachedIdTokenString = msalIdTokenString || adalIdTokenString;
|
|
if (cachedIdTokenString) {
|
|
const idTokenClaims = AuthToken.extractTokenClaims(cachedIdTokenString, base64Decode);
|
|
if (idTokenClaims.preferred_username) {
|
|
this.logger.verbose("No SSO params used and ADAL/MSAL v1 token retrieved, setting ADAL/MSAL v1 preferred_username as loginHint");
|
|
return idTokenClaims.preferred_username;
|
|
}
|
|
else if (idTokenClaims.upn) {
|
|
this.logger.verbose("No SSO params used and ADAL/MSAL v1 token retrieved, setting ADAL/MSAL v1 upn as loginHint");
|
|
return idTokenClaims.upn;
|
|
}
|
|
else {
|
|
this.logger.verbose("No SSO params used and ADAL/MSAL v1 token retrieved, however, no account hint claim found. Enable preferred_username or upn id token claim to get SSO.");
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
/**
|
|
* Updates a credential's cache key if the current cache key is outdated
|
|
*/
|
|
updateCredentialCacheKey(currentCacheKey, credential) {
|
|
const updatedCacheKey = CacheHelpers.generateCredentialKey(credential);
|
|
if (currentCacheKey !== updatedCacheKey) {
|
|
const cacheItem = this.getItem(currentCacheKey);
|
|
if (cacheItem) {
|
|
this.browserStorage.removeItem(currentCacheKey);
|
|
this.setItem(updatedCacheKey, cacheItem);
|
|
this.logger.verbose(`Updated an outdated ${credential.credentialType} cache key`);
|
|
return updatedCacheKey;
|
|
}
|
|
else {
|
|
this.logger.error(`Attempted to update an outdated ${credential.credentialType} cache key but no item matching the outdated key was found in storage`);
|
|
}
|
|
}
|
|
return currentCacheKey;
|
|
}
|
|
/**
|
|
* 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 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(" "), result.expiresOn ? result.expiresOn.getTime() / 1000 : 0, result.extExpiresOn ? result.extExpiresOn.getTime() / 1000 : 0, base64Decode, undefined, // refreshOn
|
|
result.tokenType, undefined, // userAssertionHash
|
|
request.sshKid, request.claims, claimsHash);
|
|
const cacheRecord = {
|
|
idToken: idTokenEntity,
|
|
accessToken: accessTokenEntity,
|
|
};
|
|
return this.saveCacheRecord(cacheRecord);
|
|
}
|
|
/**
|
|
* saves a cache record
|
|
* @param cacheRecord {CacheRecord}
|
|
* @param storeInCache {?StoreInCache}
|
|
* @param correlationId {?string} correlation id
|
|
*/
|
|
async saveCacheRecord(cacheRecord, storeInCache, correlationId) {
|
|
try {
|
|
await super.saveCacheRecord(cacheRecord, storeInCache, correlationId);
|
|
}
|
|
catch (e) {
|
|
if (e instanceof CacheError &&
|
|
this.performanceClient &&
|
|
correlationId) {
|
|
try {
|
|
const tokenKeys = this.getTokenKeys();
|
|
this.performanceClient.addFields({
|
|
cacheRtCount: tokenKeys.refreshToken.length,
|
|
cacheIdCount: tokenKeys.idToken.length,
|
|
cacheAtCount: tokenKeys.accessToken.length,
|
|
}, correlationId);
|
|
}
|
|
catch (e) { }
|
|
}
|
|
throw e;
|
|
}
|
|
}
|
|
}
|
|
const DEFAULT_BROWSER_CACHE_MANAGER = (clientId, logger) => {
|
|
const cacheOptions = {
|
|
cacheLocation: BrowserCacheLocation.MemoryStorage,
|
|
temporaryCacheLocation: BrowserCacheLocation.MemoryStorage,
|
|
storeAuthStateInCookie: false,
|
|
secureCookies: false,
|
|
cacheMigrationEnabled: false,
|
|
claimsBasedCachingEnabled: false,
|
|
};
|
|
return new BrowserCacheManager(clientId, cacheOptions, DEFAULT_CRYPTO_IMPLEMENTATION, logger);
|
|
};
|
|
|
|
export { BrowserCacheManager, DEFAULT_BROWSER_CACHE_MANAGER };
|
|
//# sourceMappingURL=BrowserCacheManager.mjs.map
|