Skip to content

Commit

Permalink
Supports caching in NAA apps (#7072)
Browse files Browse the repository at this point in the history
NAA apps currently always request the bridge for tokens, adding load to
the host caching hence blocking the deployment on scale. This PR adds
support for:

* Caching NAA app tokens (idToken and accessToken) in the naa app
storage (based on `cacheConfig`)
* Allows for NAA app to set `initContext` with `accountContext` to look
for specific account in cache when available.

MetaOS apps create and delete the NAA apps independent of the session.
Hence, we always assume the Bridge will add the account context and only
look for those tokens. If the account is not found, we always go for the
hub to fetch tokens.

**Minor additions:**
* AccountManager created to not duplicate account lookup code and make
them functions (saving size).
* If a token is expired while cache lookup, it is cleaned.

**Note and TBD:** 
Since there is no `logout` and naa app life cycle does not map to
session life time, cache clean up is not comprehensive. However, this PR
clears the cache when a token is identified as expired. The logout
equivalent cache clearance (if the parent logs out, nested app should
clean up cache) will be handled as a separate use case.

---------

Co-authored-by: Thomas Norling <[email protected]>
  • Loading branch information
sameerag and tnorling authored May 22, 2024
1 parent adaa895 commit 0db4239
Show file tree
Hide file tree
Showing 17 changed files with 837 additions and 223 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "Support cache in NAA apps #7072",
"packageName": "@azure/msal-browser",
"email": "[email protected]",
"dependentChangeType": "patch"
}
2 changes: 1 addition & 1 deletion lib/msal-browser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@
"jest": "^29.5.0",
"jest-environment-jsdom": "^29.5.0",
"msal-test-utils": "file:../../shared-test-utils",
"prettier": "2.8.7",
"prettier": "^2.8.7",
"rimraf": "^3.0.0",
"rollup": "^3.14.0",
"shx": "^0.3.2",
Expand Down
184 changes: 184 additions & 0 deletions lib/msal-browser/src/cache/AccountManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/

import { AccountInfo, AccountFilter, Logger } from "@azure/msal-common";
import { BrowserCacheManager } from "./BrowserCacheManager";

/**
* 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,
accountFilter?: AccountFilter
): AccountInfo[] {
logger.verbose("getAllAccounts called");
return isInBrowser ? browserStorage.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,
browserStorage: BrowserCacheManager
): AccountInfo | null {
logger.trace("getAccount called");
if (Object.keys(accountFilter).length === 0) {
logger.warning("getAccount: No accountFilter provided");
return null;
}

const account: AccountInfo | null =
browserStorage.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,
browserStorage: BrowserCacheManager
): AccountInfo | null {
logger.trace("getAccountByUsername called");
if (!username) {
logger.warning("getAccountByUsername: No username provided");
return null;
}

const account = browserStorage.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,
browserStorage: BrowserCacheManager
): AccountInfo | null {
logger.trace("getAccountByHomeId called");
if (!homeAccountId) {
logger.warning("getAccountByHomeId: No homeAccountId provided");
return null;
}

const account = browserStorage.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,
browserStorage: BrowserCacheManager
): AccountInfo | null {
logger.trace("getAccountByLocalId called");
if (!localAccountId) {
logger.warning("getAccountByLocalId: No localAccountId provided");
return null;
}

const account = browserStorage.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,
browserStorage: BrowserCacheManager
): void {
browserStorage.setActiveAccount(account);
}

/**
* Gets the currently active account
*/
export function getActiveAccount(
browserStorage: BrowserCacheManager
): AccountInfo | null {
return browserStorage.getActiveAccount();
}
Loading

0 comments on commit 0db4239

Please sign in to comment.