Estructura inicial del proyecto

This commit is contained in:
2026-06-02 16:57:08 +00:00
commit 8b306b9afc
9864 changed files with 1435687 additions and 0 deletions

View File

@ -0,0 +1,79 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { AccountInfo, AccountFilter, Logger } from "@azure/msal-common/browser";
import { BrowserCacheManager } from "./BrowserCacheManager.js";
/**
* Returns all the accounts in the cache that match the optional filter. If no filter is provided, all accounts are returned.
* @param accountFilter - (Optional) filter to narrow down the accounts returned
* @returns Array of AccountInfo objects in cache
*/
export function getAllAccounts(
logger: Logger,
browserStorage: BrowserCacheManager,
isInBrowser: boolean,
correlationId: string,
accountFilter?: AccountFilter
): AccountInfo[] {
logger.verbose("getAllAccounts called", correlationId);
return isInBrowser
? browserStorage.getAllAccounts(accountFilter, correlationId)
: [];
}
/**
* Returns the first account found in the cache that matches the account filter passed in.
* @param accountFilter
* @returns The first account found in the cache matching the provided filter or null if no account could be found.
*/
export function getAccount(
accountFilter: AccountFilter,
logger: Logger,
browserStorage: BrowserCacheManager,
correlationId: string
): AccountInfo | null {
logger.trace("getAccount called", correlationId);
const account: AccountInfo | null = browserStorage.getAccountInfoFilteredBy(
accountFilter,
correlationId
);
if (account) {
logger.verbose(
"getAccount: Account matching provided filter found, returning",
correlationId
);
return account;
} else {
logger.verbose(
"getAccount: No matching account found, returning null",
correlationId
);
return null;
}
}
/**
* Sets the account to use as the active account. If no account is passed to the acquireToken APIs, then MSAL will use this active account.
* @param account
*/
export function setActiveAccount(
account: AccountInfo | null,
browserStorage: BrowserCacheManager,
correlationId: string
): void {
browserStorage.setActiveAccount(account, correlationId);
}
/**
* Gets the currently active account
*/
export function getActiveAccount(
browserStorage: BrowserCacheManager,
correlationId: string
): AccountInfo | null {
return browserStorage.getActiveAccount(correlationId);
}

View File

