@azure/msal-browser
Version:
Microsoft Authentication Library for js
1,439 lines (1,352 loc) • 64.5 kB
text/typescript
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
AccessTokenEntity,
AccountEntity,
AccountInfo,
ActiveAccountFilters,
AppMetadataEntity,
AuthenticationScheme,
AuthorityMetadataEntity,
CacheError,
CacheErrorCodes,
CacheHelpers,
CacheManager,
CacheRecord,
CommonAuthorizationUrlRequest,
Constants,
createCacheError,
DEFAULT_CRYPTO_IMPLEMENTATION,
ICrypto,
IdTokenEntity,
invokeAsync,
IPerformanceClient,
Logger,
PerformanceEvents,
PersistentCacheKeys,
RefreshTokenEntity,
ServerTelemetryEntity,
StaticAuthorityOptions,
StoreInCache,
StringUtils,
ThrottlingEntity,
TimeUtils,
TokenKeys,
CredentialEntity,
CredentialType,
DEFAULT_TOKEN_RENEWAL_OFFSET_SEC,
} from "@azure/msal-common/browser";
import { CacheOptions } from "../config/Configuration.js";
import {
BrowserAuthErrorCodes,
createBrowserAuthError,
} from "../error/BrowserAuthError.js";
import {
BrowserCacheLocation,
InMemoryCacheKeys,
INTERACTION_TYPE,
TemporaryCacheKeys,
} from "../utils/BrowserConstants.js";
import * as CacheKeys from "./CacheKeys.js";
import { LocalStorage } from "./LocalStorage.js";
import { SessionStorage } from "./SessionStorage.js";
import { MemoryStorage } from "./MemoryStorage.js";
import { IWindowStorage } from "./IWindowStorage.js";
import { PlatformAuthRequest } from "../broker/nativeBroker/PlatformAuthRequest.js";
import { AuthenticationResult } from "../response/AuthenticationResult.js";
import { SilentRequest } from "../request/SilentRequest.js";
import { SsoSilentRequest } from "../request/SsoSilentRequest.js";
import { RedirectRequest } from "../request/RedirectRequest.js";
import { PopupRequest } from "../request/PopupRequest.js";
import { base64Decode } from "../encode/Base64Decode.js";
import { base64Encode } from "../encode/Base64Encode.js";
import { CookieStorage } from "./CookieStorage.js";
import { getAccountKeys, getTokenKeys } from "./CacheHelpers.js";
import { EventType } from "../event/EventType.js";
import { EventHandler } from "../event/EventHandler.js";
import { clearHash } from "../utils/BrowserUtils.js";
import { version } from "../packageMetadata.js";
import { removeElementFromArray } from "../utils/Helpers.js";
import { EncryptedData, isEncrypted } from "./EncryptedData.js";
/**
* This class implements the cache storage interface for MSAL through browser local or session storage.
* Cookies are only used if storeAuthStateInCookie is true, and are only used for
* parameters such as state and nonce, generally.
*/
export class BrowserCacheManager extends CacheManager {
// Cache configuration, either set by user or default values.
protected cacheConfig: Required<CacheOptions>;
// Window storage object (either local or sessionStorage)
protected browserStorage: IWindowStorage<string>;
// Internal in-memory storage object used for data used by msal that does not need to persist across page loads
protected internalStorage: MemoryStorage<string>;
// Temporary cache
protected temporaryCacheStorage: IWindowStorage<string>;
// Cookie storage
protected cookieStorage: CookieStorage;
// Logger instance
protected logger: Logger;
// Event Handler
private eventHandler: EventHandler;
constructor(
clientId: string,
cacheConfig: Required<CacheOptions>,
cryptoImpl: ICrypto,
logger: Logger,
performanceClient: IPerformanceClient,
eventHandler: EventHandler,
staticAuthorityOptions?: StaticAuthorityOptions
) {
super(
clientId,
cryptoImpl,
logger,
performanceClient,
staticAuthorityOptions
);
this.cacheConfig = cacheConfig;
this.logger = logger;
this.internalStorage = new MemoryStorage();
this.browserStorage = getStorageImplementation(
clientId,
cacheConfig.cacheLocation,
logger,
performanceClient
);
this.temporaryCacheStorage = getStorageImplementation(
clientId,
cacheConfig.temporaryCacheLocation,
logger,
performanceClient
);
this.cookieStorage = new CookieStorage();
this.eventHandler = eventHandler;
}
async initialize(correlationId: string): Promise<void> {
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: string): Promise<void> {
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(
CacheKeys.ACCOUNT_SCHEMA_VERSION,
accountKeys0,
accountKeys1,
correlationId
),
this.updateV0ToCurrent(
CacheKeys.CREDENTIAL_SCHEMA_VERSION,
tokenKeys0.idToken,
tokenKeys1.idToken,
correlationId
),
this.updateV0ToCurrent(
CacheKeys.CREDENTIAL_SCHEMA_VERSION,
tokenKeys0.accessToken,
tokenKeys1.accessToken,
correlationId
),
this.updateV0ToCurrent(
CacheKeys.CREDENTIAL_SCHEMA_VERSION,
tokenKeys0.refreshToken,
tokenKeys1.refreshToken,
correlationId
),
]);
if (accountKeys0.length > 0) {
this.browserStorage.setItem(
CacheKeys.getAccountKeysCacheKey(0),
JSON.stringify(accountKeys0)
);
} else {
this.browserStorage.removeItem(CacheKeys.getAccountKeysCacheKey(0));
}
if (accountKeys1.length > 0) {
this.browserStorage.setItem(
CacheKeys.getAccountKeysCacheKey(1),
JSON.stringify(accountKeys1)
);
} else {
this.browserStorage.removeItem(CacheKeys.getAccountKeysCacheKey(1));
}
this.setTokenKeys(tokenKeys0, correlationId, 0);
this.setTokenKeys(tokenKeys1, correlationId, 1);
}
async updateV0ToCurrent(
currentSchema: number,
v0Keys: Array<string>,
v1Keys: Array<string>,
correlationId: string
): Promise<void[]> {
const upgradePromises: Array<Promise<void>> = [];
for (const v0Key of [...v0Keys]) {
const rawV0Value = this.browserStorage.getItem(v0Key);
const parsedV0Value = this.validateAndParseJson(
rawV0Value || ""
) as CredentialEntity | AccountEntity | EncryptedData | null;
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 = `${CacheKeys.PREFIX}.${currentSchema}${CacheKeys.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
) as CredentialEntity | AccountEntity | EncryptedData;
// 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
*/
private trackVersionChanges(correlationId: string): void {
const previousVersion = this.browserStorage.getItem(
CacheKeys.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(CacheKeys.VERSION_CACHE_KEY, version, correlationId);
}
}
/**
* Parses passed value as JSON object, JSON.parse() will throw an error.
* @param input
*/
protected validateAndParseJson(jsonValue: string): object | null {
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: string, value: string, correlationId: string): void {
let tokenKeysV0Count = 0;
let accessTokenKeys: Array<string> = [];
const maxRetries = 20;
for (let i = 0; i <= maxRetries; i++) {
try {
this.browserStorage.setItem(key, value);
if (i > 0) {
// Finally update the token keys array with the tokens removed
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 ===
CacheKeys.getTokenKeysCacheKey(this.clientId, 0)
? (JSON.parse(value) as TokenKeys).accessToken
: this.getTokenKeys(0).accessToken;
const tokenKeys1 =
key ===
CacheKeys.getTokenKeysCacheKey(this.clientId)
? (JSON.parse(value) as TokenKeys).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: string,
value: string,
correlationId: string,
timestamp: string
): Promise<void> {
let tokenKeysV0Count = 0;
let accessTokenKeys: Array<string> = [];
const maxRetries = 20;
for (let i = 0; i <= maxRetries; i++) {
try {
await invokeAsync(
this.browserStorage.setUserData.bind(this.browserStorage),
PerformanceEvents.SetUserData,
this.logger,
this.performanceClient
)(key, value, correlationId, timestamp);
if (i > 0) {
// Finally update the token keys array with the tokens removed
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: string,
correlationId: string
): AccountEntity | null {
this.logger.trace("BrowserCacheManager.getAccount called");
const serializedAccount = this.browserStorage.getUserData(accountKey);
if (!serializedAccount) {
this.removeAccountKeyFromMap(accountKey, correlationId);
return null;
}
const parsedAccount = this.validateAndParseJson(serializedAccount);
if (!parsedAccount || !AccountEntity.isAccountEntity(parsedAccount)) {
return null;
}
return CacheManager.toObject<AccountEntity>(
new AccountEntity(),
parsedAccount
);
}
/**
* set account entity in the platform cache
* @param account
*/
async setAccount(
account: AccountEntity,
correlationId: string
): Promise<void> {
this.logger.trace("BrowserCacheManager.setAccount called");
const key = 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(): Array<string> {
return getAccountKeys(this.browserStorage);
}
/**
* Add a new account to the key map
* @param key
*/
addAccountKeyToMap(key: string, correlationId: string): boolean {
this.logger.trace("BrowserCacheManager.addAccountKeyToMap called");
this.logger.tracePii(
`BrowserCacheManager.addAccountKeyToMap called with key: ${key}`
);
const accountKeys = this.getAccountKeys();
if (accountKeys.indexOf(key) === -1) {
// Only add key if it does not already exist in the map
accountKeys.push(key);
this.setItem(
CacheKeys.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: string, correlationId: string): void {
this.logger.trace("BrowserCacheManager.removeAccountKeyFromMap called");
this.logger.tracePii(
`BrowserCacheManager.removeAccountKeyFromMap called with key: ${key}`
);
const accountKeys = this.getAccountKeys();
const removalIndex = accountKeys.indexOf(key);
if (removalIndex > -1) {
accountKeys.splice(removalIndex, 1);
if (accountKeys.length === 0) {
// If no keys left, remove the map
this.removeItem(CacheKeys.getAccountKeysCacheKey());
return;
} else {
this.setItem(
CacheKeys.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: AccountInfo, correlationId: string): void {
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: string, correlationId: string): void {
super.removeIdToken(key, correlationId);
const tokenKeys = this.getTokenKeys();
const idRemoval = tokenKeys.idToken.indexOf(key);
if (idRemoval > -1) {
this.logger.info("idToken removed from tokenKeys map");
tokenKeys.idToken.splice(idRemoval, 1);
this.setTokenKeys(tokenKeys, correlationId);
}
}
/**
* Removes given accessToken from the cache and from the key map
* @param key
*/
removeAccessToken(
key: string,
correlationId: string,
updateTokenKeys: boolean = true
): void {
super.removeAccessToken(key, correlationId);
updateTokenKeys && this.removeAccessTokenKeys([key], correlationId);
}
/**
* Remove access token key from the key map
* @param key
* @param correlationId
* @param tokenKeys
*/
removeAccessTokenKeys(
keys: Array<string>,
correlationId: string,
schemaVersion: number = CacheKeys.CREDENTIAL_SCHEMA_VERSION
): void {
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: string, correlationId: string): void {
super.removeRefreshToken(key, correlationId);
const tokenKeys = this.getTokenKeys();
const refreshRemoval = tokenKeys.refreshToken.indexOf(key);
if (refreshRemoval > -1) {
this.logger.info("refreshToken removed from tokenKeys map");
tokenKeys.refreshToken.splice(refreshRemoval, 1);
this.setTokenKeys(tokenKeys, correlationId);
}
}
/**
* Gets the keys for the cached tokens associated with this clientId
* @returns
*/
getTokenKeys(
schemaVersion: number = CacheKeys.CREDENTIAL_SCHEMA_VERSION
): TokenKeys {
return getTokenKeys(this.clientId, this.browserStorage, schemaVersion);
}
/**
* Stores the token keys in the cache
* @param tokenKeys
* @param correlationId
* @returns
*/
setTokenKeys(
tokenKeys: TokenKeys,
correlationId: string,
schemaVersion: number = CacheKeys.CREDENTIAL_SCHEMA_VERSION
): void {
if (
tokenKeys.idToken.length === 0 &&
tokenKeys.accessToken.length === 0 &&
tokenKeys.refreshToken.length === 0
) {
// If no keys left, remove the map
this.removeItem(
CacheKeys.getTokenKeysCacheKey(this.clientId, schemaVersion)
);
return;
} else {
this.setItem(
CacheKeys.getTokenKeysCacheKey(this.clientId, schemaVersion),
JSON.stringify(tokenKeys),
correlationId
);
}
}
/**
* generates idToken entity from a string
* @param idTokenKey
*/
getIdTokenCredential(
idTokenKey: string,
correlationId: string
): IdTokenEntity | null {
const value = this.browserStorage.getUserData(idTokenKey);
if (!value) {
this.logger.trace(
"BrowserCacheManager.getIdTokenCredential: called, no cache hit"
);
this.removeIdToken(idTokenKey, correlationId);
return null;
}
const parsedIdToken = this.validateAndParseJson(value);
if (!parsedIdToken || !CacheHelpers.isIdTokenEntity(parsedIdToken)) {
this.logger.trace(
"BrowserCacheManager.getIdTokenCredential: called, no cache hit"
);
return null;
}
this.logger.trace(
"BrowserCacheManager.getIdTokenCredential: cache hit"
);
return parsedIdToken as IdTokenEntity;
}
/**
* set IdToken credential to the platform cache
* @param idToken
*/
async setIdTokenCredential(
idToken: IdTokenEntity,
correlationId: string
): Promise<void> {
this.logger.trace("BrowserCacheManager.setIdTokenCredential called");
const idTokenKey = 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: string,
correlationId: string
): AccessTokenEntity | null {
const value = this.browserStorage.getUserData(accessTokenKey);
if (!value) {
this.logger.trace(
"BrowserCacheManager.getAccessTokenCredential: called, no cache hit"
);
this.removeAccessTokenKeys([accessTokenKey], correlationId);
return null;
}
const parsedAccessToken = this.validateAndParseJson(value);
if (
!parsedAccessToken ||
!CacheHelpers.isAccessTokenEntity(parsedAccessToken)
) {
this.logger.trace(
"BrowserCacheManager.getAccessTokenCredential: called, no cache hit"
);
return null;
}
this.logger.trace(
"BrowserCacheManager.getAccessTokenCredential: cache hit"
);
return parsedAccessToken as AccessTokenEntity;
}
/**
* set accessToken credential to the platform cache
* @param accessToken
*/
async setAccessTokenCredential(
accessToken: AccessTokenEntity,
correlationId: string
): Promise<void> {
this.logger.trace(
"BrowserCacheManager.setAccessTokenCredential called"
);
const accessTokenKey = 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: string,
correlationId: string
): RefreshTokenEntity | null {
const value = this.browserStorage.getUserData(refreshTokenKey);
if (!value) {
this.logger.trace(
"BrowserCacheManager.getRefreshTokenCredential: called, no cache hit"
);
this.removeRefreshToken(refreshTokenKey, correlationId);
return null;
}
const parsedRefreshToken = this.validateAndParseJson(value);
if (
!parsedRefreshToken ||
!CacheHelpers.isRefreshTokenEntity(parsedRefreshToken)
) {
this.logger.trace(
"BrowserCacheManager.getRefreshTokenCredential: called, no cache hit"
);
return null;
}
this.logger.trace(
"BrowserCacheManager.getRefreshTokenCredential: cache hit"
);
return parsedRefreshToken as RefreshTokenEntity;
}
/**
* set refreshToken credential to the platform cache
* @param refreshToken
*/
async setRefreshTokenCredential(
refreshToken: RefreshTokenEntity,
correlationId: string
): Promise<void> {
this.logger.trace(
"BrowserCacheManager.setRefreshTokenCredential called"
);
const refreshTokenKey = 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: string): AppMetadataEntity | null {
const value = this.browserStorage.getItem(appMetadataKey);
if (!value) {
this.logger.trace(
"BrowserCacheManager.getAppMetadata: called, no cache hit"
);
return null;
}
const parsedMetadata = this.validateAndParseJson(value);
if (
!parsedMetadata ||
!CacheHelpers.isAppMetadataEntity(appMetadataKey, parsedMetadata)
) {
this.logger.trace(
"BrowserCacheManager.getAppMetadata: called, no cache hit"
);
return null;
}
this.logger.trace("BrowserCacheManager.getAppMetadata: cache hit");
return parsedMetadata as AppMetadataEntity;
}
/**
* set appMetadata entity to the platform cache
* @param appMetadata
*/
setAppMetadata(
appMetadata: AppMetadataEntity,
correlationId: string
): void {
this.logger.trace("BrowserCacheManager.setAppMetadata called");
const appMetadataKey = CacheHelpers.generateAppMetadataKey(appMetadata);
this.setItem(
appMetadataKey,
JSON.stringify(appMetadata),
correlationId
);
}
/**
* fetch server telemetry entity from the platform cache
* @param serverTelemetryKey
*/
getServerTelemetry(
serverTelemetryKey: string
): ServerTelemetryEntity | null {
const value = this.browserStorage.getItem(serverTelemetryKey);
if (!value) {
this.logger.trace(
"BrowserCacheManager.getServerTelemetry: called, no cache hit"
);
return null;
}
const parsedEntity = this.validateAndParseJson(value);
if (
!parsedEntity ||
!CacheHelpers.isServerTelemetryEntity(
serverTelemetryKey,
parsedEntity
)
) {
this.logger.trace(
"BrowserCacheManager.getServerTelemetry: called, no cache hit"
);
return null;
}
this.logger.trace("BrowserCacheManager.getServerTelemetry: cache hit");
return parsedEntity as ServerTelemetryEntity;
}
/**
* set server telemetry entity to the platform cache
* @param serverTelemetryKey
* @param serverTelemetry
*/
setServerTelemetry(
serverTelemetryKey: string,
serverTelemetry: ServerTelemetryEntity,
correlationId: string
): void {
this.logger.trace("BrowserCacheManager.setServerTelemetry called");
this.setItem(
serverTelemetryKey,
JSON.stringify(serverTelemetry),
correlationId
);
}
/**
*
*/
getAuthorityMetadata(key: string): AuthorityMetadataEntity | null {
const value = this.internalStorage.getItem(key);
if (!value) {
this.logger.trace(
"BrowserCacheManager.getAuthorityMetadata: called, no cache hit"
);
return null;
}
const parsedMetadata = this.validateAndParseJson(value);
if (
parsedMetadata &&
CacheHelpers.isAuthorityMetadataEntity(key, parsedMetadata)
) {
this.logger.trace(
"BrowserCacheManager.getAuthorityMetadata: cache hit"
);
return parsedMetadata as AuthorityMetadataEntity;
}
return null;
}
/**
*
*/
getAuthorityMetadataKeys(): Array<string> {
const allKeys = this.internalStorage.getKeys();
return allKeys.filter((key) => {
return this.isAuthorityMetadata(key);
});
}
/**
* Sets wrapper metadata in memory
* @param wrapperSKU
* @param wrapperVersion
*/
setWrapperMetadata(wrapperSKU: string, wrapperVersion: string): void {
this.internalStorage.setItem(InMemoryCacheKeys.WRAPPER_SKU, wrapperSKU);
this.internalStorage.setItem(
InMemoryCacheKeys.WRAPPER_VER,
wrapperVersion
);
}
/**
* Returns wrapper metadata from in-memory storage
*/
getWrapperMetadata(): [string, string] {
const sku =
this.internalStorage.getItem(InMemoryCacheKeys.WRAPPER_SKU) ||
Constants.EMPTY_STRING;
const version =
this.internalStorage.getItem(InMemoryCacheKeys.WRAPPER_VER) ||
Constants.EMPTY_STRING;
return [sku, version];
}
/**
*
* @param entity
*/
setAuthorityMetadata(key: string, entity: AuthorityMetadataEntity): void {
this.logger.trace("BrowserCacheManager.setAuthorityMetadata called");
this.internalStorage.setItem(key, JSON.stringify(entity));
}
/**
* Gets the active account
*/
getActiveAccount(correlationId: string): AccountInfo | null {
const activeAccountKeyFilters = this.generateCacheKey(
PersistentCacheKeys.ACTIVE_ACCOUNT_FILTERS
);
const activeAccountValueFilters = this.browserStorage.getItem(
activeAccountKeyFilters
);
if (!activeAccountValueFilters) {
this.logger.trace(
"BrowserCacheManager.getActiveAccount: No active account filters found"
);
return null;
}
const activeAccountValueObj = this.validateAndParseJson(
activeAccountValueFilters
) as AccountInfo;
if (activeAccountValueObj) {
this.logger.trace(
"BrowserCacheManager.getActiveAccount: Active account filters schema found"
);
return this.getAccountInfoFilteredBy(
{
homeAccountId: activeAccountValueObj.homeAccountId,
localAccountId: activeAccountValueObj.localAccountId,
tenantId: activeAccountValueObj.tenantId,
},
correlationId
);
}
this.logger.trace(
"BrowserCacheManager.getActiveAccount: No active account found"
);
return null;
}
/**
* Sets the active account's localAccountId in cache
* @param account
*/
setActiveAccount(account: AccountInfo | null, correlationId: string): void {
const activeAccountKey = this.generateCacheKey(
PersistentCacheKeys.ACTIVE_ACCOUNT_FILTERS
);
if (account) {
this.logger.verbose("setActiveAccount: Active account set");
const activeAccountValue: ActiveAccountFilters = {
homeAccountId: account.homeAccountId,
localAccountId: account.localAccountId,
tenantId: account.tenantId,
lastUpdatedAt: TimeUtils.nowSeconds().toString(),
};
this.setItem(
activeAccountKey,
JSON.stringify(activeAccountValue),
correlationId
);
} else {
this.logger.verbose(
"setActiveAccount: No account passed, active account not set"
);
this.browserStorage.removeItem(activeAccountKey);
}
this.eventHandler.emitEvent(EventType.ACTIVE_ACCOUNT_CHANGED);
}
/**
* fetch throttling entity from the platform cache
* @param throttlingCacheKey
*/
getThrottlingCache(throttlingCacheKey: string): ThrottlingEntity | null {
const value = this.browserStorage.getItem(throttlingCacheKey);
if (!value) {
this.logger.trace(
"BrowserCacheManager.getThrottlingCache: called, no cache hit"
);
return null;
}
const parsedThrottlingCache = this.validateAndParseJson(value);
if (
!parsedThrottlingCache ||
!CacheHelpers.isThrottlingEntity(
throttlingCacheKey,
parsedThrottlingCache
)
) {
this.logger.trace(
"BrowserCacheManager.getThrottlingCache: called, no cache hit"
);
return null;
}
this.logger.trace("BrowserCacheManager.getThrottlingCache: cache hit");
return parsedThrottlingCache as ThrottlingEntity;
}
/**
* set throttling entity to the platform cache
* @param throttlingCacheKey
* @param throttlingCache
*/
setThrottlingCache(
throttlingCacheKey: string,
throttlingCache: ThrottlingEntity,
correlationId: string
): void {
this.logger.trace("BrowserCacheManager.setThrottlingCache called");
this.setItem(
throttlingCacheKey,
JSON.stringify(throttlingCache),
correlationId
);
}
/**
* Gets cache item with given key.
* Will retrieve from cookies if storeAuthStateInCookie is set to true.
* @param key
*/
getTemporaryCache(cacheKey: string, generateKey?: boolean): string | null {
const key = generateKey ? this.generateCacheKey(cacheKey) : cacheKey;
if (this.cacheConfig.storeAuthStateInCookie) {
const itemCookie = this.cookieStorage.getItem(key);
if (itemCookie) {
this.logger.trace(
"BrowserCacheManager.getTemporaryCache: storeAuthStateInCookies set to true, retrieving from cookies"
);
return itemCookie;
}
}
const value = this.temporaryCacheStorage.getItem(key);
if (!value) {
// If temp cache item not found in session/memory, check local storage for items set by old versions
if (
this.cacheConfig.cacheLocation ===
BrowserCacheLocation.LocalStorage
) {
const item = this.browserStorage.getItem(key);
if (item) {
this.logger.trace(
"BrowserCacheManager.getTemporaryCache: Temporary cache item found in local storage"
);
return item;
}
}
this.logger.trace(
"BrowserCacheManager.getTemporaryCache: No cache item found in local storage"
);
return null;
}
this.logger.trace(
"BrowserCacheManager.getTemporaryCache: Temporary cache item returned"
);
return value;
}
/**
* Sets the cache item with the key and value given.
* Stores in cookie if storeAuthStateInCookie is set to true.
* This can cause cookie overflow if used incorrectly.
* @param key
* @param value
*/
setTemporaryCache(
cacheKey: string,
value: string,
generateKey?: boolean
): void {
const key = generateKey ? this.generateCacheKey(cacheKey) : cacheKey;
this.temporaryCacheStorage.setItem(key, value);
if (this.cacheConfig.storeAuthStateInCookie) {
this.logger.trace(
"BrowserCacheManager.setTemporaryCache: storeAuthStateInCookie set to true, setting item cookie"
);
this.cookieStorage.setItem(
key,
value,
undefined,
this.cacheConfig.secureCookies
);
}
}
/**
* Removes the cache item with the given key.
* @param key
*/
removeItem(key: string): void {
this.browserStorage.removeItem(key);
}
/**
* Removes the temporary cache item with the given key.
* Will also clear the cookie item if storeAuthStateInCookie is set to true.
* @param key
*/
removeTemporaryItem(key: string): void {
this.temporaryCacheStorage.removeItem(key);
if (this.cacheConfig.storeAuthStateInCookie) {
this.logger.trace(
"BrowserCacheManager.removeItem: storeAuthStateInCookie is true, clearing item cookie"
);
this.cookieStorage.removeItem(key);
}
}
/**
* Gets all keys in window.
*/
getKeys(): string[] {
return this.browserStorage.getKeys();
}
/**
* Clears all cache entries created by MSAL.
*/
clear(correlationId: string): void {
// Removes all accounts and their credentials
this.removeAllAccounts(correlationId);
this.removeAppMetadata(correlationId);
// Remove temp storage first to make sure any cookies are cleared
this.temporaryCacheStorage.getKeys().forEach((cacheKey: string) => {
if (
cacheKey.indexOf(CacheKeys.PREFIX) !== -1 ||
cacheKey.indexOf(this.clientId) !== -1
) {
this.removeTemporaryItem(cacheKey);
}
});
// Removes all remaining MSAL cache items
this.browserStorage.getKeys().forEach((cacheKey: string) => {
if (
cacheKey.indexOf(CacheKeys.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