Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions lib/msal-browser/src/app/PublicClientNext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,18 @@ export class PublicClientNext implements IPublicClientApplication {
return this.controller.getAllAccounts(accountFilter);
}

/**
* 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
*/
async getAllAccountsAsync(accountFilter?: AccountFilter): Promise<AccountInfo[]> {
if (typeof this.controller.getAllAccountsAsync === "function") {
return this.controller.getAllAccountsAsync(accountFilter);
}
return Promise.resolve([]);
}

/**
* Event handler function which allows users to fire events after the PublicClientApplication object
* has loaded during redirect flows. This should be invoked on all page loads involved in redirect
Expand Down
192 changes: 192 additions & 0 deletions lib/msal-browser/src/cache/AsyncAccountManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/

import { AccountInfo, AccountFilter, Logger } from "@azure/msal-common/browser";
import { WorkerCacheManager } from "./WorkerCacheManager.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 async function getAllAccounts(
logger: Logger,
workerStorage: WorkerCacheManager,
accountFilter?: AccountFilter
): Promise<AccountInfo[]> {
logger.verbose("getAllAccounts called");
return workerStorage.getAllAccounts(accountFilter);
}

// /**
// * 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,
// workerStorage: WorkerCacheManager
// ): AccountInfo | null {
// logger.trace("getAccount called");
// if (Object.keys(accountFilter).length === 0) {
// logger.warning("getAccount: No accountFilter provided");
// return null;
// }

/*
* const account: AccountInfo | null =
* workerStorage.getAccountInfoFilteredBy(accountFilter);
*/

/*
* if (account) {
* logger.verbose(
* "getAccount: Account matching provided filter found, returning"
* );
* return account;
* } else {
* logger.verbose("getAccount: No matching account found, returning null");
* return null;
* }
* }
*/

// /**
// * Returns the signed in account matching username.
// * (the account object is created at the time of successful login)
// * or null when no matching account is found.
// * This API is provided for convenience but getAccountById should be used for best reliability
// * @param username
// * @returns The account object stored in MSAL
// */
// export function getAccountByUsername(
// username: string,
// logger: Logger,
// workerStorage: WorkerCacheManager
// ): AccountInfo | null {
// logger.trace("getAccountByUsername called");
// if (!username) {
// logger.warning("getAccountByUsername: No username provided");
// return null;
// }

/*
* const account = workerStorage.getAccountInfoFilteredBy({
* username,
* });
* if (account) {
* logger.verbose(
* "getAccountByUsername: Account matching username found, returning"
* );
* logger.verbosePii(
* `getAccountByUsername: Returning signed-in accounts matching username: ${username}`
* );
* return account;
* } else {
* logger.verbose(
* "getAccountByUsername: No matching account found, returning null"
* );
* return null;
* }
* }
*/

// /**
// * Returns the signed in account matching homeAccountId.
// * (the account object is created at the time of successful login)
// * or null when no matching account is found
// * @param homeAccountId
// * @returns The account object stored in MSAL
// */
// export function getAccountByHomeId(
// homeAccountId: string,
// logger: Logger,
// workerStorage: WorkerCacheManager
// ): AccountInfo | null {
// logger.trace("getAccountByHomeId called");
// if (!homeAccountId) {
// logger.warning("getAccountByHomeId: No homeAccountId provided");
// return null;
// }

/*
* const account = workerStorage.getAccountInfoFilteredBy({
* homeAccountId,
* });
* if (account) {
* logger.verbose(
* "getAccountByHomeId: Account matching homeAccountId found, returning"
* );
* logger.verbosePii(
* `getAccountByHomeId: Returning signed-in accounts matching homeAccountId: ${homeAccountId}`
* );
* return account;
* } else {
* logger.verbose(
* "getAccountByHomeId: No matching account found, returning null"
* );
* return null;
* }
* }
*/

// /**
// * Returns the signed in account matching localAccountId.
// * (the account object is created at the time of successful login)
// * or null when no matching account is found
// * @param localAccountId
// * @returns The account object stored in MSAL
// */
// export function getAccountByLocalId(
// localAccountId: string,
// logger: Logger,
// workerStorage: WorkerCacheManager
// ): AccountInfo | null {
// logger.trace("getAccountByLocalId called");
// if (!localAccountId) {
// logger.warning("getAccountByLocalId: No localAccountId provided");
// return null;
// }

/*
* const account = workerStorage.getAccountInfoFilteredBy({
* localAccountId,
* });
* if (account) {
* logger.verbose(
* "getAccountByLocalId: Account matching localAccountId found, returning"
* );
* logger.verbosePii(
* `getAccountByLocalId: Returning signed-in accounts matching localAccountId: ${localAccountId}`
* );
* return account;
* } else {
* logger.verbose(
* "getAccountByLocalId: No matching account found, returning null"
* );
* 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,
// workerStorage: WorkerCacheManager
// ): void {
// workerStorage.setActiveAccount(account);
// }

// /**
// * Gets the currently active account
// */
// export function getActiveAccount(
// workerStorage: WorkerCacheManager
// ): AccountInfo | null {
// return workerStorage.getActiveAccount();
// }
54 changes: 52 additions & 2 deletions lib/msal-browser/src/cache/AsyncMemoryStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Licensed under the MIT License.
*/