@ -0,0 +1,173 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { Logger } from "@azure/msal-common/browser";
import {
BrowserAuthError,
BrowserAuthErrorCodes,
} from "../error/BrowserAuthError.js";
import { DatabaseStorage } from "./DatabaseStorage.js";
import { IAsyncStorage } from "./IAsyncStorage.js";
import { MemoryStorage } from "./MemoryStorage.js";
/**
* This class allows MSAL to store artifacts asynchronously using the DatabaseStorage IndexedDB wrapper,
* backed up with the more volatile MemoryStorage object for cases in which IndexedDB may be unavailable.
*/
export class AsyncMemoryStorage<T> implements IAsyncStorage<T> {
private inMemoryCache: MemoryStorage<T>;
private indexedDBCache: DatabaseStorage<T>;
private logger: Logger;
constructor(logger: Logger) {
this.inMemoryCache = new MemoryStorage<T>();
this.indexedDBCache = new DatabaseStorage<T>();
this.logger = logger;
}
private handleDatabaseAccessError(
error: unknown,
correlationId: string
): void {
if (
error instanceof BrowserAuthError &&
error.errorCode === BrowserAuthErrorCodes.databaseUnavailable
) {
this.logger.error(
"Could not access persistent storage. This may be caused by browser privacy features which block persistent storage in third-party contexts.",
correlationId
);
} else {
throw error;
}
}
/**
* Get the item matching the given key. Tries in-memory cache first, then in the asynchronous
* storage object if item isn't found in-memory.
* @param key
* @param correlationId
*/
async getItem(key: string, correlationId: string): Promise<T | null> {
const item = this.inMemoryCache.getItem(key);
if (!item) {
try {
this.logger.verbose(
"Queried item not found in in-memory cache, now querying persistent storage.",
correlationId
);
return await this.indexedDBCache.getItem(key);
} catch (e) {
this.handleDatabaseAccessError(e, correlationId);
}
}
return item;
}
/**
* Sets the item in the in-memory cache and then tries to set it in the asynchronous
* storage object with the given key.
* @param key
* @param value
* @param correlationId
*/
async setItem(key: string, value: T, correlationId: string): Promise<void> {
this.inMemoryCache.setItem(key, value);
try {
await this.indexedDBCache.setItem(key, value);
} catch (e) {
this.handleDatabaseAccessError(e, correlationId);
}
}
/**
* Removes the item matching the key from the in-memory cache, then tries to remove it from the asynchronous storage object.
* @param key
* @param correlationId
*/
async removeItem(key: string, correlationId: string): Promise<void> {
this.inMemoryCache.removeItem(key);
try {
await this.indexedDBCache.removeItem(key);
} catch (e) {
this.handleDatabaseAccessError(e, correlationId);
}
}
/**
* Get all the keys from the in-memory cache as an iterable array of strings. If no keys are found, query the keys in the
* asynchronous storage object.
* @param correlationId
*/
async getKeys(correlationId: string): Promise<string[]> {
const cacheKeys = this.inMemoryCache.getKeys();
if (cacheKeys.length === 0) {
try {
this.logger.verbose(
"In-memory cache is empty, now querying persistent storage.",
correlationId
);
return await this.indexedDBCache.getKeys();
} catch (e) {
this.handleDatabaseAccessError(e, correlationId);
}
}
return cacheKeys;
}
/**
* Returns true or false if the given key is present in the cache.
* @param key
* @param correlationId
*/
async containsKey(key: string, correlationId: string): Promise<boolean> {
const containsKey = this.inMemoryCache.containsKey(key);
if (!containsKey) {
try {
this.logger.verbose(
"Key not found in in-memory cache, now querying persistent storage.",
correlationId
);
return await this.indexedDBCache.containsKey(key);
} catch (e) {
this.handleDatabaseAccessError(e, correlationId);
}
}
return containsKey;
}
/**
* Clears in-memory Map
* @param correlationId
*/
clearInMemory(correlationId: string): void {
// InMemory cache is a Map instance, clear is straightforward
this.logger.verbose(`Deleting in-memory keystore`, correlationId);
this.inMemoryCache.clear();
this.logger.verbose(`In-memory keystore deleted`, correlationId);
}
/**
* Tries to delete the IndexedDB database
* @param correlationId
* @returns
*/
async clearPersistent(correlationId: string): Promise<boolean> {
try {
this.logger.verbose("Deleting persistent keystore", correlationId);
const dbDeleted = await this.indexedDBCache.deleteDatabase();
if (dbDeleted) {
this.logger.verbose(
"Persistent keystore deleted",
correlationId
);
}
return dbDeleted;
} catch (e) {
this.handleDatabaseAccessError(e, correlationId);
return false;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,60 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { TokenKeys } from "@azure/msal-common/browser";
import { IWindowStorage } from "./IWindowStorage.js";
import * as CacheKeys from "./CacheKeys.js";
/**
* Returns a list of cache keys for all known accounts
* @param storage
* @returns
*/
export function getAccountKeys(
storage: IWindowStorage<string>,
schemaVersion?: number
): Array<string> {
const accountKeys = storage.getItem(
CacheKeys.getAccountKeysCacheKey(schemaVersion)
);
if (accountKeys) {
return JSON.parse(accountKeys);
}
return [];
}
/**
* Returns a list of cache keys for all known tokens
* @param clientId
* @param storage
* @returns
*/
export function getTokenKeys(
clientId: string,
storage: IWindowStorage<string>,
schemaVersion?: number
): TokenKeys {
const item = storage.getItem(
CacheKeys.getTokenKeysCacheKey(clientId, schemaVersion)
);
if (item) {
const tokenKeys = JSON.parse(item);
if (
tokenKeys &&
tokenKeys.hasOwnProperty("idToken") &&
tokenKeys.hasOwnProperty("accessToken") &&
tokenKeys.hasOwnProperty("refreshToken")
) {
return tokenKeys as TokenKeys;
}
}
return {
idToken: [],
accessToken: [],
refreshToken: [],
};
}

View File

@ -0,0 +1,40 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
export const PREFIX = "msal";
const BROWSER_PREFIX = "browser";
export const CACHE_KEY_SEPARATOR = "|";
export const CREDENTIAL_SCHEMA_VERSION = 3;
export const ACCOUNT_SCHEMA_VERSION = 3;
export const LOG_LEVEL_CACHE_KEY = `${PREFIX}.${BROWSER_PREFIX}.log.level`;
export const LOG_PII_CACHE_KEY = `${PREFIX}.${BROWSER_PREFIX}.log.pii`;
export const BROWSER_PERF_ENABLED_KEY = `${PREFIX}.${BROWSER_PREFIX}.performance.enabled`;
export const PLATFORM_AUTH_DOM_SUPPORT = `${PREFIX}.${BROWSER_PREFIX}.platform.auth.dom`;
export const VERSION_CACHE_KEY = `${PREFIX}.version`;
export const ACCOUNT_KEYS = "account.keys";
export const TOKEN_KEYS = "token.keys";
export const SSO_CAPABLE = `${PREFIX}.${BROWSER_PREFIX}.sso.capable`;
export function getAccountKeysCacheKey(
schema: number = ACCOUNT_SCHEMA_VERSION
): string {
if (schema < 1) {
return `${PREFIX}.${ACCOUNT_KEYS}`;
}
return `${PREFIX}.${schema}.${ACCOUNT_KEYS}`;
}
export function getTokenKeysCacheKey(
clientId: string,
schema: number = CREDENTIAL_SCHEMA_VERSION
): string {
if (schema < 1) {
return `${PREFIX}.${TOKEN_KEYS}.${clientId}`;
}
return `${PREFIX}.${schema}.${TOKEN_KEYS}.${clientId}`;
}

View File

@ -0,0 +1,125 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
ClientAuthErrorCodes,
createClientAuthError,
} from "@azure/msal-common/browser";
import { IWindowStorage } from "./IWindowStorage.js";
// Cookie life calculation (hours * minutes * seconds * ms)
const COOKIE_LIFE_MULTIPLIER = 24 * 60 * 60 * 1000;
export const SameSiteOptions = {
Lax: "Lax",
None: "None",
} as const;
export type SameSiteOptions =
(typeof SameSiteOptions)[keyof typeof SameSiteOptions];
export class CookieStorage implements IWindowStorage<string> {
initialize(): Promise<void> {
return Promise.resolve();
}
getItem(key: string): string | null {
const name = encodeURIComponent(key);
const cookieList = document.cookie.split(";");
for (let i = 0; i < cookieList.length; i++) {
const cookie = cookieList[i].trim();
const eqIndex = cookie.indexOf("=");
const rawKey =
eqIndex === -1 ? cookie : cookie.substring(0, eqIndex);
if (rawKey === name) {
const rawValue =
eqIndex === -1 ? "" : cookie.substring(eqIndex + 1);
try {
return decodeURIComponent(rawValue);
} catch {
return rawValue;
}
}
}
return "";
}
getUserData(): string | null {
throw createClientAuthError(ClientAuthErrorCodes.methodNotImplemented);
}
setItem(
key: string,
value: string,
cookieLifeDays?: number,
secure: boolean = true,
sameSite: SameSiteOptions = SameSiteOptions.Lax
): void {
let cookieStr = `${encodeURIComponent(key)}=${encodeURIComponent(
value
)};path=/;SameSite=${sameSite};`;
if (cookieLifeDays) {
const expireTime = getCookieExpirationTime(cookieLifeDays);
cookieStr += `expires=${expireTime};`;
}
if (secure || sameSite === SameSiteOptions.None) {
// SameSite None requires Secure flag
cookieStr += "Secure;";
}
document.cookie = cookieStr;
}
async setUserData(): Promise<void> {
return Promise.reject(
createClientAuthError(ClientAuthErrorCodes.methodNotImplemented)
);
}
removeItem(key: string): void {
// Setting expiration to -1 removes it
this.setItem(key, "", -1);
}
getKeys(): string[] {
const cookieList = document.cookie.split(";");
const keys: Array<string> = [];
cookieList.forEach((cookie) => {
const trimmed = cookie.trim();
const eqIndex = trimmed.indexOf("=");
const rawKey =
eqIndex === -1 ? trimmed : trimmed.substring(0, eqIndex);
try {
keys.push(decodeURIComponent(rawKey));
} catch {
// Skip cookies with malformed percent-encoded sequences in the key
}
});
return keys;
}
containsKey(key: string): boolean {
return this.getKeys().includes(key);
}
decryptData(): Promise<object | null> {
// Cookie storage does not support encryption, so this method is a no-op
return Promise.resolve(null);
}
}
/**
* Get cookie expiration time
* @param cookieLifeDays
*/
export function getCookieExpirationTime(cookieLifeDays: number): string {
const today = new Date();
const expr = new Date(
today.getTime() + cookieLifeDays * COOKIE_LIFE_MULTIPLIER
);
return expr.toUTCString();
}

View File

@ -0,0 +1,301 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
createBrowserAuthError,
BrowserAuthErrorCodes,
} from "../error/BrowserAuthError.js";
import {
DB_NAME,
DB_TABLE_NAME,
DB_VERSION,
} from "../utils/BrowserConstants.js";
import { IAsyncStorage } from "./IAsyncStorage.js";
interface IDBOpenDBRequestEvent extends Event {
target: IDBOpenDBRequest & EventTarget;
}
interface IDBOpenOnUpgradeNeededEvent extends IDBVersionChangeEvent {
target: IDBOpenDBRequest & EventTarget;
}
interface IDBRequestEvent extends Event {
target: IDBRequest & EventTarget;
}
/**
* Storage wrapper for IndexedDB storage in browsers: https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API
*/
export class DatabaseStorage<T> implements IAsyncStorage<T> {
private db: IDBDatabase | undefined;
private dbName: string;
private tableName: string;
private version: number;
private dbOpen: boolean;
constructor() {
this.dbName = DB_NAME;
this.version = DB_VERSION;
this.tableName = DB_TABLE_NAME;
this.dbOpen = false;
}
/**
* Opens IndexedDB instance.
*/
async open(): Promise<void> {
return new Promise((resolve, reject) => {
const openDB = window.indexedDB.open(this.dbName, this.version);
openDB.addEventListener(
"upgradeneeded",
(e: IDBVersionChangeEvent) => {
const event = e as IDBOpenOnUpgradeNeededEvent;
event.target.result.createObjectStore(this.tableName);
}
);
openDB.addEventListener("success", (e: Event) => {
const event = e as IDBOpenDBRequestEvent;
this.db = event.target.result;
this.dbOpen = true;
resolve();
});
openDB.addEventListener("error", () =>
reject(
createBrowserAuthError(
BrowserAuthErrorCodes.databaseUnavailable
)
)
);
});
}
/**
* Closes the connection to IndexedDB database when all pending transactions
* complete.
*/
closeConnection(): void {
const db = this.db;
if (db && this.dbOpen) {
db.close();
this.dbOpen = false;
}
}
/**
* Opens database if it's not already open
*/
private async validateDbIsOpen(): Promise<void> {
if (!this.dbOpen) {
return this.open();
}
}
/**
* Retrieves item from IndexedDB instance.
* @param key
*/
async getItem(key: string): Promise<T | null> {
await this.validateDbIsOpen();
return new Promise<T>((resolve, reject) => {
// TODO: Add timeouts?
if (!this.db) {
return reject(
createBrowserAuthError(
BrowserAuthErrorCodes.databaseNotOpen
)
);
}
const transaction = this.db.transaction(
[this.tableName],
"readonly"
);
const objectStore = transaction.objectStore(this.tableName);
const dbGet = objectStore.get(key);
dbGet.addEventListener("success", (e: Event) => {
const event = e as IDBRequestEvent;
this.closeConnection();
resolve(event.target.result);
});
dbGet.addEventListener("error", (e: Event) => {
this.closeConnection();
reject(e);
});
});
}
/**
* Adds item to IndexedDB under given key
* @param key
* @param payload
*/
async setItem(key: string, payload: T): Promise<void> {
await this.validateDbIsOpen();
return new Promise<void>((resolve: Function, reject: Function) => {
// TODO: Add timeouts?
if (!this.db) {
return reject(
createBrowserAuthError(
BrowserAuthErrorCodes.databaseNotOpen
)
);
}
const transaction = this.db.transaction(
[this.tableName],
"readwrite"
);
const objectStore = transaction.objectStore(this.tableName);
const dbPut = objectStore.put(payload, key);
dbPut.addEventListener("success", () => {
this.closeConnection();
resolve();
});
dbPut.addEventListener("error", (e) => {
this.closeConnection();
reject(e);
});
});
}
/**
* Removes item from IndexedDB under given key
* @param key
*/
async removeItem(key: string): Promise<void> {
await this.validateDbIsOpen();
return new Promise<void>((resolve: Function, reject: Function) => {
if (!this.db) {
return reject(
createBrowserAuthError(
BrowserAuthErrorCodes.databaseNotOpen
)
);
}
const transaction = this.db.transaction(
[this.tableName],
"readwrite"
);
const objectStore = transaction.objectStore(this.tableName);
const dbDelete = objectStore.delete(key);
dbDelete.addEventListener("success", () => {
this.closeConnection();
resolve();
});
dbDelete.addEventListener("error", (e) => {
this.closeConnection();
reject(e);
});
});
}
/**
* Get all the keys from the storage object as an iterable array of strings.
*/
async getKeys(): Promise<string[]> {
await this.validateDbIsOpen();
return new Promise<string[]>((resolve: Function, reject: Function) => {
if (!this.db) {
return reject(
createBrowserAuthError(
BrowserAuthErrorCodes.databaseNotOpen
)
);
}
const transaction = this.db.transaction(
[this.tableName],
"readonly"
);
const objectStore = transaction.objectStore(this.tableName);
const dbGetKeys = objectStore.getAllKeys();
dbGetKeys.addEventListener("success", (e: Event) => {
const event = e as IDBRequestEvent;
this.closeConnection();
resolve(event.target.result);
});
dbGetKeys.addEventListener("error", (e: Event) => {
this.closeConnection();
reject(e);
});
});
}
/**
*
* Checks whether there is an object under the search key in the object store
*/
async containsKey(key: string): Promise<boolean> {
await this.validateDbIsOpen();
return new Promise<boolean>((resolve: Function, reject: Function) => {
if (!this.db) {
return reject(
createBrowserAuthError(
BrowserAuthErrorCodes.databaseNotOpen
)
);
}
const transaction = this.db.transaction(
[this.tableName],
"readonly"
);
const objectStore = transaction.objectStore(this.tableName);
const dbContainsKey = objectStore.count(key);
dbContainsKey.addEventListener("success", (e: Event) => {
const event = e as IDBRequestEvent;
this.closeConnection();
resolve(event.target.result === 1);
});
dbContainsKey.addEventListener("error", (e: Event) => {
this.closeConnection();
reject(e);
});
});
}
/**
* Deletes the MSAL database. The database is deleted rather than cleared to make it possible
* for client applications to downgrade to a previous MSAL version without worrying about forward compatibility issues
* with IndexedDB database versions.
*/
async deleteDatabase(): Promise<boolean> {
// Check if database being deleted exists
if (this.db && this.dbOpen) {
this.closeConnection();
}
return new Promise<boolean>((resolve: Function, reject: Function) => {
const deleteDbRequest = window.indexedDB.deleteDatabase(DB_NAME);
const id = setTimeout(() => reject(false), 200); // Reject if events aren't raised within 200ms
deleteDbRequest.addEventListener("success", () => {
clearTimeout(id);
return resolve(true);
});
deleteDbRequest.addEventListener("blocked", () => {
clearTimeout(id);
return resolve(true);
});
deleteDbRequest.addEventListener("error", () => {
clearTimeout(id);
return reject(false);
});
});
}
}

View File

@ -0,0 +1,19 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
export type EncryptedData = {
id: string;
nonce: string;
data: string;
lastUpdatedAt: string;
};
export function isEncrypted(data: object): data is EncryptedData {
return (
data.hasOwnProperty("id") &&
data.hasOwnProperty("nonce") &&
data.hasOwnProperty("data")
);
}

View File

@ -0,0 +1,41 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
export interface IAsyncStorage<T> {
/**
* Get the item from the asynchronous storage object matching the given key.
* @param key
* @param correlationId
*/
getItem(key: string, correlationId: string): Promise<T | null>;
/**
* Sets the item in the asynchronous storage object with the given key.
* @param key
* @param value
* @param correlationId
*/
setItem(key: string, value: T, correlationId: string): Promise<void>;
/**
* Removes the item in the asynchronous storage object matching the given key.
* @param key
* @param correlationId
*/
removeItem(key: string, correlationId: string): Promise<void>;
/**
* Get all the keys from the asynchronous storage object as an iterable array of strings.
* @param correlationId
*/
getKeys(correlationId: string): Promise<string[]>;
/**
* Returns true or false if the given key is present in the cache.
* @param key
* @param correlationId
*/
containsKey(key: string, correlationId: string): Promise<boolean>;
}

View File

@ -0,0 +1,64 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { EncryptedData } from "./EncryptedData.js";
export interface IWindowStorage<T> {
/**
* Async initializer
*/
initialize(correlationId: string): Promise<void>;
/**
* Get the item from the window storage object matching the given key.
* @param key
*/
getItem(key: string): T | null;
/**
* Getter for sensitive data that may contain PII.
*/
getUserData(key: string): T | null;
/**
* Sets the item in the window storage object with the given key.
* @param key
* @param value
*/
setItem(key: string, value: T): void;
/**
* Setter for sensitive data that may contain PII.
*/
setUserData(
key: string,
value: T,
correlationId: string,
timestamp: string,
kmsi: boolean
): Promise<void>;
/**
* Removes the item in the window storage object matching the given key.
* @param key
*/
removeItem(key: string): void;
/**
* Get all the keys from the window storage object as an iterable array of strings.
*/
getKeys(): string[];
/**
* Returns true or false if the given key is present in the cache.
* @param key
*/
containsKey(key: string): boolean;
decryptData(
key: string,
data: EncryptedData,
correlationId: string
): Promise<object | null>;
}

View File

@ -0,0 +1,516 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
TokenKeys,
IPerformanceClient,
invokeAsync,
Logger,
invoke,
} from "@azure/msal-common/browser";
import * as BrowserPerformanceEvents from "../telemetry/BrowserPerformanceEvents.js";
import * as BrowserRootPerformanceEvents from "../telemetry/BrowserRootPerformanceEvents.js";
import {
createNewGuid,
decrypt,
encrypt,
generateBaseKey,
generateHKDF,
} from "../crypto/BrowserCrypto.js";
import { base64DecToArr } from "../encode/Base64Decode.js";
import { urlEncodeArr } from "../encode/Base64Encode.js";
import {
BrowserAuthErrorCodes,
createBrowserAuthError,
} from "../error/BrowserAuthError.js";
import {
BrowserConfigurationAuthErrorCodes,
createBrowserConfigurationAuthError,
} from "../error/BrowserConfigurationAuthError.js";
import { CookieStorage, SameSiteOptions } from "./CookieStorage.js";
import { IWindowStorage } from "./IWindowStorage.js";
import { MemoryStorage } from "./MemoryStorage.js";
import { getAccountKeys, getTokenKeys } from "./CacheHelpers.js";
import * as CacheKeys from "./CacheKeys.js";
import { EncryptedData, isEncrypted } from "./EncryptedData.js";
const ENCRYPTION_KEY = "msal.cache.encryption";
const BROADCAST_CHANNEL_NAME = "msal.broadcast.cache";
type EncryptionCookie = {
id: string;
key: CryptoKey;
};
export class LocalStorage implements IWindowStorage<string> {
private clientId: string;
private initialized: boolean;
private memoryStorage: MemoryStorage<string>;
private performanceClient: IPerformanceClient;
private logger: Logger;
private encryptionCookie?: EncryptionCookie;
private broadcast: BroadcastChannel;
constructor(
clientId: string,
logger: Logger,
performanceClient: IPerformanceClient
) {
if (!window.localStorage) {
throw createBrowserConfigurationAuthError(
BrowserConfigurationAuthErrorCodes.storageNotSupported
);
}
this.memoryStorage = new MemoryStorage<string>();
this.initialized = false;
this.clientId = clientId;
this.logger = logger;
this.performanceClient = performanceClient;
this.broadcast = new BroadcastChannel(BROADCAST_CHANNEL_NAME);
}
async initialize(correlationId: string): Promise<void> {
const cookies = new CookieStorage();
const cookieString = cookies.getItem(ENCRYPTION_KEY);
let parsedCookie = { key: "", id: "" };
if (cookieString) {
try {
parsedCookie = JSON.parse(cookieString);
} catch (e) {}
}
if (parsedCookie.key && parsedCookie.id) {
// Encryption key already exists, import
const baseKey = invoke(
base64DecToArr,
BrowserPerformanceEvents.Base64Decode,
this.logger,
this.performanceClient,
correlationId
)(parsedCookie.key);
this.encryptionCookie = {
id: parsedCookie.id,
key: await invokeAsync(
generateHKDF,
BrowserPerformanceEvents.GenerateHKDF,
this.logger,
this.performanceClient,
correlationId
)(baseKey),
};
} else {
// Encryption key doesn't exist or is invalid, generate a new one
const id = createNewGuid();
const baseKey = await invokeAsync(
generateBaseKey,
BrowserPerformanceEvents.GenerateBaseKey,
this.logger,
this.performanceClient,
correlationId
)();
const keyStr = invoke(
urlEncodeArr,
BrowserPerformanceEvents.UrlEncodeArr,
this.logger,
this.performanceClient,
correlationId
)(new Uint8Array(baseKey));
this.encryptionCookie = {
id: id,
key: await invokeAsync(
generateHKDF,
BrowserPerformanceEvents.GenerateHKDF,
this.logger,
this.performanceClient,
correlationId
)(baseKey),
};
const cookieData = {
id: id,
key: keyStr,
};
cookies.setItem(
ENCRYPTION_KEY,
JSON.stringify(cookieData),
0, // Expiration - 0 means cookie will be cleared at the end of the browser session
true, // Secure flag
SameSiteOptions.None // SameSite must be None to support iframed apps
);
}
await invokeAsync(
this.importExistingCache.bind(this),
BrowserPerformanceEvents.ImportExistingCache,
this.logger,
this.performanceClient,
correlationId
)(correlationId);
// Register listener for cache updates in other tabs
this.broadcast.addEventListener("message", (event: MessageEvent) => {
this.updateCache(event, correlationId);
});
this.initialized = true;
}
getItem(key: string): string | null {
return window.localStorage.getItem(key);
}
getUserData(key: string): string | null {
if (!this.initialized) {
throw createBrowserAuthError(
BrowserAuthErrorCodes.uninitializedPublicClientApplication
);
}
return this.memoryStorage.getItem(key);
}
async decryptData(
key: string,
data: EncryptedData,
correlationId: string
): Promise<object | null> {
if (!this.initialized || !this.encryptionCookie) {
throw createBrowserAuthError(
BrowserAuthErrorCodes.uninitializedPublicClientApplication
);
}
if (data.id !== this.encryptionCookie.id) {
// Data was encrypted with a different key. It must be removed because it is from a previous session.
this.performanceClient.incrementFields(
{ encryptedCacheExpiredCount: 1 },
correlationId
);
return null;
}
const decryptedData = await invokeAsync(
decrypt,
BrowserPerformanceEvents.Decrypt,
this.logger,
this.performanceClient,
correlationId
)(
this.encryptionCookie.key,
data.nonce,
this.getContext(key),
data.data
);
if (!decryptedData) {
return null;
}
try {
return {
...JSON.parse(decryptedData),
lastUpdatedAt: data.lastUpdatedAt,
};
} catch (e) {
this.performanceClient.incrementFields(
{ encryptedCacheCorruptionCount: 1 },
correlationId
);
return null;
}
}
setItem(key: string, value: string): void {
window.localStorage.setItem(key, value);
}
async setUserData(
key: string,
value: string,
correlationId: string,
timestamp: string,
kmsi: boolean
): Promise<void> {
if (!this.initialized || !this.encryptionCookie) {
throw createBrowserAuthError(
BrowserAuthErrorCodes.uninitializedPublicClientApplication
);
}
if (kmsi) {
this.setItem(key, value);
} else {
const { data, nonce } = await invokeAsync(
encrypt,
BrowserPerformanceEvents.Encrypt,
this.logger,
this.performanceClient,
correlationId
)(this.encryptionCookie.key, value, this.getContext(key));
const encryptedData: EncryptedData = {
id: this.encryptionCookie.id,
nonce: nonce,
data: data,
lastUpdatedAt: timestamp,
};
this.setItem(key, JSON.stringify(encryptedData));
}
this.memoryStorage.setItem(key, value);
// Notify other frames to update their in-memory cache
this.broadcast.postMessage({
key: key,
value: value,
context: this.getContext(key),
});
}
removeItem(key: string): void {
if (this.memoryStorage.containsKey(key)) {
this.memoryStorage.removeItem(key);
this.broadcast.postMessage({
key: key,
value: null,
context: this.getContext(key),
});
}
window.localStorage.removeItem(key);
}
getKeys(): string[] {
return Object.keys(window.localStorage);
}
containsKey(key: string): boolean {
return window.localStorage.hasOwnProperty(key);
}
/**
* Removes all known MSAL keys from the cache
*/
clear(): void {
// Removes all remaining MSAL cache items
this.memoryStorage.clear();
const accountKeys = getAccountKeys(this);
accountKeys.forEach((key) => this.removeItem(key));
const tokenKeys = getTokenKeys(this.clientId, this);
tokenKeys.idToken.forEach((key) => this.removeItem(key));
tokenKeys.accessToken.forEach((key) => this.removeItem(key));
tokenKeys.refreshToken.forEach((key) => this.removeItem(key));
// Clean up anything left
this.getKeys().forEach((cacheKey: string) => {
if (
cacheKey.startsWith(CacheKeys.PREFIX) ||
cacheKey.indexOf(this.clientId) !== -1
) {
this.removeItem(cacheKey);
}
});
}
/**
* Helper to decrypt all known MSAL keys in localStorage and save them to inMemory storage
* @returns
*/
private async importExistingCache(correlationId: string): Promise<void> {
if (!this.encryptionCookie) {
return;
}
let accountKeys = getAccountKeys(this);
accountKeys = await this.importArray(accountKeys, correlationId);
// Write valid account keys back to map
if (accountKeys.length) {
this.setItem(
CacheKeys.getAccountKeysCacheKey(),
JSON.stringify(accountKeys)
);
} else {
this.removeItem(CacheKeys.getAccountKeysCacheKey());
}
const tokenKeys: TokenKeys = getTokenKeys(this.clientId, this);
tokenKeys.idToken = await this.importArray(
tokenKeys.idToken,
correlationId
);
tokenKeys.accessToken = await this.importArray(
tokenKeys.accessToken,
correlationId
);
tokenKeys.refreshToken = await this.importArray(
tokenKeys.refreshToken,
correlationId
);
// Write valid token keys back to map
if (
tokenKeys.idToken.length ||
tokenKeys.accessToken.length ||
tokenKeys.refreshToken.length
) {
this.setItem(
CacheKeys.getTokenKeysCacheKey(this.clientId),
JSON.stringify(tokenKeys)
);
} else {
this.removeItem(CacheKeys.getTokenKeysCacheKey(this.clientId));
}
}
/**
* Helper to decrypt and save cache entries
* @param key
* @returns
*/
private async getItemFromEncryptedCache(
key: string,
correlationId: string
): Promise<string | null> {
if (!this.encryptionCookie) {
return null;
}
const rawCache = this.getItem(key);
if (!rawCache) {
return null;
}
let encObj: EncryptedData;
try {
encObj = JSON.parse(rawCache);
} catch (e) {
// Not a valid encrypted object, remove
return null;
}
if (!isEncrypted(encObj)) {
// Data is not encrypted
this.performanceClient.incrementFields(
{ unencryptedCacheCount: 1 },
correlationId
);
return rawCache;
}
if (encObj.id !== this.encryptionCookie.id) {
// Data was encrypted with a different key. It must be removed because it is from a previous session.
this.performanceClient.incrementFields(
{ encryptedCacheExpiredCount: 1 },
correlationId
);
return null;
}
this.performanceClient.incrementFields(
{ encryptedCacheCount: 1 },
correlationId
);
return invokeAsync(
decrypt,
BrowserPerformanceEvents.Decrypt,
this.logger,
this.performanceClient,
correlationId
)(
this.encryptionCookie.key,
encObj.nonce,
this.getContext(key),
encObj.data
);
}
/**
* Helper to decrypt and save an array of cache keys
* @param arr
* @returns Array of keys successfully imported
*/
private async importArray(
arr: Array<string>,
correlationId: string
): Promise<Array<string>> {
const importedArr: Array<string> = [];
const promiseArr: Array<Promise<void>> = [];
arr.forEach((key) => {
const promise = this.getItemFromEncryptedCache(
key,
correlationId
).then((value) => {
if (value) {
this.memoryStorage.setItem(key, value);
importedArr.push(key);
} else {
// If value is empty, unencrypted or expired remove
this.removeItem(key);
}
});
promiseArr.push(promise);
});
await Promise.all(promiseArr);
return importedArr;
}
/**
* Gets encryption context for a given cache entry. This is clientId for app specific entries, empty string for shared entries
* @param key
* @returns
*/
private getContext(key: string): string {
let context = "";
if (key.includes(this.clientId)) {
context = this.clientId; // Used to bind encryption key to this appId
}
return context;
}
private updateCache(event: MessageEvent, correlationId: string): void {
this.logger.trace(
"Updating internal cache from broadcast event",
correlationId
);
const perfMeasurement = this.performanceClient.startMeasurement(
BrowserRootPerformanceEvents.LocalStorageUpdated
);
perfMeasurement.add({ isBackground: true });
const { key, value, context } = event.data;
if (!key) {
this.logger.error("Broadcast event missing key", correlationId);
perfMeasurement.end({ success: false, errorCode: "noKey" });
return;
}
if (context && context !== this.clientId) {
this.logger.trace(
`Ignoring broadcast event from clientId: '${context}'`,
correlationId
);
perfMeasurement.end({
success: false,
errorCode: "contextMismatch",
});
return;
}
if (!value) {
this.memoryStorage.removeItem(key);
this.logger.verbose(
"Removed item from internal cache",
correlationId
);
} else {
this.memoryStorage.setItem(key, value);
this.logger.verbose(
"Updated item in internal cache",
correlationId
);
}
perfMeasurement.end({ success: true });
}
}

