Skip to content

Commit 392020b

Browse files
hectormmgsameeragtnorling
authored
Add support for Multi-tenant accounts and cross-tenant token caching (#6536)
This PR: - Adds `tenantProfiles` map to `AccountInfo` and `tenantProfiles` array (serialization) to `AccountEntity` - Adds `realm` filtering to `getIdToken` and `getAccessToken` so cached tokens are only matched if they were issued by the tenant in the request's authority - Updates cached account setting logic to cache `AccountEntity` objects with the available data of the home tenant profile in client_info (localAccountId = uid, realm = utid). - Adds logic to update the `AccountInfo` objects that `getAccount` APIs return to reflect the tenant-specific data (tenant profile) in the ID token claims from the ID token that matches the account filter passed in - Adds logic to `getAllAccounts` to expand and return all tenant profiles that match the filter into full `AccountInfo` objects to maintain backward compatibility - Sets access token and ID token realm values to the best available value from ID token claims with precedence tid > tfp > acr to cover AAD and B2C cases --------- Co-authored-by: Sameera Gajjarapu <sameera.gajjarapu@microsoft.com> Co-authored-by: Thomas Norling <thomas.norling@microsoft.com>
1 parent 67fce83 commit 392020b

File tree

87 files changed

+3630
-2047
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

87 files changed

+3630
-2047
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "minor",
3+
"comment": "Add support for Multi-tenant accounts and cross-tenant token caching #6466",
4+
"packageName": "@azure/msal-browser",
5+
"email": "hemoral@microsoft.com",
6+
"dependentChangeType": "patch"
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "minor",
3+
"comment": "Add support for Multi-tenant accounts and cross-tenant token caching #6466",
4+
"packageName": "@azure/msal-common",
5+
"email": "hemoral@microsoft.com",
6+
"dependentChangeType": "patch"
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "minor",
3+
"comment": "Add support for Multi-tenant accounts and cross-tenant token caching #6466",
4+
"packageName": "@azure/msal-node",
5+
"email": "hemoral@microsoft.com",
6+
"dependentChangeType": "patch"
7+
}

lib/msal-browser/docs/accounts.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Accounts in MSAL Browser
22

3-
> This is the platform-specific Accounts documentation for `@azure/msal-browser`. For the general documentation of the `AccountInfo` object structure, please visit the `@azure/msal-common` [Accounts document](../../msal-common/docs/Accounts.md).
3+
> This is the platform-specific Accounts documentation for `@azure/msal-browser`. For the general documentation of the `AccountInfo` object structure, please visit the `@azure/msal-common` [Accounts document](../../msal-common/docs/Accounts.md). For documentation relating to multi-tenant accounts, please visit the [Multi-tenant Accounts document](../../msal-common/docs/multi-tenant-accounts.md).
44
55
## Usage
66

lib/msal-browser/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
"@types/sinon": "^7.5.0",
8787
"dotenv": "^8.2.0",
8888
"eslint-config-msal": "^0.0.0",
89+
"msal-test-utils": "^0.0.1",
8990
"fake-indexeddb": "^3.1.3",
9091
"jest": "^29.5.0",
9192
"jest-environment-jsdom": "^29.5.0",

