1868 lines
63 KiB
TypeScript
1868 lines
63 KiB
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 {IPerformanceClient}
|
||
|
|
* @param correlationId {string} correlation id
|
||
|
|
* @returns
|
||
|
|
*/
|
||
|
|
clearTokensAndKeysWithClaims(correlationId: string): void {
|
||
|
|
this.performanceClient.addQueueMeasurement(
|
||
|
|
PerformanceEvents.ClearTokensAndKeysWithClaims,
|
||
|
|
correlationId
|
||
|
|
);
|
||
|
|
|
||
|
|
const tokenKeys = this.getTokenKeys();
|
||
|
|
let removedAccessTokens = 0;
|
||
|
|
tokenKeys.accessToken.forEach((key: string) => {
|
||
|
|
// if the access token has claims in its key, remove the token key and the token
|
||
|
|
const credential = this.getAccessTokenCredential(
|
||
|
|
key,
|
||
|
|
correlationId
|
||
|
|
);
|
||
|
|
if (
|
||
|
|
credential?.requestedClaimsHash &&
|
||
|
|
key.includes(credential.requestedClaimsHash.toLowerCase())
|
||
|
|
) {
|
||
|
|
this.removeAccessToken(key, correlationId);
|
||
|
|
removedAccessTokens++;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// warn if any access tokens are removed
|
||
|
|
if (removedAccessTokens > 0) {
|
||
|
|
this.logger.warning(
|
||
|
|
`${removedAccessTokens} access tokens with claims in the cache keys have been removed from the cache.`
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Prepend msal.<client-id> to each key
|
||
|
|
* @param key
|
||
|
|
* @param addInstanceId
|
||
|
|
*/
|
||
|
|
generateCacheKey(key: string): string {
|
||
|
|
if (StringUtils.startsWith(key, CacheKeys.PREFIX)) {
|
||
|
|
return key;
|
||
|
|
}
|
||
|
|
return `${CacheKeys.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: CredentialEntity): string {
|
||
|
|
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 = [
|
||
|
|
`${CacheKeys.PREFIX}.${CacheKeys.CREDENTIAL_SCHEMA_VERSION}`,
|
||
|
|
credential.homeAccountId,
|
||
|
|
credential.environment,
|
||
|
|
credential.credentialType,
|
||
|
|
familyId,
|
||
|
|
credential.realm || "",
|
||
|
|
credential.target || "",
|
||
|
|
credential.requestedClaimsHash || "",
|
||
|
|
scheme,
|
||
|
|
];
|
||
|
|
|
||
|
|
return credentialKey.join(CacheKeys.CACHE_KEY_SEPARATOR).toLowerCase();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Cache Key: msal.<schema_version>.<home_account_id>.<environment>.<tenant_id>
|
||
|
|
* @param account
|
||
|
|
* @returns
|
||
|
|
*/
|
||
|
|
generateAccountKey(account: AccountInfo): string {
|
||
|
|
const homeTenantId = account.homeAccountId.split(".")[1];
|
||
|
|
const accountKey = [
|
||
|
|
`${CacheKeys.PREFIX}.${CacheKeys.ACCOUNT_SCHEMA_VERSION}`,
|
||
|
|
account.homeAccountId,
|
||
|
|
account.environment,
|
||
|
|
homeTenantId || account.tenantId || "",
|
||
|
|
];
|
||
|
|
|
||
|
|
return accountKey.join(CacheKeys.CACHE_KEY_SEPARATOR).toLowerCase();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Reset all temporary cache items
|
||
|
|
* @param state
|
||
|
|
*/
|
||
|
|
resetRequestCache(): void {
|
||
|
|
this.logger.trace("BrowserCacheManager.resetRequestCache called");
|
||
|
|
|
||
|
|
this.removeTemporaryItem(
|
||
|
|
this.generateCacheKey(TemporaryCacheKeys.REQUEST_PARAMS)
|
||
|
|
);
|
||
|
|
this.removeTemporaryItem(
|
||
|
|
this.generateCacheKey(TemporaryCacheKeys.VERIFIER)
|
||
|
|
);
|
||
|
|
this.removeTemporaryItem(
|
||
|
|
this.generateCacheKey(TemporaryCacheKeys.ORIGIN_URI)
|
||
|
|
);
|
||
|
|
this.removeTemporaryItem(
|
||
|
|
this.generateCacheKey(TemporaryCacheKeys.URL_HASH)
|
||
|
|
);
|
||
|
|
this.removeTemporaryItem(
|
||
|
|
this.generateCacheKey(TemporaryCacheKeys.NATIVE_REQUEST)
|
||
|
|
);
|
||
|
|
this.setInteractionInProgress(false);
|
||
|
|
}
|
||
|
|
|
||
|
|
cacheAuthorizeRequest(
|
||
|
|
authCodeRequest: CommonAuthorizationUrlRequest,
|
||
|
|
codeVerifier?: string
|
||
|
|
): void {
|
||
|
|
this.logger.trace("BrowserCacheManager.cacheAuthorizeRequest called");
|
||
|
|
|
||
|
|
const encodedValue = base64Encode(JSON.stringify(authCodeRequest));
|
||
|
|
this.setTemporaryCache(
|
||
|
|
TemporaryCacheKeys.REQUEST_PARAMS,
|
||
|
|
encodedValue,
|
||
|
|
true
|
||
|
|
);
|
||
|
|
|
||
|
|
if (codeVerifier) {
|
||
|
|
const encodedVerifier = base64Encode(codeVerifier);
|
||
|
|
this.setTemporaryCache(
|
||
|
|
TemporaryCacheKeys.VERIFIER,
|
||
|
|
encodedVerifier,
|
||
|
|
true
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Gets the token exchange parameters from the cache. Throws an error if nothing is found.
|
||
|
|
*/
|
||
|
|
getCachedRequest(): [CommonAuthorizationUrlRequest, string] {
|
||
|
|
this.logger.trace("BrowserCacheManager.getCachedRequest called");
|
||
|
|
// Get token request from cache and parse as TokenExchangeParameters.
|
||
|
|
const encodedTokenRequest = this.getTemporaryCache(
|
||
|
|
TemporaryCacheKeys.REQUEST_PARAMS,
|
||
|
|
true
|
||
|
|
);
|
||
|
|
if (!encodedTokenRequest) {
|
||
|
|
throw createBrowserAuthError(
|
||
|
|
BrowserAuthErrorCodes.noTokenRequestCacheError
|
||
|
|
);
|
||
|
|
}
|
||
|
|
const encodedVerifier = this.getTemporaryCache(
|
||
|
|
TemporaryCacheKeys.VERIFIER,
|
||
|
|
true
|
||
|
|
);
|
||
|
|
|
||
|
|
let parsedRequest: CommonAuthorizationUrlRequest;
|
||
|
|
let verifier = "";
|
||
|
|
try {
|
||
|
|
parsedRequest = JSON.parse(base64Decode(encodedTokenRequest));
|
||
|
|
if (encodedVerifier) {
|
||
|
|
verifier = base64Decode(encodedVerifier);
|
||
|
|
}
|
||
|
|
} catch (e) {
|
||
|
|
this.logger.errorPii(`Attempted to parse: ${encodedTokenRequest}`);
|
||
|
|
this.logger.error(
|
||
|
|
`Parsing cached token request threw with error: ${e}`
|
||
|
|
);
|
||
|
|
throw createBrowserAuthError(
|
||
|
|
BrowserAuthErrorCodes.unableToParseTokenRequestCacheError
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
return [parsedRequest, verifier];
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Gets cached native request for redirect flows
|
||
|
|
*/
|
||
|
|
getCachedNativeRequest(): PlatformAuthRequest | null {
|
||
|
|
this.logger.trace("BrowserCacheManager.getCachedNativeRequest called");
|
||
|
|
const cachedRequest = this.getTemporaryCache(
|
||
|
|
TemporaryCacheKeys.NATIVE_REQUEST,
|
||
|
|
true
|
||
|
|
);
|
||
|
|
if (!cachedRequest) {
|
||
|
|
this.logger.trace(
|
||
|
|
"BrowserCacheManager.getCachedNativeRequest: No cached native request found"
|
||
|
|
);
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
const parsedRequest = this.validateAndParseJson(
|
||
|
|
cachedRequest
|
||
|
|
) as PlatformAuthRequest;
|
||
|
|
if (!parsedRequest) {
|
||
|
|
this.logger.error(
|
||
|
|
"BrowserCacheManager.getCachedNativeRequest: Unable to parse native request"
|
||
|
|
);
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
return parsedRequest;
|
||
|
|
}
|
||
|
|
|
||
|
|
isInteractionInProgress(matchClientId?: boolean): boolean {
|
||
|
|
const clientId = this.getInteractionInProgress()?.clientId;
|
||
|
|
|
||
|
|
if (matchClientId) {
|
||
|
|
return clientId === this.clientId;
|
||
|
|
} else {
|
||
|
|
return !!clientId;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
getInteractionInProgress(): {
|
||
|
|
clientId: string;
|
||
|
|
type: INTERACTION_TYPE;
|
||
|
|
} | null {
|
||
|
|
const key = `${CacheKeys.PREFIX}.${TemporaryCacheKeys.INTERACTION_STATUS_KEY}`;
|
||
|
|
const value = this.getTemporaryCache(key, false);
|
||
|
|
try {
|
||
|
|
return value ? JSON.parse(value) : null;
|
||
|
|
} catch (e) {
|
||
|
|
// Remove interaction and other temp keys if interaction status can't be parsed
|
||
|
|
this.logger.error(
|
||
|
|
`Cannot parse interaction status. Removing temporary cache items and clearing url hash. Retrying interaction should fix the error`
|
||
|
|
);
|
||
|
|
this.removeTemporaryItem(key);
|
||
|
|
this.resetRequestCache();
|
||
|
|
clearHash(window);
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
setInteractionInProgress(
|
||
|
|
inProgress: boolean,
|
||
|
|
type: INTERACTION_TYPE = INTERACTION_TYPE.SIGNIN
|
||
|
|
): void {
|
||
|
|
// Ensure we don't overwrite interaction in progress for a different clientId
|
||
|
|
const key = `${CacheKeys.PREFIX}.${TemporaryCacheKeys.INTERACTION_STATUS_KEY}`;
|
||
|
|
if (inProgress) {
|
||
|
|
if (this.getInteractionInProgress()) {
|
||
|
|
throw createBrowserAuthError(
|
||
|
|
BrowserAuthErrorCodes.interactionInProgress
|
||
|
|
);
|
||
|
|
} else {
|
||
|
|
// No interaction is in progress
|
||
|
|
this.setTemporaryCache(
|
||
|
|
key,
|
||
|
|
JSON.stringify({ clientId: this.clientId, type }),
|
||
|
|
false
|
||
|
|
);
|
||
|
|
}
|
||
|
|
} else if (
|
||
|
|
!inProgress &&
|
||
|
|
this.getInteractionInProgress()?.clientId === this.clientId
|
||
|
|
) {
|
||
|
|
this.removeTemporaryItem(key);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Builds credential entities from AuthenticationResult object and saves the resulting credentials to the cache
|
||
|
|
* @param result
|
||
|
|
* @param request
|
||
|
|
*/
|
||
|
|
async hydrateCache(
|
||
|
|
result: AuthenticationResult,
|
||
|
|
request:
|
||
|
|
| SilentRequest
|
||
|
|
| SsoSilentRequest
|
||
|
|
| RedirectRequest
|
||
|
|
| PopupRequest
|
||
|
|
): Promise<void> {
|
||
|
|
const idTokenEntity = CacheHelpers.createIdTokenEntity(
|
||
|
|
result.account?.homeAccountId,
|
||
|
|
result.account?.environment,
|
||
|
|
result.idToken,
|
||
|
|
this.clientId,
|
||
|
|
result.tenantId
|
||
|
|
);
|
||
|
|
|
||
|
|
let claimsHash;
|
||
|
|
if (request.claims) {
|
||
|
|
claimsHash = await this.cryptoImpl.hashString(request.claims);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* meta data for cache stores time in seconds from epoch
|
||
|
|
* AuthenticationResult returns expiresOn and extExpiresOn in milliseconds (as a Date object which is in ms)
|
||
|
|
* We need to map these for the cache when building tokens from AuthenticationResult
|
||
|
|
*
|
||
|
|
* The next MSAL VFuture should map these both to same value if possible
|
||
|
|
*/
|
||
|
|
|
||
|
|
const accessTokenEntity = CacheHelpers.createAccessTokenEntity(
|
||
|
|
result.account?.homeAccountId,
|
||
|
|
result.account.environment,
|
||
|
|
result.accessToken,
|
||
|
|
this.clientId,
|
||
|
|
result.tenantId,
|
||
|
|
result.scopes.join(" "),
|
||
|
|
// Access token expiresOn stored in seconds, converting from AuthenticationResult expiresOn stored as Date
|
||
|
|
result.expiresOn
|
||
|
|
? TimeUtils.toSecondsFromDate(result.expiresOn)
|
||
|
|
: 0,
|
||
|
|
result.extExpiresOn
|
||
|
|
? TimeUtils.toSecondsFromDate(result.extExpiresOn)
|
||
|
|
: 0,
|
||
|
|
base64Decode,
|
||
|
|
undefined, // refreshOn
|
||
|
|
result.tokenType as AuthenticationScheme,
|
||
|
|
undefined, // userAssertionHash
|
||
|
|
request.sshKid,
|
||
|
|
request.claims,
|
||
|
|
claimsHash
|
||
|
|
);
|
||
|
|
|
||
|
|
const cacheRecord = {
|
||
|
|
idToken: idTokenEntity,
|
||
|
|
accessToken: accessTokenEntity,
|
||
|
|
};
|
||
|
|
return this.saveCacheRecord(cacheRecord, result.correlationId);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* saves a cache record
|
||
|
|
* @param cacheRecord {CacheRecord}
|
||
|
|
* @param storeInCache {?StoreInCache}
|
||
|
|
* @param correlationId {?string} correlation id
|
||
|
|
*/
|
||
|
|
async saveCacheRecord(
|
||
|
|
cacheRecord: CacheRecord,
|
||
|
|
correlationId: string,
|
||
|
|
storeInCache?: StoreInCache
|
||
|
|
): Promise<void> {
|
||
|
|
try {
|
||
|
|
await super.saveCacheRecord(
|
||
|
|
cacheRecord,
|
||
|
|
correlationId,
|
||
|
|
storeInCache
|
||
|
|
);
|
||
|
|
} catch (e) {
|
||
|
|
if (
|
||
|
|
e instanceof CacheError &&
|
||
|
|
this.performanceClient &&
|
||
|
|
correlationId
|
||
|
|
) {
|
||
|
|
try {
|
||
|
|
const tokenKeys = this.getTokenKeys();
|
||
|
|
|
||
|
|
this.performanceClient.addFields(
|
||
|
|
{
|
||
|
|
cacheRtCount: tokenKeys.refreshToken.length,
|
||
|
|
cacheIdCount: tokenKeys.idToken.length,
|
||
|
|
cacheAtCount: tokenKeys.accessToken.length,
|
||
|
|
},
|
||
|
|
correlationId
|
||
|
|
);
|
||
|
|
} catch (e) {}
|
||
|
|
}
|
||
|
|
|
||
|
|
throw e;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Returns a window storage class implementing the IWindowStorage interface that corresponds to the configured cacheLocation.
|
||
|
|
* @param cacheLocation
|
||
|
|
*/
|
||
|
|
function getStorageImplementation(
|
||
|
|
clientId: string,
|
||
|
|
cacheLocation: BrowserCacheLocation | string,
|
||
|
|
logger: Logger,
|
||
|
|
performanceClient: IPerformanceClient
|
||
|
|
): IWindowStorage<string> {
|
||
|
|
try {
|
||
|
|
switch (cacheLocation) {
|
||
|
|
case BrowserCacheLocation.LocalStorage:
|
||
|
|
return new LocalStorage(clientId, logger, performanceClient);
|
||
|
|
case BrowserCacheLocation.SessionStorage:
|
||
|
|
return new SessionStorage();
|
||
|
|
case BrowserCacheLocation.MemoryStorage:
|
||
|
|
default:
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
} catch (e) {
|
||
|
|
logger.error(e as string);
|
||
|
|
}
|
||
|
|
|
||
|
|
return new MemoryStorage();
|
||
|
|
}
|
||
|
|
|
||
|
|
export const DEFAULT_BROWSER_CACHE_MANAGER = (
|
||
|
|
clientId: string,
|
||
|
|
logger: Logger,
|
||
|
|
performanceClient: IPerformanceClient,
|
||
|
|
eventHandler: EventHandler
|
||
|
|
): BrowserCacheManager => {
|
||
|
|
const cacheOptions: Required<CacheOptions> = {
|
||
|
|
cacheLocation: BrowserCacheLocation.MemoryStorage,
|
||
|
|
cacheRetentionDays: 5,
|
||
|
|
temporaryCacheLocation: BrowserCacheLocation.MemoryStorage,
|
||
|
|
storeAuthStateInCookie: false,
|
||
|
|
secureCookies: false,
|
||
|
|
cacheMigrationEnabled: false,
|
||
|
|
claimsBasedCachingEnabled: false,
|
||
|
|
};
|
||
|
|
return new BrowserCacheManager(
|
||
|
|
clientId,
|
||
|
|
cacheOptions,
|
||
|
|
DEFAULT_CRYPTO_IMPLEMENTATION,
|
||
|
|
logger,
|
||
|
|
performanceClient,
|
||
|
|
eventHandler
|
||
|
|
);
|
||
|
|
};
|