View File

@ -0,0 +1,59 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { IWindowStorage } from "./IWindowStorage.js";
export class MemoryStorage<T> implements IWindowStorage<T> {
private cache: Map<string, T>;
constructor() {
this.cache = new Map<string, T>();
}
async initialize(): Promise<void> {
// Memory storage does not require initialization
}
getItem(key: string): T | null {
return this.cache.get(key) || null;
}
getUserData(key: string): T | null {
return this.getItem(key);
}
setItem(key: string, value: T): void {
this.cache.set(key, value);
}
async setUserData(key: string, value: T): Promise<void> {
this.setItem(key, value);
}
removeItem(key: string): void {
this.cache.delete(key);
}
getKeys(): string[] {
const cacheKeys: string[] = [];
this.cache.forEach((value: T, key: string) => {
cacheKeys.push(key);
});
return cacheKeys;
}
containsKey(key: string): boolean {
return this.cache.has(key);
}
clear(): void {
this.cache.clear();
}
decryptData(): Promise<object | null> {
// Memory storage does not support encryption, so this method is a no-op
return Promise.resolve(null);
}
}

View File

@ -0,0 +1,57 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
BrowserConfigurationAuthErrorCodes,
createBrowserConfigurationAuthError,
} from "../error/BrowserConfigurationAuthError.js";
import { IWindowStorage } from "./IWindowStorage.js";
export class SessionStorage implements IWindowStorage<string> {
constructor() {
if (!window.sessionStorage) {
throw createBrowserConfigurationAuthError(
BrowserConfigurationAuthErrorCodes.storageNotSupported
);
}
}
async initialize(): Promise<void> {
// Session storage does not require initialization
}
getItem(key: string): string | null {
return window.sessionStorage.getItem(key);
}
getUserData(key: string): string | null {
return this.getItem(key);
}
setItem(key: string, value: string): void {
window.sessionStorage.setItem(key, value);
}
async setUserData(key: string, value: string): Promise<void> {
this.setItem(key, value);
}
removeItem(key: string): void {
window.sessionStorage.removeItem(key);
}
getKeys(): string[] {
return Object.keys(window.sessionStorage);
}
containsKey(key: string): boolean {
return window.sessionStorage.hasOwnProperty(key);
}
decryptData(): Promise<object | null> {
// Session storage does not support encryption, so this method is a no-op
return Promise.resolve(null);
}
}