lib/msal-browser/src/cache/BrowserCacheManager.ts

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -394,15 +394,31 @@ export class BrowserCacheManager extends CacheManager {
394394
* fetch the account entity from the platform cache
395395
* @param accountKey
396396
*/
397-
getAccount(accountKey: string): AccountEntity | null {
397+
getAccount(accountKey: string, logger?: Logger): AccountEntity | null {
398398
this.logger.trace("BrowserCacheManager.getAccount called");
399-
const account = this.getItem(accountKey);
400-
if (!account) {
399+
const accountEntity = this.getCachedAccountEntity(accountKey);
400+
401+
return this.updateOutdatedCachedAccount(
402+
accountKey,
403+
accountEntity,
404+
logger
405+
);
406+
}
407+
408+
/**
409+
* Reads account from cache, deserializes it into an account entity and returns it.
410+
* If account is not found from the key, returns null and removes key from map.
411+
* @param accountKey
412+
* @returns
413+
*/
414+
getCachedAccountEntity(accountKey: string): AccountEntity | null {
415+
const serializedAccount = this.getItem(accountKey);
416+
if (!serializedAccount) {
401417
this.removeAccountKeyFromMap(accountKey);
402418
return null;
403419
}
404420

405-
const parsedAccount = this.validateAndParseJson(account);
421+
const parsedAccount = this.validateAndParseJson(serializedAccount);
406422
if (!parsedAccount || !AccountEntity.isAccountEntity(parsedAccount)) {
407423
this.removeAccountKeyFromMap(accountKey);
408424
return null;
@@ -505,6 +521,15 @@ export class BrowserCacheManager extends CacheManager {
505521
this.removeAccountKeyFromMap(key);
506522
}
507523

524+
/**
525+
* Remove account entity from the platform cache if it's outdated
526+
* @param accountKey
527+
*/
528+
removeOutdatedAccount(accountKey: string): void {
529+
this.removeItem(accountKey);
530+
this.removeAccountKeyFromMap(accountKey);
531+
}
532+
508533
/**
509534
* Removes given idToken from the cache and from the key map
510535
* @param key
@@ -1034,6 +1059,7 @@ export class BrowserCacheManager extends CacheManager {
10341059
return this.getAccountInfoFilteredBy({
10351060
homeAccountId: activeAccountValueObj.homeAccountId,
10361061
localAccountId: activeAccountValueObj.localAccountId,
1062+
tenantId: activeAccountValueObj.tenantId,
10371063
});
10381064
}
10391065
this.logger.trace(
@@ -1058,6 +1084,7 @@ export class BrowserCacheManager extends CacheManager {
10581084
const activeAccountValue: ActiveAccountFilters = {
10591085
homeAccountId: account.homeAccountId,
10601086
localAccountId: account.localAccountId,
1087+
tenantId: account.tenantId,
10611088
};
10621089
this.browserStorage.setItem(
10631090
activeAccountKey,

lib/msal-browser/src/cache/TokenCache.ts

Lines changed: 33 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
CacheRecord,
2020
TokenClaims,
2121
CacheHelpers,
22+
buildAccountToCache,
2223
} from "@azure/msal-common";
2324
import { BrowserConfiguration } from "../config/Configuration";
2425
import { SilentRequest } from "../request/SilentRequest";
@@ -240,40 +241,43 @@ export class TokenCache implements ITokenCache {
240241
clientInfo?: string,
241242
requestHomeAccountId?: string
242243
): AccountEntity {
243-
let homeAccountId;
244-
if (requestHomeAccountId) {
245-
homeAccountId = requestHomeAccountId;
246-
} else if (authority.authorityType !== undefined && clientInfo) {
247-
homeAccountId = AccountEntity.generateHomeAccountId(
248-
clientInfo,
249-
authority.authorityType,
250-
this.logger,
251-
this.cryptoObj,
252-
idTokenClaims
253-
);
254-
}
244+
if (this.isBrowserEnvironment) {
245+
this.logger.verbose("TokenCache - loading account");
246+
let homeAccountId;
247+
if (requestHomeAccountId) {
248+
homeAccountId = requestHomeAccountId;
249+
} else if (authority.authorityType !== undefined && clientInfo) {
250+
homeAccountId = AccountEntity.generateHomeAccountId(
251+
clientInfo,
252+
authority.authorityType,
253+
this.logger,
254+
this.cryptoObj,
255+
idTokenClaims
256+
);
257+
}
255258

256-
if (!homeAccountId) {
257-
throw createBrowserAuthError(
258-
BrowserAuthErrorCodes.unableToLoadToken
259-
);
260-
}
259+
if (!homeAccountId) {
260+
throw createBrowserAuthError(
261+
BrowserAuthErrorCodes.unableToLoadToken
262+
);
263+
}
264+
const claimsTenantId = idTokenClaims.tid;
261265

262-
const accountEntity = AccountEntity.createAccount(
263-
{
266+
const cachedAccount = buildAccountToCache(
267+
this.storage,
268+
authority,
264269
homeAccountId,
265-
idTokenClaims: idTokenClaims,
270+
idTokenClaims,
271+
base64Decode,
266272
clientInfo,
267-
environment: authority.hostnameAndPort,
268-
},
269-
authority
270-
);
271-
272-
if (this.isBrowserEnvironment) {
273-
this.logger.verbose("TokenCache - loading account");
273+
claimsTenantId,
274+
undefined,
275+
undefined,
276+
this.logger
277+
);
274278

275-
this.storage.setAccount(accountEntity);
276-
return accountEntity;
279+
this.storage.setAccount(cachedAccount);
280+
return cachedAccount;
277281
} else {
278282
throw createBrowserAuthError(
279283
BrowserAuthErrorCodes.unableToLoadToken

lib/msal-browser/src/controllers/StandardController.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1549,7 +1549,7 @@ export class StandardController implements IController {
15491549
): string {
15501550
const account =
15511551
request.account ||
1552-
this.browserStorage.getAccountInfoFilteredBy({
1552+
this.getAccount({
15531553
loginHint: request.loginHint,
15541554
sid: request.sid,
15551555
}) ||

lib/msal-browser/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ export {
146146
PerformanceEvents,
147147
// Telemetry
148148
InProgressPerformanceEvent,
149+
TenantProfile,
149150
} from "@azure/msal-common";
150151

151152
export { version } from "./packageMetadata";

lib/msal-browser/src/interaction_client/NativeInteractionClient.ts

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@ import {
3333
invokeAsync,
3434
createAuthError,
3535
AuthErrorCodes,
36+
updateAccountTenantProfileData,
3637
CacheHelpers,
38+
buildAccountToCache,
3739
} from "@azure/msal-common";
3840
import { BaseInteractionClient } from "./BaseInteractionClient";
3941
import { BrowserConfiguration } from "../config/Configuration";
@@ -420,28 +422,32 @@ export class NativeInteractionClient extends BaseInteractionClient {
420422
response,
421423
idTokenClaims
422424
);
423-
const accountEntity = AccountEntity.createAccount(
424-
{
425-
homeAccountId: homeAccountIdentifier,
426-
idTokenClaims: idTokenClaims,
427-
clientInfo: response.client_info,
428-
nativeAccountId: response.account.id,
429-
},
430-
authority
425+
426+
const baseAccount = buildAccountToCache(
427+
this.browserStorage,
428+
authority,
429+
homeAccountIdentifier,
430+
idTokenClaims,
431+
base64Decode,
432+
response.client_info,
433+
idTokenClaims.tid,
434+
undefined,
435+
response.account.id,
436+
this.logger
431437
);
432438

433439
// generate authenticationResult
434440
const result = await this.generateAuthenticationResult(
435441
response,
436442
request,
437443
idTokenClaims,
438-
accountEntity,
444+
baseAccount,
439445
authority.canonicalAuthority,
440446
reqTimestamp
441447
);
442448

443449
// cache accounts and tokens in the appropriate storage
444-
this.cacheAccount(accountEntity);
450+
this.cacheAccount(baseAccount);
445451
this.cacheNativeTokens(
446452
response,
447453
request,
@@ -580,14 +586,11 @@ export class NativeInteractionClient extends BaseInteractionClient {
580586
idTokenClaims.tid ||
581587
Constants.EMPTY_STRING;
582588

583-
const fullAccountEntity: AccountEntity = idTokenClaims
584-
? Object.assign(new AccountEntity(), {
585-
...accountEntity,
586-
idTokenClaims: idTokenClaims,
587-
})
588-
: accountEntity;
589-
590-
const accountInfo = fullAccountEntity.getAccountInfo();
589+
const accountInfo: AccountInfo | null = updateAccountTenantProfileData(
590+
accountEntity.getAccountInfo(),
591+
undefined, // tenantProfile optional
592+
idTokenClaims
593+
);
591594

592595
// generate PoP token as needed
593596
const responseAccessToken = await this.generatePopAccessToken(

0 commit comments

Comments
 (0)