import { Logger } from "@azure/msal-common/browser";
import { IPerformanceClient, Logger, PerformanceEvents } from "@azure/msal-common/browser";
import {
BrowserAuthError,
BrowserAuthErrorCodes,
Expand All @@ -12,21 +12,36 @@ import { DatabaseStorage } from "./DatabaseStorage.js";
import { IAsyncStorage } from "./IAsyncStorage.js";
import { MemoryStorage } from "./MemoryStorage.js";

const BROADCAST_CHANNEL_NAME = "msal.broadcast.cache"; // TODO: Dedupe

/**
* 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 clientId: string | undefined;
private inMemoryCache: MemoryStorage<T>;
private indexedDBCache: DatabaseStorage<T>;
private logger: Logger;
private initialized: boolean = false;
private performanceClient: IPerformanceClient | undefined;
private broadcast: BroadcastChannel;

constructor(logger: Logger) {
constructor(logger: Logger, clientId?: string, performanceClient?: IPerformanceClient) {
this.inMemoryCache = new MemoryStorage<T>();
this.indexedDBCache = new DatabaseStorage<T>();
this.clientId = clientId;
this.logger = logger;
this.performanceClient = performanceClient;
this.broadcast = new BroadcastChannel(BROADCAST_CHANNEL_NAME);
}

async initialize(): Promise<void> {
this.initialized = true;
// Register listener for cache updates in other tabs
this.broadcast.addEventListener("message", this.updateCache.bind(this));
};

private handleDatabaseAccessError(error: unknown): void {
if (
error instanceof BrowserAuthError &&
Expand Down Expand Up @@ -153,4 +168,39 @@ export class AsyncMemoryStorage<T> implements IAsyncStorage<T> {
return false;
}
}

private updateCache(event: MessageEvent): void {
this.logger.trace("Updating internal cache from broadcast event");
const perfMeasurement = this.performanceClient?.startMeasurement(
PerformanceEvents.LocalStorageUpdated
);
perfMeasurement?.add({ isBackground: true });

const { key, value, context } = event.data;
if (!key) {
this.logger.error("Broadcast event missing key");
perfMeasurement?.end({ success: false, errorCode: "noKey" });
return;
}

if (context && context !== this.clientId) {
this.logger.trace(
`Ignoring broadcast event from clientId: ${context}`
);
perfMeasurement?.end({
success: false,
errorCode: "contextMismatch",
});
return;
}

if (!value) {
this.inMemoryCache.removeItem(key);
this.logger.verbose("Removed item from internal cache");
} else {
this.inMemoryCache.setItem(key, value);
this.logger.verbose("Updated item in internal cache");
}
perfMeasurement?.end({ success: true });
}
}
40 changes: 40 additions & 0 deletions lib/msal-browser/src/cache/CacheHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import { TokenKeys } from "@azure/msal-common/browser";
import { StaticCacheKeys } from "../utils/BrowserConstants.js";
import { IWindowStorage } from "./IWindowStorage.js";
import { IAsyncStorage } from "./IAsyncStorage.js";

/**
* Returns a list of cache keys for all known accounts
Expand All @@ -21,6 +22,15 @@ export function getAccountKeys(storage: IWindowStorage<string>): Array<string> {
return [];
}

export async function getAccountKeysAsync(storage: IAsyncStorage<string>): Promise<Array<string>> {
const accountKeys = await storage.getItem(StaticCacheKeys.ACCOUNT_KEYS);
if (accountKeys) {
return JSON.parse(accountKeys);
}

return [];
}

/**
* Returns a list of cache keys for all known tokens
* @param clientId
Expand Down Expand Up @@ -50,3 +60,33 @@ export function getTokenKeys(
refreshToken: [],
};
}

/**
* Returns a list of cache keys for all known tokens
* @param clientId
* @param storage
* @returns
*/
export async function getTokenKeysAsync(
clientId: string,
storage: IAsyncStorage<string>
): Promise<TokenKeys> {
const item = await storage.getItem(`${StaticCacheKeys.TOKEN_KEYS}.${clientId}`);
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: [],
};
}
12 changes: 11 additions & 1 deletion lib/msal-browser/src/cache/DatabaseStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,22 @@ export class DatabaseStorage<T> implements IAsyncStorage<T> {
this.dbOpen = false;
}

async initialize(): Promise<void> {
}

/**
* Opens IndexedDB instance.
*/
async open(): Promise<void> {
return new Promise((resolve, reject) => {
const openDB = window.indexedDB.open(this.dbName, this.version);

let openDB = null;
if(typeof window !== "undefined" && window.indexedDB) {
openDB = window.indexedDB.open(this.dbName, this.version);
} else {
openDB = self.indexedDB.open(this.dbName, this.version);
}

openDB.addEventListener(
"upgradeneeded",
(e: IDBVersionChangeEvent) => {
Expand Down
5 changes: 5 additions & 0 deletions lib/msal-browser/src/cache/IAsyncStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
*/

export interface IAsyncStorage<T> {
/**
* Async initializer
*/

initialize(): Promise<void>;
/**
* Get the item from the asynchronous storage object matching the given key.
* @param key
Expand Down
Loading