View File

@ -0,0 +1,523 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
AccessTokenEntity,
AccountEntity,
AccountEntityUtils,
Authority,
AuthorityFactory,
AuthorityOptions,
AuthToken,
buildAccountToCache,
buildStaticAuthorityOptions,
CacheHelpers,
CacheRecord,
ExternalTokenResponse,
ICrypto,
IdTokenEntity,
invokeAsync,
IPerformanceClient,
Logger,
RefreshTokenEntity,
ScopeSet,
StubPerformanceClient,
TimeUtils,
TokenClaims,
} from "@azure/msal-common/browser";
import { buildConfiguration, Configuration } from "../config/Configuration.js";
import * as BrowserCrypto from "../crypto/BrowserCrypto.js";
import { CryptoOps } from "../crypto/CryptoOps.js";
import { base64Decode } from "../encode/Base64Decode.js";
import {
BrowserAuthErrorCodes,
createBrowserAuthError,
} from "../error/BrowserAuthError.js";
import { EventHandler } from "../event/EventHandler.js";
import type { SilentRequest } from "../request/SilentRequest.js";
import type { AuthenticationResult } from "../response/AuthenticationResult.js";
import * as BrowserPerformanceEvents from "../telemetry/BrowserPerformanceEvents.js";
import * as BrowserRootPerformanceEvents from "../telemetry/BrowserRootPerformanceEvents.js";
import { ApiId } from "../utils/BrowserConstants.js";
import * as BrowserUtils from "../utils/BrowserUtils.js";
import { BrowserCacheManager } from "./BrowserCacheManager.js";
export type LoadTokenOptions = {
clientInfo?: string;
expiresOn?: number;
extendedExpiresOn?: number;
};
/**
* API to load tokens to msal-browser cache.
* @param config - Object to configure the MSAL app.
* @param request - Silent request containing authority, scopes, and account.
* @param response - External token response to load into the cache.
* @param options - Options controlling how tokens are loaded into the cache.
* @param performanceClient - Optional performance client used for telemetry measurements.
* @returns `AuthenticationResult` for the response that was loaded.
*/
export async function loadExternalTokens(
config: Configuration,
request: SilentRequest,
response: ExternalTokenResponse,
options: LoadTokenOptions,
performanceClient: IPerformanceClient = new StubPerformanceClient()
): Promise<AuthenticationResult> {
BrowserUtils.blockNonBrowserEnvironment();
const browserConfig = buildConfiguration(config, true);
const correlationId =
request.correlationId || BrowserCrypto.createNewGuid();
const rootMeasurement = performanceClient.startMeasurement(
BrowserRootPerformanceEvents.LoadExternalTokens,
correlationId
);
try {
const idTokenClaims = response.id_token
? AuthToken.extractTokenClaims(response.id_token, base64Decode)
: undefined;
const kmsi = AuthToken.isKmsi(idTokenClaims || {});
const authorityOptions: AuthorityOptions = {
protocolMode: browserConfig.system.protocolMode,
knownAuthorities: browserConfig.auth.knownAuthorities,
cloudDiscoveryMetadata: browserConfig.auth.cloudDiscoveryMetadata,
authorityMetadata: browserConfig.auth.authorityMetadata,
};
const logger = new Logger(browserConfig.system.loggerOptions || {});
const cryptoOps = new CryptoOps(logger, browserConfig.telemetry.client);
const storage = new BrowserCacheManager(
browserConfig.auth.clientId,
browserConfig.cache,
cryptoOps,
logger,
browserConfig.telemetry.client,
new EventHandler(logger),
buildStaticAuthorityOptions(browserConfig.auth)
);
const authorityString =
request.authority || browserConfig.auth.authority;
const authority = await AuthorityFactory.createDiscoveredInstance(
Authority.generateAuthority(
authorityString,
request.azureCloudOptions
),
browserConfig.system.networkClient,
storage,
authorityOptions,
logger,
correlationId,
performanceClient
);
const cacheRecordAccount: AccountEntity = await invokeAsync(
loadAccount,
BrowserPerformanceEvents.LoadAccount,
logger,
performanceClient,
correlationId
)(
request,
options.clientInfo || response.client_info || "",
correlationId,
storage,
logger,
cryptoOps,
authority,
idTokenClaims,
performanceClient
);
const idToken = await invokeAsync(
loadIdToken,
BrowserPerformanceEvents.LoadIdToken,
logger,
performanceClient,
correlationId
)(
response,
cacheRecordAccount.homeAccountId,
cacheRecordAccount.environment,
cacheRecordAccount.realm,
kmsi,
correlationId,
storage,
logger,
config.auth.clientId
);
const accessToken = await invokeAsync(
loadAccessToken,
BrowserPerformanceEvents.LoadAccessToken,
logger,
performanceClient,
correlationId
)(
request,
response,
cacheRecordAccount.homeAccountId,
cacheRecordAccount.environment,
cacheRecordAccount.realm,
kmsi,
options,
correlationId,
storage,
logger,
config.auth.clientId
);
const refreshToken = await invokeAsync(
loadRefreshToken,
BrowserPerformanceEvents.LoadRefreshToken,
logger,
performanceClient,
correlationId
)(
response,
cacheRecordAccount.homeAccountId,
cacheRecordAccount.environment,
kmsi,
correlationId,
storage,
logger,
config.auth.clientId,
performanceClient
);
rootMeasurement.end(
{ success: true },
undefined,
AccountEntityUtils.getAccountInfo(cacheRecordAccount)
);
return generateAuthenticationResult(
request,
{
account: cacheRecordAccount,
idToken,
accessToken,
refreshToken,
},
authority,
idTokenClaims
);
} catch (error) {
rootMeasurement.end({ success: false }, error);
throw error;
}
}
/**
* Helper function to load account to msal-browser cache
* @param idToken
* @param environment
* @param clientInfo
* @param authorityType
* @param requestHomeAccountId
* @returns `AccountEntity`
*/
async function loadAccount(
request: SilentRequest,
clientInfo: string,
correlationId: string,
storage: BrowserCacheManager,
logger: Logger,
cryptoObj: ICrypto,
authority: Authority,
idTokenClaims?: TokenClaims,
performanceClient?: IPerformanceClient
): Promise<AccountEntity> {
logger.verbose("TokenCache - loading account", correlationId);
if (request.account) {
const accountEntity =
AccountEntityUtils.createAccountEntityFromAccountInfo(
request.account
);
await storage.setAccount(
accountEntity,
correlationId,
AuthToken.isKmsi(idTokenClaims || {}),
ApiId.loadExternalTokens
);
return accountEntity;
} else if (!clientInfo && !idTokenClaims) {
logger.error(
"TokenCache - if an account is not provided on the request, clientInfo or idToken must be provided instead.",
correlationId
);
throw createBrowserAuthError(BrowserAuthErrorCodes.unableToLoadToken);
}
const homeAccountId = AccountEntityUtils.generateHomeAccountId(
clientInfo,
authority.authorityType,
logger,
cryptoObj,
correlationId,
idTokenClaims
);
const claimsTenantId = idTokenClaims?.tid;
const cachedAccount = buildAccountToCache(
storage,
authority,
homeAccountId,
base64Decode,
correlationId,
idTokenClaims,
clientInfo,
authority.getPreferredCache(),
claimsTenantId,
undefined, // authCodePayload
undefined, // nativeAccountId
logger,
performanceClient
);
await storage.setAccount(
cachedAccount,
correlationId,
AuthToken.isKmsi(idTokenClaims || {}),
ApiId.loadExternalTokens
);
return cachedAccount;
}
/**
* Helper function to load id tokens to msal-browser cache
* @param idToken
* @param homeAccountId
* @param environment
* @param tenantId
* @returns `IdTokenEntity`
*/
async function loadIdToken(
response: ExternalTokenResponse,
homeAccountId: string,
environment: string,
tenantId: string,
kmsi: boolean,
correlationId: string,
storage: BrowserCacheManager,
logger: Logger,
clientId: string
): Promise<IdTokenEntity | null> {
if (!response.id_token) {
logger.verbose(
"TokenCache - no id token found in response",
correlationId
);
return null;
}
logger.verbose("TokenCache - loading id token", correlationId);
const idTokenEntity = CacheHelpers.createIdTokenEntity(
homeAccountId,
environment,
response.id_token,
clientId,
tenantId
);
await storage.setIdTokenCredential(idTokenEntity, correlationId, kmsi);
return idTokenEntity;
}
/**
* Helper function to load access tokens to msal-browser cache
* @param request
* @param response
* @param homeAccountId
* @param environment
* @param tenantId
* @returns `AccessTokenEntity`
*/
async function loadAccessToken(
request: SilentRequest,
response: ExternalTokenResponse,
homeAccountId: string,
environment: string,
tenantId: string,
kmsi: boolean,
options: LoadTokenOptions,
correlationId: string,
storage: BrowserCacheManager,
logger: Logger,
clientId: string
): Promise<AccessTokenEntity | null> {
if (!response.access_token) {
logger.verbose(
"TokenCache - no access token found in response",
correlationId
);
return null;
} else if (!response.expires_in) {
logger.error(
"TokenCache - no expiration set on the access token. Cannot add it to the cache.",
correlationId
);
return null;
} else if (!response.scope && (!request.scopes || !request.scopes.length)) {
logger.error(
"TokenCache - scopes not specified in the request or response. Cannot add token to the cache.",
correlationId
);
return null;
}
logger.verbose("TokenCache - loading access token", correlationId);
const scopes = response.scope
? ScopeSet.fromString(response.scope)
: new ScopeSet(request.scopes);
const expiresOn =
options.expiresOn || response.expires_in + TimeUtils.nowSeconds();
const extendedExpiresOn =
options.extendedExpiresOn ||
(response.ext_expires_in || response.expires_in) +
TimeUtils.nowSeconds();
const accessTokenEntity = CacheHelpers.createAccessTokenEntity(
homeAccountId,
environment,
response.access_token,
clientId,
tenantId,
scopes.printScopes(),
expiresOn,
extendedExpiresOn,
base64Decode
);
await storage.setAccessTokenCredential(
accessTokenEntity,
correlationId,
kmsi
);
return accessTokenEntity;
}
/**
* Helper function to load refresh tokens to msal-browser cache
* @param request
* @param response
* @param homeAccountId
* @param environment
* @returns `RefreshTokenEntity`
*/
async function loadRefreshToken(
response: ExternalTokenResponse,
homeAccountId: string,
environment: string,
kmsi: boolean,
correlationId: string,
storage: BrowserCacheManager,
logger: Logger,
clientId: string,
performanceClient: IPerformanceClient
): Promise<RefreshTokenEntity | null> {
if (!response.refresh_token) {
logger.verbose(
"TokenCache - no refresh token found in response",
correlationId
);
return null;
}
const expiresOn = response.refresh_token_expires_in
? response.refresh_token_expires_in + TimeUtils.nowSeconds()
: undefined;
performanceClient.addFields(
{
extRtExpiresOnSeconds: expiresOn,
},
correlationId
);
logger.verbose("TokenCache - loading refresh token", correlationId);
const refreshTokenEntity = CacheHelpers.createRefreshTokenEntity(
homeAccountId,
environment,
response.refresh_token,
clientId,
response.foci,
undefined, // userAssertionHash
expiresOn
);
await storage.setRefreshTokenCredential(
refreshTokenEntity,
correlationId,
kmsi
);
return refreshTokenEntity;
}
/**
* Helper function to generate an `AuthenticationResult` for the result.
* @param request
* @param idTokenObj
* @param cacheRecord
* @param authority
* @returns `AuthenticationResult`
*/
function generateAuthenticationResult(
request: SilentRequest,
cacheRecord: CacheRecord & { account: AccountEntity },
authority: Authority,
idTokenClaims?: TokenClaims
): AuthenticationResult {
let accessToken: string = "";
let responseScopes: Array<string> = [];
let expiresOn: Date | null = null;
let extExpiresOn: Date | undefined;
if (cacheRecord?.accessToken) {
accessToken = cacheRecord.accessToken.secret;
responseScopes = ScopeSet.fromString(
cacheRecord.accessToken.target
).asArray();
// Access token expiresOn stored in seconds, converting to Date for AuthenticationResult
expiresOn = TimeUtils.toDateFromSeconds(
cacheRecord.accessToken.expiresOn
);
extExpiresOn = TimeUtils.toDateFromSeconds(
cacheRecord.accessToken.extendedExpiresOn
);
}
const accountEntity = cacheRecord.account;
return {
authority: authority.canonicalAuthority,
uniqueId: cacheRecord.account.localAccountId,
tenantId: cacheRecord.account.realm,
scopes: responseScopes,
account: AccountEntityUtils.getAccountInfo(accountEntity),
idToken: cacheRecord.idToken?.secret || "",
idTokenClaims: idTokenClaims || {},
accessToken: accessToken,
fromCache: true,
expiresOn: expiresOn,
correlationId: request.correlationId || "",
requestId: "",
extExpiresOn: extExpiresOn,
familyId: cacheRecord.refreshToken?.familyId || "",
tokenType: cacheRecord?.accessToken?.tokenType || "",
state: request.state || "",
cloudGraphHostName: accountEntity.cloudGraphHostName || "",
msGraphHost: accountEntity.msGraphHost || "",
fromPlatformBroker: false,
};
}