From 33d94a692df69eb0c769880cf525ba364b6dda93 Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Sat, 30 Sep 2023 15:36:46 -0700 Subject: [PATCH 01/91] Remove ID token claims from account entity before caching --- lib/msal-common/src/cache/CacheManager.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/msal-common/src/cache/CacheManager.ts b/lib/msal-common/src/cache/CacheManager.ts index 928e8a403b..9933775db7 100644 --- a/lib/msal-common/src/cache/CacheManager.ts +++ b/lib/msal-common/src/cache/CacheManager.ts @@ -302,6 +302,8 @@ export abstract class CacheManager implements ICacheManager { } if (!!cacheRecord.account) { + // Remove ID token claims before saving account entity + cacheRecord.account.idTokenClaims = undefined; this.setAccount(cacheRecord.account); } @@ -853,6 +855,7 @@ export abstract class CacheManager implements ICacheManager { environment: string ): CacheRecord { const tokenKeys = this.getTokenKeys(); + debugger; const cachedAccount = this.readAccountFromCache(account); const cachedIdToken = this.getIdToken(account, tokenKeys); const cachedAccessToken = this.getAccessToken( From 8b7e6ade8b3d5dca734fc6fd97252c960e89e75c Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Sat, 30 Sep 2023 15:51:06 -0700 Subject: [PATCH 02/91] Refactor getAllAccounts to remove duplicated logic --- lib/msal-common/src/cache/CacheManager.ts | 29 +---------------------- 1 file changed, 1 insertion(+), 28 deletions(-) diff --git a/lib/msal-common/src/cache/CacheManager.ts b/lib/msal-common/src/cache/CacheManager.ts index 9933775db7..caccb36029 100644 --- a/lib/msal-common/src/cache/CacheManager.ts +++ b/lib/msal-common/src/cache/CacheManager.ts @@ -226,38 +226,11 @@ export abstract class CacheManager implements ICacheManager { * @returns Array of AccountInfo objects in cache */ getAllAccounts(accountFilter?: AccountFilter): AccountInfo[] { - if (accountFilter) { - return this.getAccountsFilteredBy(accountFilter).map( - (accountEntity) => { - return accountEntity.getAccountInfo(); - } - ); - } - - const allAccountKeys = this.getAccountKeys(); - if (allAccountKeys.length < 1) { - return []; - } - - const accountEntities: AccountEntity[] = allAccountKeys.reduce( - (accounts: AccountEntity[], key: string) => { - const entity: AccountEntity | null = this.getAccount(key); - - if (!entity) { - return accounts; - } - accounts.push(entity); - return accounts; - }, - [] - ); - - const allAccounts = accountEntities.map( + return this.getAccountsFilteredBy(accountFilter || {}).map( (accountEntity) => { return this.getAccountInfoFromEntity(accountEntity); } ); - return allAccounts; } /** From 13b2f8bdaa2ddb8ab772d5d0733d1129ec787008 Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Sun, 1 Oct 2023 12:09:21 -0700 Subject: [PATCH 03/91] Persist authority metadata in temporary cache to make it available for ID token matching --- .../src/cache/BrowserCacheManager.ts | 7 ++-- lib/msal-common/src/cache/CacheManager.ts | 15 +++++++- .../test/cache/CacheManager.spec.ts | 9 +++++ lib/msal-common/test/cache/MockCache.ts | 38 +++++++++++++++++++ 4 files changed, 65 insertions(+), 4 deletions(-) diff --git a/lib/msal-browser/src/cache/BrowserCacheManager.ts b/lib/msal-browser/src/cache/BrowserCacheManager.ts index 664a937c55..5a05b6176b 100644 --- a/lib/msal-browser/src/cache/BrowserCacheManager.ts +++ b/lib/msal-browser/src/cache/BrowserCacheManager.ts @@ -934,7 +934,7 @@ export class BrowserCacheManager extends CacheManager { * */ getAuthorityMetadata(key: string): AuthorityMetadataEntity | null { - const value = this.internalStorage.getItem(key); + const value = this.temporaryCacheStorage.getItem(key); if (!value) { this.logger.trace( "BrowserCacheManager.getAuthorityMetadata: called, no cache hit" @@ -964,7 +964,7 @@ export class BrowserCacheManager extends CacheManager { * */ getAuthorityMetadataKeys(): Array { - const allKeys = this.internalStorage.getKeys(); + const allKeys = this.temporaryCacheStorage.getKeys(); return allKeys.filter((key) => { return this.isAuthorityMetadata(key); }); @@ -1002,7 +1002,7 @@ export class BrowserCacheManager extends CacheManager { */ setAuthorityMetadata(key: string, entity: AuthorityMetadataEntity): void { this.logger.trace("BrowserCacheManager.setAuthorityMetadata called"); - this.internalStorage.setItem(key, JSON.stringify(entity)); + this.temporaryCacheStorage.setItem(key, JSON.stringify(entity)); } /** @@ -1343,6 +1343,7 @@ export class BrowserCacheManager extends CacheManager { // Removes all accounts and their credentials await this.removeAllAccounts(); this.removeAppMetadata(); + this.removeAuthorityMetadata(); // Removes all remaining MSAL cache items this.getKeys().forEach((cacheKey: string) => { diff --git a/lib/msal-common/src/cache/CacheManager.ts b/lib/msal-common/src/cache/CacheManager.ts index caccb36029..7ebae3457b 100644 --- a/lib/msal-common/src/cache/CacheManager.ts +++ b/lib/msal-common/src/cache/CacheManager.ts @@ -814,6 +814,20 @@ export abstract class CacheManager implements ICacheManager { return true; } + /** + * Removes all authority metadata objects from cache. + */ + removeAuthorityMetadata(): boolean { + const allCacheKeys = this.getKeys(); + allCacheKeys.forEach((cacheKey) => { + if (this.isAuthorityMetadata(cacheKey)) { + this.removeItem(cacheKey); + } + }); + + return true; + } + /** * Retrieve the cached credentials into a cacherecord * @param account @@ -828,7 +842,6 @@ export abstract class CacheManager implements ICacheManager { environment: string ): CacheRecord { const tokenKeys = this.getTokenKeys(); - debugger; const cachedAccount = this.readAccountFromCache(account); const cachedIdToken = this.getIdToken(account, tokenKeys); const cachedAccessToken = this.getAccessToken( diff --git a/lib/msal-common/test/cache/CacheManager.spec.ts b/lib/msal-common/test/cache/CacheManager.spec.ts index 59d5c31ee9..bbc03b60f0 100644 --- a/lib/msal-common/test/cache/CacheManager.spec.ts +++ b/lib/msal-common/test/cache/CacheManager.spec.ts @@ -1026,6 +1026,15 @@ describe("CacheManager.ts test cases", () => { ).toBeUndefined(); }); + it("removeAuthorityMetadata", () => { + mockCache.cacheManager.removeAuthorityMetadata(); + expect( + mockCache.cacheManager.getAuthorityMetadata( + `authority-metadata-${CACHE_MOCKS.MOCK_CLIENT_ID}-login.windows.net` + ) + ).toBeUndefined(); + }); + it("removeAllAccounts", async () => { const ac = new AccountEntity(); ac.homeAccountId = "someUid.someUtid"; diff --git a/lib/msal-common/test/cache/MockCache.ts b/lib/msal-common/test/cache/MockCache.ts index dc33df9401..2f5254fbeb 100644 --- a/lib/msal-common/test/cache/MockCache.ts +++ b/lib/msal-common/test/cache/MockCache.ts @@ -7,6 +7,7 @@ import { AccessTokenEntity, AccountEntity, AppMetadataEntity, + AuthorityMetadataEntity, CacheManager, ICrypto, IdTokenEntity, @@ -34,6 +35,7 @@ export class MockCache { this.createAccessTokenEntries(); this.createRefreshTokenEntries(); this.createAppMetadataEntries(); + this.createAuthorityMetadataEntries(); } // clear the cache @@ -305,4 +307,40 @@ export class MockCache { ); this.cacheManager.setAppMetadata(appMetaData); } + + // create authorityMetadata entries + createAuthorityMetadataEntries(): void { + const authorityMetadata_data = { + aliases: [ + "login.microsoftonline.com", + "login.windows.net", + "login.microsoft.com", + "sts.windows.net", + ], + aliasesFromNetwork: false, + authorization_endpoint: + "https://login.microsoftonline.com/common/oauth2/v2.0/authorize", + canonicalAuthority: "https://login.microsoftonline.com/common", + end_session_endpoint: + "https://login.microsoftonline.com/common/oauth2/v2.0/logout", + endpointsFromNetwork: false, + expiresAt: 1607952000, + issuer: "https://login.microsoftonline.com/{tenantId}/v2.0", + jwks_uri: + "https://login.microsoftonline.com/common/discovery/v2.0/keys", + preferred_cache: "login.windows.net", + token_endpoint: + "https://login.microsoftonline.com/common/oauth2/v2.0/token", + }; + + const authorityMetadata = CacheManager.toObject( + new AuthorityMetadataEntity(), + authorityMetadata_data + ); + const cacheKey = this.cacheManager.generateAuthorityMetadataCacheKey( + authorityMetadata.preferred_cache + ); + + this.cacheManager.setAuthorityMetadata(cacheKey, authorityMetadata); + } } From 4c1f845fbdb994856e9054c1fdc3530ae96ae61c Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Sun, 1 Oct 2023 12:54:19 -0700 Subject: [PATCH 04/91] Update getAccount APIs to only return accounts if there's a matching ID token in the cache --- lib/msal-common/src/cache/CacheManager.ts | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/lib/msal-common/src/cache/CacheManager.ts b/lib/msal-common/src/cache/CacheManager.ts index 7ebae3457b..b7d05d382a 100644 --- a/lib/msal-common/src/cache/CacheManager.ts +++ b/lib/msal-common/src/cache/CacheManager.ts @@ -226,11 +226,13 @@ export abstract class CacheManager implements ICacheManager { * @returns Array of AccountInfo objects in cache */ getAllAccounts(accountFilter?: AccountFilter): AccountInfo[] { - return this.getAccountsFilteredBy(accountFilter || {}).map( - (accountEntity) => { + return this.getAccountsFilteredBy(accountFilter || {}) + .map((accountEntity) => { return this.getAccountInfoFromEntity(accountEntity); - } - ); + }) + .filter((accoutnInfo) => { + return accoutnInfo.idTokenClaims; + }); } /** @@ -239,7 +241,14 @@ export abstract class CacheManager implements ICacheManager { getAccountInfoFilteredBy(accountFilter: AccountFilter): AccountInfo | null { const allAccounts = this.getAccountsFilteredBy(accountFilter); if (allAccounts.length > 0) { - return this.getAccountInfoFromEntity(allAccounts[0]); + const validAccounts = allAccounts + .map((accountEntity) => { + return this.getAccountInfoFromEntity(accountEntity); + }) + .filter((accountInfo) => { + return accountInfo.idTokenClaims; + }); + return validAccounts[0]; } else { return null; } From 9ea89fdc94cdadc835b06cc2b0cfbbdadee81bbd Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Sun, 1 Oct 2023 16:19:56 -0700 Subject: [PATCH 05/91] Update tests to reflect authority metadata caching changes --- .../test/app/PublicClientApplication.spec.ts | 4 +- .../test/cache/BrowserCacheManager.spec.ts | 17 ++--- .../BaseInteractionClient.spec.ts | 62 ++++++++++++++++++- .../interaction_client/PopupClient.spec.ts | 4 +- .../interaction_client/RedirectClient.spec.ts | 4 +- 5 files changed, 73 insertions(+), 18 deletions(-) diff --git a/lib/msal-browser/test/app/PublicClientApplication.spec.ts b/lib/msal-browser/test/app/PublicClientApplication.spec.ts index 6a9c5d074e..e2633aa400 100644 --- a/lib/msal-browser/test/app/PublicClientApplication.spec.ts +++ b/lib/msal-browser/test/app/PublicClientApplication.spec.ts @@ -4031,7 +4031,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { }); } catch (e) { // Test that error was cached for telemetry purposes and then thrown - expect(window.sessionStorage).toHaveLength(1); + expect(window.sessionStorage).toHaveLength(2); const failures = window.sessionStorage.getItem( `server-telemetry-${TEST_CONFIG.MSAL_CLIENT_ID}` ); @@ -4089,7 +4089,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { await silentRequest3.catch(() => {}); // Test that error was cached for telemetry purposes and then thrown expect(atsSpy.calledOnce).toBe(true); - expect(window.sessionStorage).toHaveLength(1); + expect(window.sessionStorage).toHaveLength(2); const failures = window.sessionStorage.getItem( `server-telemetry-${TEST_CONFIG.MSAL_CLIENT_ID}` ); diff --git a/lib/msal-browser/test/cache/BrowserCacheManager.spec.ts b/lib/msal-browser/test/cache/BrowserCacheManager.spec.ts index cf95dcf1a1..4e9ab1178a 100644 --- a/lib/msal-browser/test/cache/BrowserCacheManager.spec.ts +++ b/lib/msal-browser/test/cache/BrowserCacheManager.spec.ts @@ -1547,15 +1547,12 @@ describe("BrowserCacheManager tests", () => { .returns(false); browserSessionStorage.setAuthorityMetadata(key, testObj); browserLocalStorage.setAuthorityMetadata(key, testObj); - expect( browserSessionStorage.getAuthorityMetadata(key) ).toBeNull(); expect( browserLocalStorage.getAuthorityMetadata(key) ).toBeNull(); - expect(browserSessionStorage.containsKey(key)).toBe(false); - expect(browserLocalStorage.containsKey(key)).toBe(false); expect( browserLocalStorage.getAuthorityMetadataKeys() ).toEqual(expect.arrayContaining([key])); @@ -1564,7 +1561,7 @@ describe("BrowserCacheManager tests", () => { ).toEqual(expect.arrayContaining([key])); }); - it("setAuthorityMetadata() and getAuthorityMetadata() sets and returns AuthorityMetadataEntity in-memory", () => { + it("setAuthorityMetadata() and getAuthorityMetadata() sets and returns AuthorityMetadataEntity in configured cache location", () => { browserSessionStorage.setAuthorityMetadata(key, testObj); browserLocalStorage.setAuthorityMetadata(key, testObj); @@ -1574,8 +1571,8 @@ describe("BrowserCacheManager tests", () => { expect( browserLocalStorage.getAuthorityMetadata(key) ).toEqual(testObj); - expect(browserSessionStorage.containsKey(key)).toBe(false); - expect(browserLocalStorage.containsKey(key)).toBe(false); + expect(browserSessionStorage.containsKey(key)).toBe(true); + expect(browserLocalStorage.containsKey(key)).toBe(true); expect( browserLocalStorage.getAuthorityMetadataKeys() ).toEqual(expect.arrayContaining([key])); @@ -2501,8 +2498,6 @@ describe("BrowserCacheManager tests", () => { expect( browserLocalStorage.getAuthorityMetadata(key) ).toBeNull(); - expect(browserSessionStorage.containsKey(key)).toBe(false); - expect(browserLocalStorage.containsKey(key)).toBe(false); expect( browserLocalStorage.getAuthorityMetadataKeys() ).toEqual(expect.arrayContaining([key])); @@ -2511,7 +2506,7 @@ describe("BrowserCacheManager tests", () => { ).toEqual(expect.arrayContaining([key])); }); - it("setAuthorityMetadata() and getAuthorityMetadata() sets and returns AuthorityMetadataEntity in-memory", () => { + it("setAuthorityMetadata() and getAuthorityMetadata() sets and returns AuthorityMetadataEntity in configured cache location", () => { browserSessionStorage.setAuthorityMetadata(key, testObj); browserLocalStorage.setAuthorityMetadata(key, testObj); @@ -2521,8 +2516,8 @@ describe("BrowserCacheManager tests", () => { expect( browserLocalStorage.getAuthorityMetadata(key) ).toEqual(testObj); - expect(browserSessionStorage.containsKey(key)).toBe(false); - expect(browserLocalStorage.containsKey(key)).toBe(false); + expect(browserSessionStorage.containsKey(key)).toBe(true); + expect(browserLocalStorage.containsKey(key)).toBe(true); expect( browserLocalStorage.getAuthorityMetadataKeys() ).toEqual(expect.arrayContaining([key])); diff --git a/lib/msal-browser/test/interaction_client/BaseInteractionClient.spec.ts b/lib/msal-browser/test/interaction_client/BaseInteractionClient.spec.ts index b5b39764e0..dd7ae05aaa 100644 --- a/lib/msal-browser/test/interaction_client/BaseInteractionClient.spec.ts +++ b/lib/msal-browser/test/interaction_client/BaseInteractionClient.spec.ts @@ -10,8 +10,17 @@ import { TokenClaims, createClientConfigurationError, ClientConfigurationErrorCodes, + CacheManager, + IdTokenEntity, + AuthorityMetadataEntity, } from "@azure/msal-common"; -import { TEST_DATA_CLIENT_INFO, TEST_CONFIG } from "../utils/StringConstants"; +import { + TEST_DATA_CLIENT_INFO, + TEST_CONFIG, + TEST_TOKENS, + DEFAULT_TENANT_DISCOVERY_RESPONSE, + DEFAULT_OPENID_CONFIG_RESPONSE, +} from "../utils/StringConstants"; import { BaseInteractionClient } from "../../src/interaction_client/BaseInteractionClient"; import { EndSessionRequest, PublicClientApplication } from "../../src"; @@ -83,6 +92,20 @@ describe("BaseInteractionClient", () => { username: testIdTokenClaims.preferred_username || "", }; + const idTokenData1 = { + realm: testAccountInfo1.tenantId, + environment: testAccountInfo1.environment, + credentialType: "IdToken", + secret: TEST_TOKENS.IDTOKEN_V2, + clientId: TEST_CONFIG.MSAL_CLIENT_ID, + homeAccountId: testAccountInfo1.homeAccountId, + }; + + const idToken1 = CacheManager.toObject( + new IdTokenEntity(), + idTokenData1 + ); + const testAccount1: AccountEntity = new AccountEntity(); testAccount1.homeAccountId = testAccountInfo1.homeAccountId; testAccount1.localAccountId = testAccountInfo1.localAccountId; @@ -102,6 +125,20 @@ describe("BaseInteractionClient", () => { username: testIdTokenClaims.preferred_username || "", }; + const idTokenData2 = { + realm: testAccountInfo2.tenantId, + environment: testAccountInfo2.environment, + credentialType: "IdToken", + secret: TEST_TOKENS.IDTOKEN_V2, + clientId: TEST_CONFIG.MSAL_CLIENT_ID, + homeAccountId: testAccountInfo2.homeAccountId, + }; + + const idToken2 = CacheManager.toObject( + new IdTokenEntity(), + idTokenData2 + ); + const testAccount2: AccountEntity = new AccountEntity(); testAccount2.homeAccountId = testAccountInfo2.homeAccountId; testAccount2.localAccountId = testAccountInfo2.localAccountId; @@ -117,11 +154,34 @@ describe("BaseInteractionClient", () => { // @ts-ignore pca.browserStorage.setAccount(testAccount1); // @ts-ignore + pca.browserStorage.setIdTokenCredential(idToken1); + // @ts-ignore pca.browserStorage.setAccount(testAccount2); + // @ts-ignore + pca.browserStorage.setIdTokenCredential(idToken2); + + jest.spyOn( + CacheManager.prototype, + "getAuthorityMetadataByAlias" + ).mockImplementation((host: string) => { + const metadata = + DEFAULT_TENANT_DISCOVERY_RESPONSE.body.metadata[0]; + const openIdConfigResponse = + DEFAULT_OPENID_CONFIG_RESPONSE.body; + const authorityMetadata = new AuthorityMetadataEntity(); + authorityMetadata.updateCloudDiscoveryMetadata(metadata, true); + authorityMetadata.updateEndpointMetadata( + // @ts-ignore + openIdConfigResponse, + true + ); + return authorityMetadata; + }); }); afterEach(() => { window.sessionStorage.clear(); + jest.clearAllMocks(); }); it("Removes all accounts from cache if no account provided", async () => { diff --git a/lib/msal-browser/test/interaction_client/PopupClient.spec.ts b/lib/msal-browser/test/interaction_client/PopupClient.spec.ts index 2a16b18de8..dfa0c14227 100644 --- a/lib/msal-browser/test/interaction_client/PopupClient.spec.ts +++ b/lib/msal-browser/test/interaction_client/PopupClient.spec.ts @@ -737,7 +737,7 @@ describe("PopupClient", () => { }); } catch (e) { // Test that error was cached for telemetry purposes and then thrown - expect(window.sessionStorage).toHaveLength(1); + expect(window.sessionStorage).toHaveLength(2); const failures = window.sessionStorage.getItem( `server-telemetry-${TEST_CONFIG.MSAL_CLIENT_ID}` ); @@ -848,7 +848,7 @@ describe("PopupClient", () => { await popupClient.logout(); } catch (e) { // Test that error was cached for telemetry purposes and then thrown - expect(window.sessionStorage).toHaveLength(1); + expect(window.sessionStorage).toHaveLength(2); const failures = window.sessionStorage.getItem( `server-telemetry-${TEST_CONFIG.MSAL_CLIENT_ID}` ); diff --git a/lib/msal-browser/test/interaction_client/RedirectClient.spec.ts b/lib/msal-browser/test/interaction_client/RedirectClient.spec.ts index ca71bf4d69..55f2e2785d 100644 --- a/lib/msal-browser/test/interaction_client/RedirectClient.spec.ts +++ b/lib/msal-browser/test/interaction_client/RedirectClient.spec.ts @@ -2192,7 +2192,7 @@ describe("RedirectClient", () => { await redirectClient.acquireToken(emptyRequest); } catch (e) { // Test that error was cached for telemetry purposes and then thrown - expect(window.sessionStorage).toHaveLength(1); + expect(window.sessionStorage).toHaveLength(2); const failures = window.sessionStorage.getItem( `server-telemetry-${TEST_CONFIG.MSAL_CLIENT_ID}` ); @@ -2777,7 +2777,7 @@ describe("RedirectClient", () => { await redirectClient.acquireToken(emptyRequest); } catch (e) { // Test that error was cached for telemetry purposes and then thrown - expect(window.sessionStorage).toHaveLength(1); + expect(window.sessionStorage).toHaveLength(2); const failures = window.sessionStorage.getItem( `server-telemetry-${TEST_CONFIG.MSAL_CLIENT_ID}` ); From d747689e0609864fedc799fce73aaa79850be127 Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Tue, 3 Oct 2023 14:07:36 -0700 Subject: [PATCH 06/91] Revert authority metadata storage to in-memory cache and add a synchronous authority endpoint resolution step in standard controller contstructor --- .../src/cache/BrowserCacheManager.ts | 6 +- .../src/controllers/StandardController.ts | 25 ++ .../test/app/PublicClientApplication.spec.ts | 4 +- .../test/cache/BrowserCacheManager.spec.ts | 12 +- .../interaction_client/PopupClient.spec.ts | 4 +- .../interaction_client/RedirectClient.spec.ts | 4 +- lib/msal-common/src/authority/Authority.ts | 266 +++++++++++++----- .../telemetry/performance/PerformanceEvent.ts | 2 + 8 files changed, 230 insertions(+), 93 deletions(-) diff --git a/lib/msal-browser/src/cache/BrowserCacheManager.ts b/lib/msal-browser/src/cache/BrowserCacheManager.ts index 5a05b6176b..600babb4ff 100644 --- a/lib/msal-browser/src/cache/BrowserCacheManager.ts +++ b/lib/msal-browser/src/cache/BrowserCacheManager.ts @@ -934,7 +934,7 @@ export class BrowserCacheManager extends CacheManager { * */ getAuthorityMetadata(key: string): AuthorityMetadataEntity | null { - const value = this.temporaryCacheStorage.getItem(key); + const value = this.internalStorage.getItem(key); if (!value) { this.logger.trace( "BrowserCacheManager.getAuthorityMetadata: called, no cache hit" @@ -964,7 +964,7 @@ export class BrowserCacheManager extends CacheManager { * */ getAuthorityMetadataKeys(): Array { - const allKeys = this.temporaryCacheStorage.getKeys(); + const allKeys = this.internalStorage.getKeys(); return allKeys.filter((key) => { return this.isAuthorityMetadata(key); }); @@ -1002,7 +1002,7 @@ export class BrowserCacheManager extends CacheManager { */ setAuthorityMetadata(key: string, entity: AuthorityMetadataEntity): void { this.logger.trace("BrowserCacheManager.setAuthorityMetadata called"); - this.temporaryCacheStorage.setItem(key, JSON.stringify(entity)); + this.internalStorage.setItem(key, JSON.stringify(entity)); } /** diff --git a/lib/msal-browser/src/controllers/StandardController.ts b/lib/msal-browser/src/controllers/StandardController.ts index bd22b75150..1a7fb2ecfa 100644 --- a/lib/msal-browser/src/controllers/StandardController.ts +++ b/lib/msal-browser/src/controllers/StandardController.ts @@ -29,6 +29,8 @@ import { createClientAuthError, ClientAuthErrorCodes, AccountFilter, + AuthorityFactory, + AuthorityOptions, } from "@azure/msal-common"; import { BrowserCacheManager, @@ -246,6 +248,8 @@ export class StandardController implements IController { // Register listener functions this.trackPageVisibilityWithMeasurement = this.trackPageVisibilityWithMeasurement.bind(this); + // Load local authority metadata into memory + this.loadLocalAuthorityMetadata(); } static async createController( @@ -256,6 +260,27 @@ export class StandardController implements IController { return controller; } + private loadLocalAuthorityMetadata(): void { + const authorityOptions: AuthorityOptions = { + protocolMode: this.config.auth.protocolMode, + OIDCOptions: this.config.auth.OIDCOptions, + knownAuthorities: this.config.auth.knownAuthorities, + cloudDiscoveryMetadata: this.config.auth.cloudDiscoveryMetadata, + authorityMetadata: this.config.auth.authorityMetadata, + }; + + const locallyDiscoveredAuthorityInstance = + AuthorityFactory.createInstance( + this.config.auth.authority, + this.networkClient, + this.browserStorage, + authorityOptions, + this.logger, + this.performanceClient + ); + locallyDiscoveredAuthorityInstance.resolveEndpointsFromLocalSources(); + } + private trackPageVisibility(): void { if (!this.atsAsyncMeasurement) { return; diff --git a/lib/msal-browser/test/app/PublicClientApplication.spec.ts b/lib/msal-browser/test/app/PublicClientApplication.spec.ts index c8c09a5408..56a1e66dd8 100644 --- a/lib/msal-browser/test/app/PublicClientApplication.spec.ts +++ b/lib/msal-browser/test/app/PublicClientApplication.spec.ts @@ -4045,7 +4045,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { }); } catch (e) { // Test that error was cached for telemetry purposes and then thrown - expect(window.sessionStorage).toHaveLength(2); + expect(window.sessionStorage).toHaveLength(1); const failures = window.sessionStorage.getItem( `server-telemetry-${TEST_CONFIG.MSAL_CLIENT_ID}` ); @@ -4103,7 +4103,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { await silentRequest3.catch(() => {}); // Test that error was cached for telemetry purposes and then thrown expect(atsSpy.calledOnce).toBe(true); - expect(window.sessionStorage).toHaveLength(2); + expect(window.sessionStorage).toHaveLength(1); const failures = window.sessionStorage.getItem( `server-telemetry-${TEST_CONFIG.MSAL_CLIENT_ID}` ); diff --git a/lib/msal-browser/test/cache/BrowserCacheManager.spec.ts b/lib/msal-browser/test/cache/BrowserCacheManager.spec.ts index 4e9ab1178a..338d70f60c 100644 --- a/lib/msal-browser/test/cache/BrowserCacheManager.spec.ts +++ b/lib/msal-browser/test/cache/BrowserCacheManager.spec.ts @@ -1561,7 +1561,7 @@ describe("BrowserCacheManager tests", () => { ).toEqual(expect.arrayContaining([key])); }); - it("setAuthorityMetadata() and getAuthorityMetadata() sets and returns AuthorityMetadataEntity in configured cache location", () => { + it("setAuthorityMetadata() and getAuthorityMetadata() sets and returns AuthorityMetadataEntity in-memory", () => { browserSessionStorage.setAuthorityMetadata(key, testObj); browserLocalStorage.setAuthorityMetadata(key, testObj); @@ -1571,8 +1571,8 @@ describe("BrowserCacheManager tests", () => { expect( browserLocalStorage.getAuthorityMetadata(key) ).toEqual(testObj); - expect(browserSessionStorage.containsKey(key)).toBe(true); - expect(browserLocalStorage.containsKey(key)).toBe(true); + expect(browserSessionStorage.containsKey(key)).toBe(false); + expect(browserLocalStorage.containsKey(key)).toBe(false); expect( browserLocalStorage.getAuthorityMetadataKeys() ).toEqual(expect.arrayContaining([key])); @@ -2506,7 +2506,7 @@ describe("BrowserCacheManager tests", () => { ).toEqual(expect.arrayContaining([key])); }); - it("setAuthorityMetadata() and getAuthorityMetadata() sets and returns AuthorityMetadataEntity in configured cache location", () => { + it("setAuthorityMetadata() and getAuthorityMetadata() sets and returns AuthorityMetadataEntity in-memory", () => { browserSessionStorage.setAuthorityMetadata(key, testObj); browserLocalStorage.setAuthorityMetadata(key, testObj); @@ -2516,8 +2516,8 @@ describe("BrowserCacheManager tests", () => { expect( browserLocalStorage.getAuthorityMetadata(key) ).toEqual(testObj); - expect(browserSessionStorage.containsKey(key)).toBe(true); - expect(browserLocalStorage.containsKey(key)).toBe(true); + expect(browserSessionStorage.containsKey(key)).toBe(false); + expect(browserLocalStorage.containsKey(key)).toBe(false); expect( browserLocalStorage.getAuthorityMetadataKeys() ).toEqual(expect.arrayContaining([key])); diff --git a/lib/msal-browser/test/interaction_client/PopupClient.spec.ts b/lib/msal-browser/test/interaction_client/PopupClient.spec.ts index dfa0c14227..2a16b18de8 100644 --- a/lib/msal-browser/test/interaction_client/PopupClient.spec.ts +++ b/lib/msal-browser/test/interaction_client/PopupClient.spec.ts @@ -737,7 +737,7 @@ describe("PopupClient", () => { }); } catch (e) { // Test that error was cached for telemetry purposes and then thrown - expect(window.sessionStorage).toHaveLength(2); + expect(window.sessionStorage).toHaveLength(1); const failures = window.sessionStorage.getItem( `server-telemetry-${TEST_CONFIG.MSAL_CLIENT_ID}` ); @@ -848,7 +848,7 @@ describe("PopupClient", () => { await popupClient.logout(); } catch (e) { // Test that error was cached for telemetry purposes and then thrown - expect(window.sessionStorage).toHaveLength(2); + expect(window.sessionStorage).toHaveLength(1); const failures = window.sessionStorage.getItem( `server-telemetry-${TEST_CONFIG.MSAL_CLIENT_ID}` ); diff --git a/lib/msal-browser/test/interaction_client/RedirectClient.spec.ts b/lib/msal-browser/test/interaction_client/RedirectClient.spec.ts index 55f2e2785d..ca71bf4d69 100644 --- a/lib/msal-browser/test/interaction_client/RedirectClient.spec.ts +++ b/lib/msal-browser/test/interaction_client/RedirectClient.spec.ts @@ -2192,7 +2192,7 @@ describe("RedirectClient", () => { await redirectClient.acquireToken(emptyRequest); } catch (e) { // Test that error was cached for telemetry purposes and then thrown - expect(window.sessionStorage).toHaveLength(2); + expect(window.sessionStorage).toHaveLength(1); const failures = window.sessionStorage.getItem( `server-telemetry-${TEST_CONFIG.MSAL_CLIENT_ID}` ); @@ -2777,7 +2777,7 @@ describe("RedirectClient", () => { await redirectClient.acquireToken(emptyRequest); } catch (e) { // Test that error was cached for telemetry purposes and then thrown - expect(window.sessionStorage).toHaveLength(2); + expect(window.sessionStorage).toHaveLength(1); const failures = window.sessionStorage.getItem( `server-telemetry-${TEST_CONFIG.MSAL_CLIENT_ID}` ); diff --git a/lib/msal-common/src/authority/Authority.ts b/lib/msal-common/src/authority/Authority.ts index 95332103f3..39fd1c8650 100644 --- a/lib/msal-common/src/authority/Authority.ts +++ b/lib/msal-common/src/authority/Authority.ts @@ -383,6 +383,35 @@ export class Authority { return !!this.metadata; } + /** + * Performs locally-sourced, synchronous endpoint discovery to discover aliases, preferred_cache, preferred_network + * and the /authorize, /token and logout endpoints. + */ + public resolveEndpointsFromLocalSources(): void { + this.performanceClient?.addQueueMeasurement( + PerformanceEvents.AuthorityResolveEndpointsFromLocalSources, + this.correlationId + ); + + const metadataEntity = this.getCurrentMetadataEntity(); + + const cloudDiscoverySource = + this.updateCloudDiscoveryMetadataFromLocalSources(metadataEntity); + + this.canonicalAuthority = this.canonicalAuthority.replace( + this.hostnameAndPort, + metadataEntity.preferred_network + ); + const endpointMetadataResult = + this.updateEndpointMetadataFromLocalSources(metadataEntity); + + this.updateCachedMetadata( + metadataEntity, + cloudDiscoverySource, + endpointMetadataResult + ); + } + /** * Perform endpoint discovery to discover aliases, preferred_cache, preferred_network * and the /authorize, /token and logout endpoints. @@ -393,14 +422,7 @@ export class Authority { this.correlationId ); - let metadataEntity = this.cacheManager.getAuthorityMetadataByAlias( - this.hostnameAndPort - ); - - if (!metadataEntity) { - metadataEntity = new AuthorityMetadataEntity(); - metadataEntity.updateCanonicalAuthority(this.canonicalAuthority); - } + const metadataEntity = this.getCurrentMetadataEntity(); const cloudDiscoverySource = await invokeAsync( this.updateCloudDiscoveryMetadata.bind(this), @@ -420,10 +442,46 @@ export class Authority { this.performanceClient, this.correlationId )(metadataEntity); + this.updateCachedMetadata(metadataEntity, cloudDiscoverySource, { + source: endpointSource, + }); + } + /** + * Returns metadata entity from cache if it exists, otherwiser returns a new metadata entity built + * from the configured canonical authority + * @returns + */ + private getCurrentMetadataEntity(): AuthorityMetadataEntity { + let metadataEntity = this.cacheManager.getAuthorityMetadataByAlias( + this.hostnameAndPort + ); + + if (!metadataEntity) { + metadataEntity = new AuthorityMetadataEntity(); + metadataEntity.updateCanonicalAuthority(this.canonicalAuthority); + } + return metadataEntity; + } + + /** + * Updates cached metadata based on metadata source and sets the instance's metadata + * property to the same value + * @param metadataEntity + * @param cloudDiscoverySource + * @param endpointMetadataResult + */ + private updateCachedMetadata( + metadataEntity: AuthorityMetadataEntity, + cloudDiscoverySource: AuthorityMetadataSource | null, + endpointMetadataResult: { + source: AuthorityMetadataSource; + metadata?: OpenIdConfigResponse; + } | null + ): void { if ( cloudDiscoverySource !== AuthorityMetadataSource.CACHE && - endpointSource !== AuthorityMetadataSource.CACHE + endpointMetadataResult?.source !== AuthorityMetadataSource.CACHE ) { // Reset the expiration time unless both values came from a successful cache lookup metadataEntity.resetExpiresAt(); @@ -448,16 +506,95 @@ export class Authority { PerformanceEvents.AuthorityUpdateEndpointMetadata, this.correlationId ); + + const localMetadata = + this.updateEndpointMetadataFromLocalSources(metadataEntity); + + // Further update may be required for hardcoded metadata if regional metadata is preferred + if (localMetadata) { + if ( + localMetadata.source === + AuthorityMetadataSource.HARDCODED_VALUES + ) { + // If the user prefers to use an azure region replace the global endpoints with regional information. + if ( + this.authorityOptions.azureRegionConfiguration?.azureRegion + ) { + if (localMetadata.metadata) { + const hardcodedMetadata = await invokeAsync( + this.updateMetadataWithRegionalInformation.bind( + this + ), + PerformanceEvents.AuthorityUpdateMetadataWithRegionalInformation, + this.logger, + this.performanceClient, + this.correlationId + )(localMetadata.metadata); + metadataEntity.updateEndpointMetadata( + hardcodedMetadata, + false + ); + } + } + } + return localMetadata.source; + } + + // Get metadata from network if local sources aren't available + let metadata = await invokeAsync( + this.getEndpointMetadataFromNetwork.bind(this), + PerformanceEvents.AuthorityGetEndpointMetadataFromNetwork, + this.logger, + this.performanceClient, + this.correlationId + )(); + if (metadata) { + // If the user prefers to use an azure region replace the global endpoints with regional information. + if (this.authorityOptions.azureRegionConfiguration?.azureRegion) { + metadata = await invokeAsync( + this.updateMetadataWithRegionalInformation.bind(this), + PerformanceEvents.AuthorityUpdateMetadataWithRegionalInformation, + this.logger, + this.performanceClient, + this.correlationId + )(metadata); + } + + metadataEntity.updateEndpointMetadata(metadata, true); + return AuthorityMetadataSource.NETWORK; + } else { + // Metadata could not be obtained from the config, cache, network or hardcoded values + throw createClientAuthError( + ClientAuthErrorCodes.openIdConfigError, + this.defaultOpenIdConfigurationEndpoint + ); + } + } + + /** + * Updates endpoint metadata from local sources and returns where the information was retrieved from and the metadata config + * response if the source is hardcoded metadata + * @param metadataEntity + * @returns + */ + private updateEndpointMetadataFromLocalSources( + metadataEntity: AuthorityMetadataEntity + ): { + source: AuthorityMetadataSource; + metadata?: OpenIdConfigResponse; + } | null { this.logger.verbose( "Attempting to get endpoint metadata from authority configuration" ); - let metadata = this.getEndpointMetadataFromConfig(); - if (metadata) { + const configMetadata = this.getEndpointMetadataFromConfig(); + if (configMetadata) { this.logger.verbose( "Found endpoint metadata in authority configuration" ); - metadataEntity.updateEndpointMetadata(metadata, false); - return AuthorityMetadataSource.CONFIG; + metadataEntity.updateEndpointMetadata(configMetadata, false); + return { + source: AuthorityMetadataSource.CONFIG, + }; } this.logger.verbose( @@ -470,27 +607,14 @@ export class Authority { "Skipping hardcoded metadata cache since skipAuthorityMetadataCache is set to true. Attempting to get endpoint metadata from the network metadata cache." ); } else { - let hardcodedMetadata = + const hardcodedMetadata = this.getEndpointMetadataFromHardcodedValues(); if (hardcodedMetadata) { - this.logger.verbose( - "Found endpoint metadata from hardcoded values." - ); - // If the user prefers to use an azure region replace the global endpoints with regional information. - if ( - this.authorityOptions.azureRegionConfiguration?.azureRegion - ) { - hardcodedMetadata = await invokeAsync( - this.updateMetadataWithRegionalInformation.bind(this), - PerformanceEvents.AuthorityUpdateMetadataWithRegionalInformation, - this.logger, - this.performanceClient, - this.correlationId - )(hardcodedMetadata); - } - metadataEntity.updateEndpointMetadata(hardcodedMetadata, false); - return AuthorityMetadataSource.HARDCODED_VALUES; + return { + source: AuthorityMetadataSource.HARDCODED_VALUES, + metadata: hardcodedMetadata, + }; } else { this.logger.verbose( "Did not find endpoint metadata in hardcoded values... Attempting to get endpoint metadata from the network metadata cache." @@ -507,39 +631,12 @@ export class Authority { ) { // No need to update this.logger.verbose("Found endpoint metadata in the cache."); - return AuthorityMetadataSource.CACHE; + return { source: AuthorityMetadataSource.CACHE }; } else if (metadataEntityExpired) { this.logger.verbose("The metadata entity is expired."); } - metadata = await invokeAsync( - this.getEndpointMetadataFromNetwork.bind(this), - PerformanceEvents.AuthorityGetEndpointMetadataFromNetwork, - this.logger, - this.performanceClient, - this.correlationId - )(); - if (metadata) { - // If the user prefers to use an azure region replace the global endpoints with regional information. - if (this.authorityOptions.azureRegionConfiguration?.azureRegion) { - metadata = await invokeAsync( - this.updateMetadataWithRegionalInformation.bind(this), - PerformanceEvents.AuthorityUpdateMetadataWithRegionalInformation, - this.logger, - this.performanceClient, - this.correlationId - )(metadata); - } - - metadataEntity.updateEndpointMetadata(metadata, true); - return AuthorityMetadataSource.NETWORK; - } else { - // Metadata could not be obtained from the config, cache, network or hardcoded values - throw createClientAuthError( - ClientAuthErrorCodes.openIdConfigError, - this.defaultOpenIdConfigurationEndpoint - ); - } + return null; } /** @@ -712,6 +809,35 @@ export class Authority { PerformanceEvents.AuthorityUpdateCloudDiscoveryMetadata, this.correlationId ); + const localMetadataSource = + this.updateCloudDiscoveryMetadataFromLocalSources(metadataEntity); + if (localMetadataSource) { + return localMetadataSource; + } + + // Fallback to network as metadata source + const metadata = await invokeAsync( + this.getCloudDiscoveryMetadataFromNetwork.bind(this), + PerformanceEvents.AuthorityGetCloudDiscoveryMetadataFromNetwork, + this.logger, + this.performanceClient, + this.correlationId + )(); + + if (metadata) { + metadataEntity.updateCloudDiscoveryMetadata(metadata, true); + return AuthorityMetadataSource.NETWORK; + } + + // Metadata could not be obtained from the config, cache, network or hardcoded values + throw createClientConfigurationError( + ClientConfigurationErrorCodes.untrustedAuthority + ); + } + + private updateCloudDiscoveryMetadataFromLocalSources( + metadataEntity: AuthorityMetadataEntity + ): AuthorityMetadataSource | null { this.logger.verbose( "Attempting to get cloud discovery metadata from authority configuration" ); @@ -732,7 +858,7 @@ export class Authority { metadataEntity.canonical_authority || Constants.NOT_APPLICABLE }` ); - let metadata = this.getCloudDiscoveryMetadataFromConfig(); + const metadata = this.getCloudDiscoveryMetadataFromConfig(); if (metadata) { this.logger.verbose( "Found cloud discovery metadata in authority configuration" @@ -782,23 +908,7 @@ export class Authority { this.logger.verbose("The metadata entity is expired."); } - metadata = await invokeAsync( - this.getCloudDiscoveryMetadataFromNetwork.bind(this), - PerformanceEvents.AuthorityGetCloudDiscoveryMetadataFromNetwork, - this.logger, - this.performanceClient, - this.correlationId - )(); - - if (metadata) { - metadataEntity.updateCloudDiscoveryMetadata(metadata, true); - return AuthorityMetadataSource.NETWORK; - } - - // Metadata could not be obtained from the config, cache, network or hardcoded values - throw createClientConfigurationError( - ClientConfigurationErrorCodes.untrustedAuthority - ); + return null; } /** diff --git a/lib/msal-common/src/telemetry/performance/PerformanceEvent.ts b/lib/msal-common/src/telemetry/performance/PerformanceEvent.ts index 8d5fb298e6..9b853998a3 100644 --- a/lib/msal-common/src/telemetry/performance/PerformanceEvent.ts +++ b/lib/msal-common/src/telemetry/performance/PerformanceEvent.ts @@ -230,6 +230,8 @@ export const PerformanceEvents = { AuthorityFactoryCreateDiscoveredInstance: "authorityFactoryCreateDiscoveredInstance", AuthorityResolveEndpointsAsync: "authorityResolveEndpointsAsync", + AuthorityResolveEndpointsFromLocalSources: + "authorityResolveEndpointsFromLocalSources", AuthorityGetCloudDiscoveryMetadataFromNetwork: "authorityGetCloudDiscoveryMetadataFromNetwork", AuthorityUpdateCloudDiscoveryMetadata: From 6149773c71cb0134c9d82441f66a32018097d3a8 Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Tue, 3 Oct 2023 14:13:09 -0700 Subject: [PATCH 07/91] Change files --- ...-msal-browser-213a4bf6-cff8-4dc4-a8f2-90b2bab46242.json | 7 +++++++ ...e-msal-common-beeb49bf-aa51-4076-806c-d70227b5f480.json | 7 +++++++ 2 files changed, 14 insertions(+) create mode 100644 change/@azure-msal-browser-213a4bf6-cff8-4dc4-a8f2-90b2bab46242.json create mode 100644 change/@azure-msal-common-beeb49bf-aa51-4076-806c-d70227b5f480.json diff --git a/change/@azure-msal-browser-213a4bf6-cff8-4dc4-a8f2-90b2bab46242.json b/change/@azure-msal-browser-213a4bf6-cff8-4dc4-a8f2-90b2bab46242.json new file mode 100644 index 0000000000..583c7014f8 --- /dev/null +++ b/change/@azure-msal-browser-213a4bf6-cff8-4dc4-a8f2-90b2bab46242.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "Build account objects from cached ID Token #6529", + "packageName": "@azure/msal-browser", + "email": "hemoral@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@azure-msal-common-beeb49bf-aa51-4076-806c-d70227b5f480.json b/change/@azure-msal-common-beeb49bf-aa51-4076-806c-d70227b5f480.json new file mode 100644 index 0000000000..1b6aa67690 --- /dev/null +++ b/change/@azure-msal-common-beeb49bf-aa51-4076-806c-d70227b5f480.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "Build account objects from cached ID Token #6529", + "packageName": "@azure/msal-common", + "email": "hemoral@microsoft.com", + "dependentChangeType": "patch" +} From 1c737688bb977db6d4bddb71665e7fd90cf44da7 Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Tue, 3 Oct 2023 14:14:55 -0700 Subject: [PATCH 08/91] Remove unused authority metadata removal method --- lib/msal-browser/src/cache/BrowserCacheManager.ts | 1 - lib/msal-common/test/cache/CacheManager.spec.ts | 9 --------- 2 files changed, 10 deletions(-) diff --git a/lib/msal-browser/src/cache/BrowserCacheManager.ts b/lib/msal-browser/src/cache/BrowserCacheManager.ts index 600babb4ff..664a937c55 100644 --- a/lib/msal-browser/src/cache/BrowserCacheManager.ts +++ b/lib/msal-browser/src/cache/BrowserCacheManager.ts @@ -1343,7 +1343,6 @@ export class BrowserCacheManager extends CacheManager { // Removes all accounts and their credentials await this.removeAllAccounts(); this.removeAppMetadata(); - this.removeAuthorityMetadata(); // Removes all remaining MSAL cache items this.getKeys().forEach((cacheKey: string) => { diff --git a/lib/msal-common/test/cache/CacheManager.spec.ts b/lib/msal-common/test/cache/CacheManager.spec.ts index bbc03b60f0..59d5c31ee9 100644 --- a/lib/msal-common/test/cache/CacheManager.spec.ts +++ b/lib/msal-common/test/cache/CacheManager.spec.ts @@ -1026,15 +1026,6 @@ describe("CacheManager.ts test cases", () => { ).toBeUndefined(); }); - it("removeAuthorityMetadata", () => { - mockCache.cacheManager.removeAuthorityMetadata(); - expect( - mockCache.cacheManager.getAuthorityMetadata( - `authority-metadata-${CACHE_MOCKS.MOCK_CLIENT_ID}-login.windows.net` - ) - ).toBeUndefined(); - }); - it("removeAllAccounts", async () => { const ac = new AccountEntity(); ac.homeAccountId = "someUid.someUtid"; From efdfff055f4d886a541c0ed4f66c505a72b030ed Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Tue, 3 Oct 2023 14:26:53 -0700 Subject: [PATCH 09/91] Remove unused method from cachemanager --- lib/msal-common/src/cache/CacheManager.ts | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/lib/msal-common/src/cache/CacheManager.ts b/lib/msal-common/src/cache/CacheManager.ts index b7d05d382a..8ab7704c86 100644 --- a/lib/msal-common/src/cache/CacheManager.ts +++ b/lib/msal-common/src/cache/CacheManager.ts @@ -823,20 +823,6 @@ export abstract class CacheManager implements ICacheManager { return true; } - /** - * Removes all authority metadata objects from cache. - */ - removeAuthorityMetadata(): boolean { - const allCacheKeys = this.getKeys(); - allCacheKeys.forEach((cacheKey) => { - if (this.isAuthorityMetadata(cacheKey)) { - this.removeItem(cacheKey); - } - }); - - return true; - } - /** * Retrieve the cached credentials into a cacherecord * @param account From 2f35887759a71a3b5b762c78d7dbf92faadb290f Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Tue, 3 Oct 2023 14:27:49 -0700 Subject: [PATCH 10/91] Load local authority metadata in MSAL Node PCA --- .../src/client/PublicClientApplication.ts | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/lib/msal-node/src/client/PublicClientApplication.ts b/lib/msal-node/src/client/PublicClientApplication.ts index c2e55f364b..97abaf75c8 100644 --- a/lib/msal-node/src/client/PublicClientApplication.ts +++ b/lib/msal-node/src/client/PublicClientApplication.ts @@ -22,6 +22,8 @@ import { AccountInfo, INativeBrokerPlugin, ServerAuthorizationCodeResponse, + AuthorityOptions, + AuthorityFactory, } from "@azure/msal-common"; import { Configuration } from "../config/Configuration.js"; import { ClientApplication } from "./ClientApplication.js"; @@ -66,6 +68,7 @@ export class PublicClientApplication */ constructor(configuration: Configuration) { super(configuration); + this.loadLocalAuthorityMetadata(); if (this.config.broker.nativeBrokerPlugin) { if (this.config.broker.nativeBrokerPlugin.isBrokerAvailable) { this.nativeBrokerPlugin = this.config.broker.nativeBrokerPlugin; @@ -80,6 +83,25 @@ export class PublicClientApplication } } + private loadLocalAuthorityMetadata(): void { + const authorityOptions: AuthorityOptions = { + protocolMode: this.config.auth.protocolMode, + knownAuthorities: this.config.auth.knownAuthorities, + cloudDiscoveryMetadata: this.config.auth.cloudDiscoveryMetadata, + authorityMetadata: this.config.auth.authorityMetadata, + }; + + const locallyDiscoveredAuthorityInstance = + AuthorityFactory.createInstance( + this.config.auth.authority, + this.config.system.networkClient, + this.storage, + authorityOptions, + this.logger + ); + locallyDiscoveredAuthorityInstance.resolveEndpointsFromLocalSources(); + } + /** * Acquires a token from the authority using OAuth2.0 device code flow. * This flow is designed for devices that do not have access to a browser or have input constraints. From 1b535775826e3022d0b333449cce5a9841f8fb3b Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Tue, 3 Oct 2023 14:28:31 -0700 Subject: [PATCH 11/91] Change files --- ...ure-msal-node-695c255d-9b72-4c81-b93a-886e651da57a.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 change/@azure-msal-node-695c255d-9b72-4c81-b93a-886e651da57a.json diff --git a/change/@azure-msal-node-695c255d-9b72-4c81-b93a-886e651da57a.json b/change/@azure-msal-node-695c255d-9b72-4c81-b93a-886e651da57a.json new file mode 100644 index 0000000000..997fdb599f --- /dev/null +++ b/change/@azure-msal-node-695c255d-9b72-4c81-b93a-886e651da57a.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "Build account objects from cached ID Token #6455", + "packageName": "@azure/msal-node", + "email": "hemoral@microsoft.com", + "dependentChangeType": "patch" +} From dbc7d8c2604c499d20974253e50958d32641997a Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Tue, 3 Oct 2023 15:44:30 -0700 Subject: [PATCH 12/91] Add multi-tenant sample --- .../app/multi-tenant/auth.js | 102 ++++++++++++++++++ .../app/multi-tenant/authConfig.js | 67 ++++++++++++ .../app/multi-tenant/graph.js | 31 ++++++ .../app/multi-tenant/index.html | 76 +++++++++++++ .../app/multi-tenant/redirect.html | 6 ++ .../app/multi-tenant/ui.js | 76 +++++++++++++ 6 files changed, 358 insertions(+) create mode 100644 samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/auth.js create mode 100644 samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/authConfig.js create mode 100644 samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/graph.js create mode 100644 samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/index.html create mode 100644 samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/redirect.html create mode 100644 samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/ui.js diff --git a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/auth.js b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/auth.js new file mode 100644 index 0000000000..22503ebc1a --- /dev/null +++ b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/auth.js @@ -0,0 +1,102 @@ +let signInType; +let accountId = ""; + +// Create the main myMSALObj instance +// configuration parameters are located at authConfig.js +const myMSALObj = new msal.PublicClientApplication(msalConfig); + +// Redirect: once login is successful and redirects with tokens, call Graph API +myMSALObj.initialize().then(() => { + myMSALObj.handleRedirectPromise().then(handleResponse).catch(err => { + console.error(err); + }); +}); + +function handleResponse() { + // when a filter is passed into getAllAccounts, it returns all cached accounts that match the filter. Use isHomeTenant filter to get the home accounts. + const homeAccounts = myMSALObj.getAllAccounts(); + console.log("Get all accounts: ", homeAccounts); + if (!homeAccounts || homeAccounts.length < 1) { + return; + } else if (homeAccounts.length === 1) { + // Get all accounts returns the homeAccount with tenantProfiles when multiTenantAccountsEnabled is set to true + pickActiveAccountAndTenantProfile(homeAccounts[0]); + } else if (homeAccounts.length > 1) { + // Select account logic + } +} + +// Determines whether there is one or multiple tenant profiles to pick from and sets the active account based on the user selection if necessary. +async function pickActiveAccountAndTenantProfile(homeAccount) { + // Set home tenant profile as default active account + let activeAccount = myMSALObj.getActiveAccount(); + if (!activeAccount) { + activeAccount = homeAccount; + myMSALObj.setActiveAccount(activeAccount); + } + accountId = activeAccount.homeAccountId; + showWelcomeMessage(activeAccount); + showTenantProfilePicker(homeAccount.tenants || [], activeAccount); +} + +async function setActiveAccount(tenantId) { + // Sets the active account to the cached account object matching the tenant profile selected by the user. + let activeAccount = myMSALObj.getActiveAccount(); + const tenantProfile = activeAccount.tenantProfiles.get(tenantId); + const newActiveAccount = myMSALObj.getAccountByFilter({ tenantProfile: tenantProfile}); + if (newActiveAccount) { + myMSALObj.setActiveAccount(newActiveAccount); + accountId = activeAccount.homeAccountId; + } + handleResponse(); +} + +async function signIn(signInType) { + if (signInType === "popup") { + return myMSALObj.loginPopup(loginRequest).then(handleResponse).catch(function (error) { + console.log(error); + }); + } else if (signInType === "redirect") { + return myMSALObj.loginRedirect(loginRequest) + } +} + +function signOut(interactionType) { + const logoutRequest = { + account: myMSALObj.getActiveAccount() + }; + + if (interactionType === "popup") { + myMSALObj.logoutPopup(logoutRequest).then(() => { + window.location.reload(); + }); + } else { + myMSALObj.logoutRedirect(logoutRequest); + } +} + +async function requestGuestToken() { + const currentAcc = myMSALObj.getAccountByHomeId(accountId); + if (currentAcc) { + const response = await getTokenRedirect({ ...guestTenantRequest, account: currentAcc }).catch(error => { + console.log(error); + }); + console.log(response); + callMSGraph(graphConfig.graphMeEndpoint, response.accessToken, updateUI); + guestProfileButton.style.display = 'none'; + } +} + +async function getTokenRedirect(request) { + console.log("Request: ", request); + return await myMSALObj.acquireTokenSilent(request).catch(async (error) => { + console.log("silent token acquisition fails."); + if (error instanceof msal.InteractionRequiredAuthError) { + // fallback to interaction when silent call fails + console.log("acquiring token using redirect"); + myMSALObj.acquireTokenRedirect(request); + } else { + console.error(error); + } + }); +} \ No newline at end of file diff --git a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/authConfig.js b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/authConfig.js new file mode 100644 index 0000000000..a66b2d2cc7 --- /dev/null +++ b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/authConfig.js @@ -0,0 +1,67 @@ +const homeTenant = "common"; +const guestTenant = "5d97b14d-c396-4aee-b524-c86d33e9b660" +const baseAuthority = "https://login.microsoftonline.com" + +// Config object to be passed to Msal on creation +const msalConfig = { + auth: { + clientId: "bc77b0a7-16aa-4af4-884b-41b968c9c71a", + authority: `${baseAuthority}/${homeTenant}` + }, + cache: { + cacheLocation: "sessionStorage", // This configures where your cache will be stored + storeAuthStateInCookie: false, // Set this to "true" if you are having issues on IE11 or Edge + }, + system: { + loggerOptions: { + logLevel: msal.LogLevel.Trace, + loggerCallback: (level, message, containsPii) => { + if (containsPii) { + return; + } + switch (level) { + case msal.LogLevel.Error: + console.error(message); + return; + case msal.LogLevel.Info: + console.info(message); + return; + case msal.LogLevel.Verbose: + console.debug(message); + return; + case msal.LogLevel.Warning: + console.warn(message); + return; + default: + console.log(message); + return; + } + } + } + }, + telemetry: { + application: { + appName: "MSAL Browser V2 Multi-tenant Sample", + appVersion: "1.0.0" + } + } +}; + +// Add here scopes for id token to be used at MS Identity Platform endpoints. +const loginRequest = { + scopes: ["User.Read"] +}; + +// Add here the endpoints for MS Graph API services you would like to use. +const graphConfig = { + graphMeEndpoint: "https://graph.microsoft.com/v1.0/me", +}; + +const silentRequest = { + scopes: ["openid", "profile", "User.Read"] +}; + +const guestTenantRequest = { + ...loginRequest, + authority: `${baseAuthority}/${guestTenant}` +} \ No newline at end of file diff --git a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/graph.js b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/graph.js new file mode 100644 index 0000000000..5b8e4f5e30 --- /dev/null +++ b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/graph.js @@ -0,0 +1,31 @@ +// Helper function to call MS Graph API endpoint +// using authorization bearer token scheme +function callMSGraph(endpoint, accessToken, callback) { + const headers = new Headers(); + const bearer = `Bearer ${accessToken}`; + + headers.append("Authorization", bearer); + + const options = { + method: "GET", + headers: headers + }; + + console.log('request made to Graph API at: ' + new Date().toString()); + + fetch(endpoint, options) + .then(response => response.json()) + .then(response => callback(response, endpoint)) + .catch(error => console.log(error)); +} + +async function seeProfile() { + const currentAcc = myMSALObj.getActiveAccount(); + if (currentAcc) { + const request = {...loginRequest, authority: `${baseAuthority}/${currentAcc.tenantId}`}; + const response = await getTokenRedirect(request, currentAcc).catch(error => { + console.log(error); + }); + callMSGraph(graphConfig.graphMeEndpoint, response.accessToken, updateUI); + } +} \ No newline at end of file diff --git a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/index.html b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/index.html new file mode 100644 index 0000000000..68c5040ad1 --- /dev/null +++ b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/index.html @@ -0,0 +1,76 @@ + + + + + + Quickstart | MSAL.JS Vanilla JavaScript SPA + + + + + + + + + +
+
Vanilla JavaScript SPA calling MS Graph API with MSAL.JS (w/ Multiple Tenants)
+
+
+ +
+
+ +
+
+
+
+
+ +
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/redirect.html b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/redirect.html new file mode 100644 index 0000000000..dcc8b0ed7d --- /dev/null +++ b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/redirect.html @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/ui.js b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/ui.js new file mode 100644 index 0000000000..a42286c33d --- /dev/null +++ b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/ui.js @@ -0,0 +1,76 @@ +// Select DOM elements to work with +const welcomeDiv = document.getElementById("WelcomeMessage"); +const pickProfileMessage = document.getElementById("PickProfileMessage"); +const signInDiv = document.getElementById("sign-in-div"); +const signOutDiv = document.getElementById("sign-out-div"); +const signInButton = document.getElementById("SignIn"); +const signOutButton = document.getElementById("SignOut"); +const popupButton = document.getElementById("popup"); +const redirectButton = document.getElementById("redirect"); +const cardDivTenantProfiles = document.getElementById("card-div-profiles"); +const cardDivGraphProfiles = document.getElementById("card-div-graph"); +const mailButton = document.getElementById("readMail"); +const profileButton = document.getElementById("seeProfile"); +const guestProfileButton = document.getElementById("seeProfileGuest"); +const tenantProfileDiv = document.getElementById("tenant-profile-div"); +const graphProfileDiv = document.getElementById("graph-profile-div"); + +function showWelcomeMessage(activeAccount) { + // Reconfiguring DOM elements + cardDivGraphProfiles.style.display = 'initial'; + welcomeDiv.innerHTML = `Welcome ${activeAccount.name}`; + signInButton.setAttribute('class', "btn btn-success dropdown-toggle"); + signInButton.innerHTML = "Sign Out"; + popupButton.setAttribute('onClick', "signOut(this.id)"); + popupButton.innerHTML = "Sign Out with Popup"; + redirectButton.setAttribute('onClick', "signOut(this.id)"); + redirectButton.innerHTML = "Sign Out with Redirect"; +} + +function createTenantProfileButton(tenantProfile, activeTenantId) { + const tenantIdButton = document.createElement('button'); + const styleClass = (tenantProfile === activeTenantId) ? "btn btn-success" : "btn btn-primary"; + tenantIdButton.setAttribute('class', styleClass); + tenantIdButton.setAttribute('id', tenantProfile); + tenantIdButton.setAttribute('onClick', "setActiveAccount(this.id)"); + const label = `${tenantProfile}${ tenantProfile.isHomeTenant ? " (Home Tenant)" : "" }`; + tenantIdButton.innerHTML = label; + return tenantIdButton; +} + +function showTenantProfilePicker(tenantProfiles, activeAccount) { + // Reconfiguring DOM elements + cardDivTenantProfiles.style.display = 'initial'; + pickProfileMessage.innerHTML = "Select a tenant profile to set as the active account"; + tenantProfileDiv.innerHTML = ""; + tenantProfiles.forEach(function (profile) { + tenantProfileDiv.appendChild(createTenantProfileButton(profile, activeAccount.tenantId)); + tenantProfileDiv.appendChild(document.createElement('br')); + tenantProfileDiv.appendChild(document.createElement('br')); + }); + const newTenantProfileButton = document.createElement('button'); + newTenantProfileButton.setAttribute('class', "btn btn-primary"); + newTenantProfileButton.setAttribute('id', "new-tenant-button"); + newTenantProfileButton.setAttribute('onClick', "requestGuestToken()"); + newTenantProfileButton.innerHTML = "Add New Tenant Profile"; + tenantProfileDiv.appendChild(newTenantProfileButton); +} + +function updateUI(data, endpoint) { + console.log('Graph API responded at: ' + new Date().toString()); + if (endpoint === graphConfig.graphMeEndpoint) { + const title = document.createElement('p'); + title.innerHTML = "Title: " + data.jobTitle; + const email = document.createElement('p'); + email.innerHTML = "Mail: " + data.mail; + const phone = document.createElement('p'); + phone.innerHTML = "Phone: " + data.businessPhones[0]; + const address = document.createElement('p'); + address.innerHTML = "Location: " + data.officeLocation; + graphProfileDiv.innerHTML = ""; + graphProfileDiv.appendChild(title); + graphProfileDiv.appendChild(email); + graphProfileDiv.appendChild(phone); + graphProfileDiv.appendChild(address); + } +} From 6e964e2d758bbeaf6e514df346913772b192383f Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Wed, 4 Oct 2023 09:41:09 -0700 Subject: [PATCH 13/91] Update change/@azure-msal-node-695c255d-9b72-4c81-b93a-886e651da57a.json Co-authored-by: Sameera Gajjarapu --- .../@azure-msal-node-695c255d-9b72-4c81-b93a-886e651da57a.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/change/@azure-msal-node-695c255d-9b72-4c81-b93a-886e651da57a.json b/change/@azure-msal-node-695c255d-9b72-4c81-b93a-886e651da57a.json index 997fdb599f..c0a35fbeb4 100644 --- a/change/@azure-msal-node-695c255d-9b72-4c81-b93a-886e651da57a.json +++ b/change/@azure-msal-node-695c255d-9b72-4c81-b93a-886e651da57a.json @@ -1,6 +1,6 @@ { "type": "minor", - "comment": "Build account objects from cached ID Token #6455", + "comment": "Build account objects from cached ID Token #6529", "packageName": "@azure/msal-node", "email": "hemoral@microsoft.com", "dependentChangeType": "patch" From 2888e74916c164eda03f88beb932d28c267d02d1 Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Wed, 4 Oct 2023 15:37:43 -0700 Subject: [PATCH 14/91] Add support for cross-tenant token caching and multi-tenant accounts --- .../src/cache/BrowserCacheManager.ts | 72 +----- .../src/controllers/StandardController.ts | 8 +- lib/msal-common/src/account/AccountInfo.ts | 3 + lib/msal-common/src/authority/Authority.ts | 16 ++ lib/msal-common/src/cache/CacheManager.ts | 237 ++++++++++++------ .../src/cache/entities/AccountEntity.ts | 76 ++++-- lib/msal-common/src/cache/utils/CacheTypes.ts | 6 + .../src/response/ResponseHandler.ts | 61 ++++- .../test/cache/CacheManager.spec.ts | 111 ++++---- lib/msal-common/test/cache/MockCache.ts | 38 --- 10 files changed, 358 insertions(+), 270 deletions(-) diff --git a/lib/msal-browser/src/cache/BrowserCacheManager.ts b/lib/msal-browser/src/cache/BrowserCacheManager.ts index 664a937c55..2eb78d357b 100644 --- a/lib/msal-browser/src/cache/BrowserCacheManager.ts +++ b/lib/msal-browser/src/cache/BrowserCacheManager.ts @@ -1028,10 +1028,9 @@ export class BrowserCacheManager extends CacheManager { ); return null; } - const activeAccount = - this.getAccountInfoByFilter({ - localAccountId: activeAccountValueLocal, - })[0] || null; + const activeAccount = this.getAccountInfoFilteredBy({ + localAccountId: activeAccountValueLocal, + }); if (activeAccount) { this.logger.trace( "BrowserCacheManager.getActiveAccount: Legacy active account cache schema found" @@ -1051,12 +1050,11 @@ export class BrowserCacheManager extends CacheManager { this.logger.trace( "BrowserCacheManager.getActiveAccount: Active account filters schema found" ); - return ( - this.getAccountInfoByFilter({ - homeAccountId: activeAccountValueObj.homeAccountId, - localAccountId: activeAccountValueObj.localAccountId, - })[0] || null - ); + return this.getAccountInfoFilteredBy({ + homeAccountId: activeAccountValueObj.homeAccountId, + localAccountId: activeAccountValueObj.localAccountId, + tenantId: activeAccountValueObj.tenantId, + }); } this.logger.trace( "BrowserCacheManager.getActiveAccount: No active account found" @@ -1080,6 +1078,7 @@ export class BrowserCacheManager extends CacheManager { const activeAccountValue: ActiveAccountFilters = { homeAccountId: account.homeAccountId, localAccountId: account.localAccountId, + tenantId: account.tenantId, }; this.browserStorage.setItem( activeAccountKey, @@ -1098,59 +1097,6 @@ export class BrowserCacheManager extends CacheManager { } } - /** - * Gets a list of accounts that match all of the filters provided - * @param account - */ - getAccountInfoByFilter( - accountFilter: Partial> - ): AccountInfo[] { - const allAccounts = this.getAllAccounts(); - this.logger.trace( - `BrowserCacheManager.getAccountInfoByFilter: total ${allAccounts.length} accounts found` - ); - - return allAccounts.filter((accountObj) => { - if ( - accountFilter.username && - accountFilter.username.toLowerCase() !== - accountObj.username.toLowerCase() - ) { - return false; - } - - if ( - accountFilter.homeAccountId && - accountFilter.homeAccountId !== accountObj.homeAccountId - ) { - return false; - } - - if ( - accountFilter.localAccountId && - accountFilter.localAccountId !== accountObj.localAccountId - ) { - return false; - } - - if ( - accountFilter.tenantId && - accountFilter.tenantId !== accountObj.tenantId - ) { - return false; - } - - if ( - accountFilter.environment && - accountFilter.environment !== accountObj.environment - ) { - return false; - } - - return true; - }); - } - /** * Checks the cache for accounts matching loginHint or SID * @param loginHint diff --git a/lib/msal-browser/src/controllers/StandardController.ts b/lib/msal-browser/src/controllers/StandardController.ts index 1a7fb2ecfa..eb7c8225bb 100644 --- a/lib/msal-browser/src/controllers/StandardController.ts +++ b/lib/msal-browser/src/controllers/StandardController.ts @@ -1584,10 +1584,10 @@ export class StandardController implements IController { ): string { const account = request.account || - this.browserStorage.getAccountInfoByHints( - request.loginHint, - request.sid - ) || + this.getAccount({ + loginHint: request.loginHint, + sid: request.sid, + }) || this.getActiveAccount(); return (account && account.nativeAccountId) || ""; diff --git a/lib/msal-common/src/account/AccountInfo.ts b/lib/msal-common/src/account/AccountInfo.ts index 3471373ae9..b35ff5177f 100644 --- a/lib/msal-common/src/account/AccountInfo.ts +++ b/lib/msal-common/src/account/AccountInfo.ts @@ -15,6 +15,7 @@ import { TokenClaims } from "./TokenClaims"; * - idToken - raw ID token * - idTokenClaims - Object contains claims from ID token * - nativeAccountId - The user's native account ID + * - tenants - Array of strings that represent a unique identifier for the tenants that the account has been authenticated with. */ export type AccountInfo = { homeAccountId: string; @@ -35,9 +36,11 @@ export type AccountInfo = { }; nativeAccountId?: string; authorityType?: string; + tenants?: string[]; }; export type ActiveAccountFilters = { homeAccountId: string; localAccountId: string; + tenantId: string; }; diff --git a/lib/msal-common/src/authority/Authority.ts b/lib/msal-common/src/authority/Authority.ts index 39fd1c8650..fba3b0df11 100644 --- a/lib/msal-common/src/authority/Authority.ts +++ b/lib/msal-common/src/authority/Authority.ts @@ -1314,4 +1314,20 @@ export class Authority { return ciamAuthority; } + + /** + * Extract tenantId from authority + */ + static getTenantFromAuthorityString(authority: string): string | undefined { + const authorityUrl = new UrlString(authority); + const authorityUrlComponents = authorityUrl.getUrlComponents(); + /** + * Tenant ID is the path after the domain: + * AAD Authority - domain/tenantId + * B2C Authority - domain/tenantId?/tfp?/policy + */ + return authorityUrlComponents.PathSegments.join( + Constants.FORWARD_SLASH + ); + } } diff --git a/lib/msal-common/src/cache/CacheManager.ts b/lib/msal-common/src/cache/CacheManager.ts index 8ab7704c86..547876cbda 100644 --- a/lib/msal-common/src/cache/CacheManager.ts +++ b/lib/msal-common/src/cache/CacheManager.ts @@ -10,6 +10,7 @@ import { AppMetadataFilter, AppMetadataCache, TokenKeys, + TenantProfileFilter, } from "./utils/CacheTypes"; import { CacheRecord } from "./entities/CacheRecord"; import { @@ -42,6 +43,8 @@ import { BaseAuthRequest } from "../request/BaseAuthRequest"; import { Logger } from "../logger/Logger"; import { name, version } from "../packageMetadata"; import { StoreInCache } from "../request/StoreInCache"; +import { Authority } from "../authority/Authority"; +import { TokenClaims } from "../account/TokenClaims"; /** * Interface class which implement cache storage functions used by MSAL to perform validity checks, and store tokens. @@ -226,13 +229,10 @@ export abstract class CacheManager implements ICacheManager { * @returns Array of AccountInfo objects in cache */ getAllAccounts(accountFilter?: AccountFilter): AccountInfo[] { - return this.getAccountsFilteredBy(accountFilter || {}) - .map((accountEntity) => { - return this.getAccountInfoFromEntity(accountEntity); - }) - .filter((accoutnInfo) => { - return accoutnInfo.idTokenClaims; - }); + return this.buildTenantProfiles( + this.getAccountsFilteredBy(accountFilter || {}), + accountFilter + ); } /** @@ -241,31 +241,117 @@ export abstract class CacheManager implements ICacheManager { getAccountInfoFilteredBy(accountFilter: AccountFilter): AccountInfo | null { const allAccounts = this.getAccountsFilteredBy(accountFilter); if (allAccounts.length > 0) { - const validAccounts = allAccounts - .map((accountEntity) => { - return this.getAccountInfoFromEntity(accountEntity); - }) - .filter((accountInfo) => { - return accountInfo.idTokenClaims; - }); + const validAccounts = this.buildTenantProfiles( + allAccounts, + accountFilter + ); return validAccounts[0]; } else { return null; } } + /** + * Matches filtered account entities with cached ID tokens that match the tenant profile-specific account filters + * and builds the account info objects from the matching ID token's claims + * @param cachedAccounts + * @param accountFilter + * @returns Array of AccountInfo objects that match account and tenant profile filters + */ + private buildTenantProfiles( + cachedAccounts: AccountEntity[], + accountFilter?: AccountFilter + ): AccountInfo[] { + return cachedAccounts + .map((accountEntity) => { + return this.getAccountInfoForTenantProfile( + accountEntity, + accountFilter + ); + }) + .filter((accountInfo) => { + // Tenant Profiles with matching ID tokens in the cache will have ID token claims + return accountInfo.idTokenClaims; + }); + } + + private getAccountInfoForTenantProfile( + accountEntity: AccountEntity, + accountFilter?: AccountFilter + ): AccountInfo { + const tenantProfileFilter: TenantProfileFilter = { + localAccountId: accountFilter?.localAccountId, + loginHint: accountFilter?.loginHint, + name: accountFilter?.name, + sid: accountFilter?.sid, + }; + return this.getAccountInfoFromEntity( + accountEntity, + accountFilter?.tenantId, + tenantProfileFilter + ); + } + private getAccountInfoFromEntity( - accountEntity: AccountEntity + accountEntity: AccountEntity, + targetTenantId?: string, + tenantProfileFilter?: TenantProfileFilter ): AccountInfo { - const accountInfo = accountEntity.getAccountInfo(); - const idToken = this.getIdToken(accountInfo); + let accountInfo = accountEntity.getAccountInfo(); + const idToken = this.getIdToken(accountInfo, undefined, targetTenantId); + if (idToken) { - accountInfo.idToken = idToken.secret; - accountInfo.idTokenClaims = extractTokenClaims( + const idTokenClaims = extractTokenClaims( idToken.secret, this.cryptoImpl.base64Decode ); + // Tenant Profile filtering + if (tenantProfileFilter) { + if ( + !!tenantProfileFilter.localAccountId && + !this.matchLocalAccountId( + idTokenClaims, + tenantProfileFilter.localAccountId + ) + ) { + return accountInfo; + } + + if ( + !!tenantProfileFilter.loginHint && + !this.matchLoginHint( + idTokenClaims, + tenantProfileFilter.loginHint + ) + ) { + return accountInfo; + } + + if ( + !!tenantProfileFilter.name && + !this.matchName(idTokenClaims, tenantProfileFilter.name) + ) { + return accountInfo; + } + + if ( + !!tenantProfileFilter.sid && + !this.matchSid(idTokenClaims, tenantProfileFilter.sid) + ) { + return accountInfo; + } + } + + accountInfo = { + ...accountInfo, + idToken: idToken.secret, + idTokenClaims: idTokenClaims, + localAccountId: idTokenClaims.oid || accountInfo.localAccountId, + name: idTokenClaims.name || accountInfo.name, + tenantId: idToken.realm || accountInfo.tenantId, + }; } + return accountInfo; } @@ -363,13 +449,7 @@ export abstract class CacheManager implements ICacheManager { const matchingAccounts: AccountEntity[] = []; allAccountKeys.forEach((cacheKey) => { - if ( - !this.isAccountKey( - cacheKey, - accountFilter.homeAccountId, - accountFilter.tenantId - ) - ) { + if (!this.isAccountKey(cacheKey, accountFilter.homeAccountId)) { // Don't parse value if the key doesn't match the account filters return; } @@ -387,13 +467,6 @@ export abstract class CacheManager implements ICacheManager { return; } - if ( - !!accountFilter.localAccountId && - !this.matchLocalAccountId(entity, accountFilter.localAccountId) - ) { - return; - } - if ( !!accountFilter.username && !this.matchUsername(entity, accountFilter.username) @@ -415,14 +488,6 @@ export abstract class CacheManager implements ICacheManager { return; } - // tenantId is another name for realm - if ( - !!accountFilter.tenantId && - !this.matchRealm(entity, accountFilter.tenantId) - ) { - return; - } - if ( !!accountFilter.nativeAccountId && !this.matchNativeAccountId( @@ -433,13 +498,6 @@ export abstract class CacheManager implements ICacheManager { return; } - if ( - !!accountFilter.loginHint && - !this.matchLoginHint(entity, accountFilter.loginHint) - ) { - return; - } - if ( !!accountFilter.authorityType && !this.matchAuthorityType(entity, accountFilter.authorityType) @@ -447,13 +505,6 @@ export abstract class CacheManager implements ICacheManager { return; } - if ( - !!accountFilter.name && - !this.matchName(entity, accountFilter.name) - ) { - return; - } - matchingAccounts.push(entity); }); @@ -836,13 +887,21 @@ export abstract class CacheManager implements ICacheManager { request: BaseAuthRequest, environment: string ): CacheRecord { + const requestTenantId = Authority.getTenantFromAuthorityString( + request.authority + ); const tokenKeys = this.getTokenKeys(); const cachedAccount = this.readAccountFromCache(account); - const cachedIdToken = this.getIdToken(account, tokenKeys); + const cachedIdToken = this.getIdToken( + account, + tokenKeys, + requestTenantId + ); const cachedAccessToken = this.getAccessToken( account, request, - tokenKeys + tokenKeys, + requestTenantId ); const cachedRefreshToken = this.getRefreshToken( account, @@ -852,10 +911,16 @@ export abstract class CacheManager implements ICacheManager { const cachedAppMetadata = this.readAppMetadataFromCache(environment); if (cachedAccount && cachedIdToken) { - cachedAccount.idTokenClaims = extractTokenClaims( + const idTokenClaims = extractTokenClaims( cachedIdToken.secret, this.cryptoImpl.base64Decode ); + + cachedAccount.idTokenClaims = idTokenClaims; + cachedAccount.localAccountId = + idTokenClaims.oid || cachedAccount.localAccountId; + cachedAccount.name = idTokenClaims.name || cachedAccount.name; + cachedAccount.realm = idTokenClaims.tid || cachedAccount.realm; } return { @@ -885,7 +950,8 @@ export abstract class CacheManager implements ICacheManager { */ getIdToken( account: AccountInfo, - tokenKeys?: TokenKeys + tokenKeys?: TokenKeys, + targetRealm?: string ): IdTokenEntity | null { this.commonLogger.trace("CacheManager - getIdToken called"); const idTokenFilter: CredentialFilter = { @@ -893,7 +959,7 @@ export abstract class CacheManager implements ICacheManager { environment: account.environment, credentialType: CredentialType.ID_TOKEN, clientId: this.clientId, - realm: account.tenantId, + realm: targetRealm || account.tenantId, }; const idTokens: IdTokenEntity[] = this.getIdTokensByFilter( @@ -1005,7 +1071,8 @@ export abstract class CacheManager implements ICacheManager { getAccessToken( account: AccountInfo, request: BaseAuthRequest, - tokenKeys?: TokenKeys + tokenKeys?: TokenKeys, + targetRealm?: string ): AccessTokenEntity | null { this.commonLogger.trace("CacheManager - getAccessToken called"); const scopes = ScopeSet.createSearchScopes(request.scopes); @@ -1027,7 +1094,7 @@ export abstract class CacheManager implements ICacheManager { environment: account.environment, credentialType: credentialType, clientId: this.clientId, - realm: account.tenantId, + realm: targetRealm || account.tenantId, target: scopes, tokenType: authScheme, keyId: request.sshKid, @@ -1319,13 +1386,31 @@ export abstract class CacheManager implements ICacheManager { * @returns */ private matchLocalAccountId( - entity: AccountEntity, + claims: TokenClaims, localAccountId: string ): boolean { - return !!( - typeof entity.localAccountId === "string" && - localAccountId === entity.localAccountId - ); + const idTokenLocalAccountId = claims.oid || claims.sub; + return localAccountId === idTokenLocalAccountId; + } + + /** + * helper to match names + * @param entity + * @param name + * @returns true if the downcased name properties are present and match in the filter and the entity + */ + private matchName(claims: TokenClaims, name: string): boolean { + return !!(name.toLowerCase() === claims.name?.toLowerCase()); + } + + /** + * helper to match sid + * @param claims + * @param sid + * @returns true if the sid claim in the token matches the sid in the filter + */ + private matchSid(claims: TokenClaims, sid: string): boolean { + return !!(sid === claims.sid); } /** @@ -1341,16 +1426,6 @@ export abstract class CacheManager implements ICacheManager { ); } - /** - * helper to match names - * @param entity - * @param name - * @returns true if the downcased name properties are present and match in the filter and the entity - */ - private matchName(entity: AccountEntity, name: string): boolean { - return !!(name.toLowerCase() === entity.name?.toLowerCase()); - } - /** * helper to match assertion * @param value @@ -1461,16 +1536,16 @@ export abstract class CacheManager implements ICacheManager { * @param loginHint * @returns */ - private matchLoginHint(entity: AccountEntity, loginHint: string): boolean { - if (entity.idTokenClaims?.login_hint === loginHint) { + private matchLoginHint(claims: TokenClaims, loginHint: string): boolean { + if (claims.login_hint === loginHint) { return true; } - if (entity.username === loginHint) { + if (claims.preferred_username === loginHint) { return true; } - if (entity.idTokenClaims?.upn === loginHint) { + if (claims.upn === loginHint) { return true; } diff --git a/lib/msal-common/src/cache/entities/AccountEntity.ts b/lib/msal-common/src/cache/entities/AccountEntity.ts index 95f13aeafe..0039bb0358 100644 --- a/lib/msal-common/src/cache/entities/AccountEntity.ts +++ b/lib/msal-common/src/cache/entities/AccountEntity.ts @@ -6,7 +6,7 @@ import { Separators, CacheAccountType, Constants } from "../../utils/Constants"; import { Authority } from "../../authority/Authority"; import { ICrypto } from "../../crypto/ICrypto"; -import { buildClientInfo } from "../../account/ClientInfo"; +import { ClientInfo, buildClientInfo } from "../../account/ClientInfo"; import { AccountInfo } from "../../account/AccountInfo"; import { createClientAuthError, @@ -55,6 +55,7 @@ export class AccountEntity { msGraphHost?: string; idTokenClaims?: TokenClaims; nativeAccountId?: string; + tenants?: string[]; /** * Generate Account Id key component as per the schema: - @@ -91,18 +92,29 @@ export class AccountEntity { idTokenClaims: this.idTokenClaims, nativeAccountId: this.nativeAccountId, authorityType: this.authorityType, + tenants: this.tenants, }; } + /** + * Update attributes that are sourced from ID token claims and vary by tenant profile + */ + updateTenantProfile(idTokenClaims: TokenClaims): void { + this.idTokenClaims = idTokenClaims; + this.realm = idTokenClaims.tid || Constants.EMPTY_STRING; + this.name = idTokenClaims.name || Constants.EMPTY_STRING; + } + /** * Generates account key from interface * @param accountInterface */ static generateAccountCacheKey(accountInterface: AccountInfo): string { + const homeTenantId = accountInterface.homeAccountId.split(".")[1]; const accountKey = [ accountInterface.homeAccountId, accountInterface.environment || Constants.EMPTY_STRING, - accountInterface.tenantId || Constants.EMPTY_STRING, + homeTenantId || accountInterface.tenantId || Constants.EMPTY_STRING, ]; return accountKey.join(Separators.CACHE_KEY_SEPARATOR).toLowerCase(); @@ -122,7 +134,8 @@ export class AccountEntity { environment?: string; nativeAccountId?: string; }, - authority: Authority + authority: Authority, + cryptoObj?: ICrypto ): AccountEntity { const account: AccountEntity = new AccountEntity(); @@ -134,6 +147,19 @@ export class AccountEntity { account.authorityType = CacheAccountType.GENERIC_ACCOUNT_TYPE; } + let clientInfo: ClientInfo | undefined; + + try { + if (accountDetails.clientInfo && cryptoObj) { + clientInfo = buildClientInfo( + accountDetails.clientInfo, + cryptoObj + ); + } + } catch (e) { + throw e; + } + account.clientInfo = accountDetails.clientInfo; account.homeAccountId = accountDetails.homeAccountId; account.nativeAccountId = accountDetails.nativeAccountId; @@ -151,12 +177,15 @@ export class AccountEntity { account.environment = env; // non AAD scenarios can have empty realm account.realm = - accountDetails.idTokenClaims.tid || Constants.EMPTY_STRING; + clientInfo?.utid || + accountDetails.idTokenClaims.tid || + Constants.EMPTY_STRING; account.idTokenClaims = accountDetails.idTokenClaims; // How do you account for MSA CID here? account.localAccountId = + clientInfo?.uid || accountDetails.idTokenClaims.oid || accountDetails.idTokenClaims.sub || Constants.EMPTY_STRING; @@ -227,31 +256,30 @@ export class AccountEntity { cryptoObj: ICrypto, idTokenClaims?: TokenClaims ): string { - const accountId = idTokenClaims?.sub - ? idTokenClaims.sub - : Constants.EMPTY_STRING; - - // since ADFS does not have tid and does not set client_info + // since ADFS/DSTS do not have tid and does not set client_info if ( - authType === AuthorityType.Adfs || - authType === AuthorityType.Dsts + !( + authType === AuthorityType.Adfs || + authType === AuthorityType.Dsts + ) ) { - return accountId; - } - - // for cases where there is clientInfo - if (serverClientInfo) { - try { - const clientInfo = buildClientInfo(serverClientInfo, cryptoObj); - if (clientInfo.uid && clientInfo.utid) { - return `${clientInfo.uid}${Separators.CLIENT_INFO_SEPARATOR}${clientInfo.utid}`; - } - } catch (e) {} + // for cases where there is clientInfo + if (serverClientInfo) { + try { + const clientInfo = buildClientInfo( + serverClientInfo, + cryptoObj + ); + if (clientInfo.uid && clientInfo.utid) { + return `${clientInfo.uid}${Separators.CLIENT_INFO_SEPARATOR}${clientInfo.utid}`; + } + } catch (e) {} + } + logger.verbose("No client info in response"); } // default to "sub" claim - logger.verbose("No client info in response"); - return accountId; + return idTokenClaims?.sub ? idTokenClaims.sub : Constants.EMPTY_STRING; } /** diff --git a/lib/msal-common/src/cache/utils/CacheTypes.ts b/lib/msal-common/src/cache/utils/CacheTypes.ts index ca6f41f5c8..e083ba76d8 100644 --- a/lib/msal-common/src/cache/utils/CacheTypes.ts +++ b/lib/msal-common/src/cache/utils/CacheTypes.ts @@ -59,8 +59,14 @@ export type AccountFilter = Omit< > & { realm?: string; loginHint?: string; + sid?: string; }; +export type TenantProfileFilter = Pick< + AccountFilter, + "localAccountId" | "loginHint" | "name" | "sid" +>; + /** * Credential: ------ */ diff --git a/lib/msal-common/src/response/ResponseHandler.ts b/lib/msal-common/src/response/ResponseHandler.ts index ecd3c69b21..fdc7d5b9d0 100644 --- a/lib/msal-common/src/response/ResponseHandler.ts +++ b/lib/msal-common/src/response/ResponseHandler.ts @@ -375,6 +375,7 @@ export class ResponseHandler { await this.persistencePlugin.afterCacheAccess(cacheContext); } } + return ResponseHandler.generateAuthenticationResult( this.cryptoObj, authority, @@ -422,15 +423,11 @@ export class ResponseHandler { idTokenClaims.tid || "" ); - cachedAccount = AccountEntity.createAccount( - { - homeAccountId: this.homeAccountIdentifier, - idTokenClaims: idTokenClaims, - clientInfo: serverTokenResponse.client_info, - cloudGraphHostName: authCodePayload?.cloud_graph_host_name, - msGraphHost: authCodePayload?.msgraph_host, - }, - authority + cachedAccount = this.setCachedAccount( + authority, + serverTokenResponse, + idTokenClaims, + authCodePayload ); } @@ -518,6 +515,48 @@ export class ResponseHandler { ); } + private setCachedAccount( + authority: Authority, + serverTokenResponse: ServerAuthorizationTokenResponse, + idTokenClaims: TokenClaims, + authCodePayload?: AuthorizationCodePayload + ): AccountEntity | undefined { + this.logger.verbose("setCachedAccount called"); + const responseTenantId = idTokenClaims.tid || Constants.EMPTY_STRING; + + // Check if base account is already cached + const accountKeys = this.cacheStorage.getAccountKeys(); + const baseAccountKey = accountKeys.find((accountKey: string) => { + return accountKey.startsWith(this.homeAccountIdentifier); + }); + + let cachedAccount: AccountEntity | null = null; + if (baseAccountKey) { + cachedAccount = this.cacheStorage.getAccount(baseAccountKey); + } + + const baseAccount = + cachedAccount || + AccountEntity.createAccount( + { + homeAccountId: this.homeAccountIdentifier, + idTokenClaims: idTokenClaims, + clientInfo: serverTokenResponse.client_info, + cloudGraphHostName: authCodePayload?.cloud_graph_host_name, + msGraphHost: authCodePayload?.msgraph_host, + }, + authority, + this.cryptoObj + ); + + const tenants = baseAccount.tenants || []; + if (!tenants.includes(responseTenantId)) { + tenants.push(responseTenantId); + } + baseAccount.tenants = tenants; + return baseAccount; + } + /** * Creates an @AuthenticationResult from @CacheRecord , @IdToken , and a boolean that states whether or not the result is from cache. * @@ -584,6 +623,10 @@ export class ResponseHandler { } } + if (cacheRecord.account && idTokenClaims) { + cacheRecord.account.updateTenantProfile(idTokenClaims); + } + if (cacheRecord.appMetadata) { familyId = cacheRecord.appMetadata.familyId === THE_FAMILY_ID diff --git a/lib/msal-common/test/cache/CacheManager.spec.ts b/lib/msal-common/test/cache/CacheManager.spec.ts index 59d5c31ee9..397a22bdb5 100644 --- a/lib/msal-common/test/cache/CacheManager.spec.ts +++ b/lib/msal-common/test/cache/CacheManager.spec.ts @@ -261,7 +261,7 @@ describe("CacheManager.ts test cases", () => { }); }); - it("getAccounts (gets all AccountInfo objects)", async () => { + it("getAllAccounts (gets all AccountInfo objects)", async () => { const accounts = mockCache.cacheManager.getAllAccounts(); expect(accounts).not.toBeNull(); @@ -269,6 +269,60 @@ describe("CacheManager.ts test cases", () => { expect(accounts[0].idTokenClaims).toEqual(ID_TOKEN_CLAIMS); }); + describe("getAllAccounts with loginHint filter", () => { + it("loginHint filter matching login_hint ID token claim", () => { + // filter by loginHint = login_hint + const loginHint = "testLoginHint"; + const successFilter: AccountFilter = { + loginHint: loginHint, + }; + jest.spyOn(mockCrypto, "base64Decode").mockReturnValueOnce( + JSON.stringify({ login_hint: loginHint }) + ); + let accounts = mockCache.cacheManager.getAllAccounts(successFilter); + expect(accounts.length).toEqual(1); + + const wrongFilter: AccountFilter = { loginHint: "WrongHint" }; + accounts = mockCache.cacheManager.getAllAccounts(wrongFilter); + expect(accounts.length).toBe(0); + }); + + it("loginHint filter matching username", () => { + // filter by loginHint = preferred_username + const username = "janedoe@microsoft.com"; + const successFilter: AccountFilter = { + loginHint: username, + }; + jest.spyOn(mockCrypto, "base64Decode").mockReturnValueOnce( + JSON.stringify({ preferred_username: username }) + ); + let accounts = mockCache.cacheManager.getAllAccounts(successFilter); + expect(accounts.length).toEqual(1); + + const wrongFilter: AccountFilter = { loginHint: "WrongHint" }; + accounts = mockCache.cacheManager.getAllAccounts(wrongFilter); + expect(accounts.length).toBe(0); + }); + + it("loginHint filter matching upn ID token claim", () => { + // filter by loginHint = upn + const upn = "testUpn"; + const successFilter: AccountFilter = { + loginHint: upn, + }; + + jest.spyOn(mockCrypto, "base64Decode").mockReturnValueOnce( + JSON.stringify({ upn: upn }) + ); + let accounts = mockCache.cacheManager.getAllAccounts(successFilter); + expect(accounts.length).toEqual(1); + + const wrongFilter: AccountFilter = { loginHint: "WrongHint" }; + accounts = mockCache.cacheManager.getAllAccounts(wrongFilter); + expect(accounts.length).toBe(0); + }); + }); + it("getAccount (gets one AccountEntity object)", async () => { const ac = new AccountEntity(); ac.homeAccountId = "someUid.someUtid"; @@ -360,7 +414,7 @@ describe("CacheManager.ts test cases", () => { }; let accounts = mockCache.cacheManager.getAccountsFilteredBy(successFilter); - expect(Object.keys(accounts).length).toEqual(3); + expect(Object.keys(accounts).length).toEqual(1); sinon.restore(); const wrongFilter: AccountFilter = { environment: "Wrong Env" }; @@ -374,7 +428,7 @@ describe("CacheManager.ts test cases", () => { const successFilter: AccountFilter = { realm: "microsoft" }; let accounts = mockCache.cacheManager.getAccountsFilteredBy(successFilter); - expect(Object.keys(accounts).length).toEqual(3); + expect(Object.keys(accounts).length).toEqual(1); const wrongFilter: AccountFilter = { realm: "Wrong Realm" }; accounts = @@ -396,51 +450,6 @@ describe("CacheManager.ts test cases", () => { mockCache.cacheManager.getAccountsFilteredBy(wrongFilter); expect(Object.keys(accounts).length).toEqual(0); }); - - it("loginHint filter matching login_hint ID token claim", () => { - // filter by loginHint - const successFilter: AccountFilter = { - loginHint: "testLoginHint", - }; - let accounts = - mockCache.cacheManager.getAccountsFilteredBy(successFilter); - expect(accounts.length).toEqual(1); - - const wrongFilter: AccountFilter = { loginHint: "WrongHint" }; - accounts = - mockCache.cacheManager.getAccountsFilteredBy(wrongFilter); - expect(accounts.length).toBe(0); - }); - - it("loginHint filter matching username", () => { - // filter by loginHint - const successFilter: AccountFilter = { - loginHint: "Jane Doe", - }; - let accounts = - mockCache.cacheManager.getAccountsFilteredBy(successFilter); - expect(accounts.length).toEqual(1); - - const wrongFilter: AccountFilter = { loginHint: "WrongHint" }; - accounts = - mockCache.cacheManager.getAccountsFilteredBy(wrongFilter); - expect(accounts.length).toBe(0); - }); - - it("loginHint filter matching upn ID token claim", () => { - // filter by loginHint - const successFilter: AccountFilter = { - loginHint: "testUpn", - }; - let accounts = - mockCache.cacheManager.getAccountsFilteredBy(successFilter); - expect(accounts.length).toEqual(1); - - const wrongFilter: AccountFilter = { loginHint: "WrongHint" }; - accounts = - mockCache.cacheManager.getAccountsFilteredBy(wrongFilter); - expect(accounts.length).toBe(0); - }); }); describe("isCredentialKey", () => { @@ -1048,15 +1057,15 @@ describe("CacheManager.ts test cases", () => { it("removeAccount", async () => { expect( mockCache.cacheManager.getAccount( - "uid.utid-login.microsoftonline.com-microsoft" + "uid.utid-login.microsoftonline.com-utid" ) ).not.toBeNull(); await mockCache.cacheManager.removeAccount( - "uid.utid-login.microsoftonline.com-microsoft" + "uid.utid-login.microsoftonline.com-utid" ); expect( mockCache.cacheManager.getAccount( - "uid.utid-login.microsoftonline.com-microsoft" + "uid.utid-login.microsoftonline.com-utid" ) ).toBeNull(); }); diff --git a/lib/msal-common/test/cache/MockCache.ts b/lib/msal-common/test/cache/MockCache.ts index 2f5254fbeb..6d04576c23 100644 --- a/lib/msal-common/test/cache/MockCache.ts +++ b/lib/msal-common/test/cache/MockCache.ts @@ -72,44 +72,6 @@ export class MockCache { accountDataWithNativeAccountId ); this.cacheManager.setAccount(accountWithNativeAccountId); - - // Accounts with ID Token Claims - - const accountDataWithLoginHint = { - username: "Jane Doe", - localAccountId: "object4321", - realm: "microsoft", - environment: "login.microsoftonline.com", - homeAccountId: "homeAccountId2", - authorityType: "MSSTS", - clientInfo: "eyJ1aWQiOiJ1aWQiLCAidXRpZCI6InV0aWQifQ==", - idTokenClaims: { - login_hint: "testLoginHint", - }, - }; - const accountWithLoginHint = CacheManager.toObject( - new AccountEntity(), - accountDataWithLoginHint - ); - this.cacheManager.setAccount(accountWithLoginHint); - - const accountDataWithUpn = { - username: "Another Doe", - localAccountId: "object4321", - realm: "microsoft", - environment: "login.microsoftonline.com", - homeAccountId: "homeAccountId3", - authorityType: "MSSTS", - clientInfo: "eyJ1aWQiOiJ1aWQiLCAidXRpZCI6InV0aWQifQ==", - idTokenClaims: { - upn: "testUpn", - }, - }; - const accountWithUpn = CacheManager.toObject( - new AccountEntity(), - accountDataWithUpn - ); - this.cacheManager.setAccount(accountWithUpn); } // create id token entries in the cache From 2acc32dea81102e17f8217f754790e5d02da6545 Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Thu, 5 Oct 2023 13:51:59 -0700 Subject: [PATCH 15/91] Update getAccount API tests to use test ID token secrets instead of account ID token claims --- .../test/app/PublicClientApplication.spec.ts | 47 +++++++++---------- 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/lib/msal-browser/test/app/PublicClientApplication.spec.ts b/lib/msal-browser/test/app/PublicClientApplication.spec.ts index 56a1e66dd8..902e0e7fb2 100644 --- a/lib/msal-browser/test/app/PublicClientApplication.spec.ts +++ b/lib/msal-browser/test/app/PublicClientApplication.spec.ts @@ -21,6 +21,7 @@ import { testNavUrlNoRequest, TEST_SSH_VALUES, TEST_CRYPTO_VALUES, + ID_TOKEN_ALT_CLAIMS, } from "../utils/StringConstants"; import { AuthorityMetadataEntity, @@ -4844,7 +4845,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { tenantId: TEST_DATA_CLIENT_INFO.TEST_UTID, username: "example@microsoft.com", name: "Abe Lincoln", - localAccountId: TEST_CONFIG.OID, + localAccountId: ID_TOKEN_CLAIMS.oid, idToken: TEST_TOKENS.IDTOKEN_V2, idTokenClaims: ID_TOKEN_CLAIMS, nativeAccountId: undefined, @@ -4852,7 +4853,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { const testAccount1: AccountEntity = new AccountEntity(); testAccount1.homeAccountId = testAccountInfo1.homeAccountId; - testAccount1.localAccountId = TEST_CONFIG.OID; + testAccount1.localAccountId = testAccountInfo1.localAccountId; testAccount1.environment = testAccountInfo1.environment; testAccount1.realm = testAccountInfo1.tenantId; testAccount1.username = testAccountInfo1.username; @@ -4929,16 +4930,16 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { }); }); - describe("getAccount tests", () => { + describe.only("getAccount tests", () => { // Account 1 const testAccountInfo1: AccountInfo = { authorityType: "MSSTS", homeAccountId: TEST_DATA_CLIENT_INFO.TEST_HOME_ACCOUNT_ID, environment: "login.windows.net", tenantId: TEST_DATA_CLIENT_INFO.TEST_UTID, - username: "example@microsoft.com", + username: ID_TOKEN_CLAIMS.preferred_username, name: "Abe Lincoln", - localAccountId: TEST_CONFIG.OID, + localAccountId: ID_TOKEN_CLAIMS.oid, idToken: TEST_TOKENS.IDTOKEN_V2, idTokenClaims: ID_TOKEN_CLAIMS, nativeAccountId: undefined, @@ -4972,15 +4973,15 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { tenantId: TEST_DATA_CLIENT_INFO.TEST_UTID, username: "anotherExample@microsoft.com", name: "Abe Lincoln", - localAccountId: TEST_CONFIG.OID, - idToken: TEST_TOKENS.IDTOKEN_V2, - idTokenClaims: ID_TOKEN_CLAIMS, + localAccountId: ID_TOKEN_ALT_CLAIMS.oid, + idToken: TEST_TOKENS.IDTOKEN_V2_ALT, + idTokenClaims: ID_TOKEN_ALT_CLAIMS, nativeAccountId: undefined, }; const testAccount2: AccountEntity = new AccountEntity(); testAccount2.homeAccountId = testAccountInfo2.homeAccountId; - testAccount2.localAccountId = TEST_CONFIG.OID; + testAccount2.localAccountId = ID_TOKEN_ALT_CLAIMS.oid; testAccount2.environment = testAccountInfo2.environment; testAccount2.realm = testAccountInfo2.tenantId; testAccount2.username = testAccountInfo2.username; @@ -4993,7 +4994,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { realm: testAccountInfo2.tenantId, environment: testAccountInfo2.environment, credentialType: "IdToken", - secret: TEST_TOKENS.IDTOKEN_V2, + secret: TEST_TOKENS.IDTOKEN_V2_ALT, clientId: TEST_CONFIG.MSAL_CLIENT_ID, homeAccountId: testAccountInfo2.homeAccountId, }; @@ -5008,7 +5009,6 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { name: "Abe Lincoln Two", localAccountId: TEST_CONFIG.OID, idToken: TEST_TOKENS.ID_TOKEN_V2_WITH_LOGIN_HINT, - idTokenClaims: { ...ID_TOKEN_CLAIMS, login_hint: "testLoginHint" }, nativeAccountId: undefined, }; @@ -5023,13 +5023,12 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { testAccount3.clientInfo = TEST_DATA_CLIENT_INFO.TEST_CLIENT_INFO_B64ENCODED; - testAccount3.idTokenClaims = testAccountInfo3.idTokenClaims; const idTokenData3 = { realm: testAccountInfo3.tenantId, environment: testAccountInfo3.environment, credentialType: "IdToken", - secret: TEST_TOKENS.IDTOKEN_V2, + secret: TEST_TOKENS.ID_TOKEN_V2_WITH_LOGIN_HINT, clientId: TEST_CONFIG.MSAL_CLIENT_ID, homeAccountId: testAccountInfo3.homeAccountId, login_hint: "testLoginHint", @@ -5045,7 +5044,6 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { name: "Abe Lincoln Three", localAccountId: TEST_CONFIG.OID, idToken: TEST_TOKENS.ID_TOKEN_V2_WITH_UPN, - idTokenClaims: { ...ID_TOKEN_CLAIMS, upn: "testUpn" }, nativeAccountId: undefined, }; @@ -5060,13 +5058,12 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { testAccount4.clientInfo = TEST_DATA_CLIENT_INFO.TEST_CLIENT_INFO_B64ENCODED; - testAccount4.idTokenClaims = testAccountInfo4.idTokenClaims; const idTokenData4 = { realm: testAccountInfo4.tenantId, environment: testAccountInfo4.environment, credentialType: "IdToken", - secret: TEST_TOKENS.IDTOKEN_V2, + secret: TEST_TOKENS.ID_TOKEN_V2_WITH_UPN, clientId: TEST_CONFIG.MSAL_CLIENT_ID, homeAccountId: testAccountInfo4.homeAccountId, upn: "testUpn", @@ -5156,13 +5153,13 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { }); it("getAccountByUsername returns account specified", () => { - const account = pca.getAccountByUsername("example@microsoft.com"); + const account = pca.getAccountByUsername("AbeLi@microsoft.com"); expect(account?.idToken).not.toBeUndefined(); expect(account).toEqual(testAccountInfo1); }); it("getAccountByUsername returns account specified with case mismatch", () => { - const account = pca.getAccountByUsername("Example@Microsoft.com"); + const account = pca.getAccountByUsername("abeli@Microsoft.com"); expect(account?.idToken).not.toBeUndefined(); expect(account).toEqual(testAccountInfo1); @@ -5206,7 +5203,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { }); it("getAccountByLocalId returns account specified", () => { - const account = pca.getAccountByLocalId(TEST_CONFIG.OID); + const account = pca.getAccountByLocalId(ID_TOKEN_CLAIMS.oid); expect(account?.idToken).not.toBeUndefined(); expect(account).toEqual(testAccountInfo1); }); @@ -5240,11 +5237,11 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { }); it("getAccount returns account specified using username", () => { const account = pca.getAccount({ - loginHint: "Unique Username", + loginHint: ID_TOKEN_ALT_CLAIMS.preferred_username, }); expect(account?.idToken).not.toBeUndefined(); expect(account?.homeAccountId).toEqual( - testAccountInfo3.homeAccountId + testAccountInfo2.homeAccountId ); }); it("getAccount returns account specified using upn", () => { @@ -5268,7 +5265,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { it("getAccount returns account specified using localAccountId", () => { const account = pca.getAccount({ - localAccountId: TEST_CONFIG.OID, + localAccountId: ID_TOKEN_CLAIMS.oid, }); expect(account?.idToken).not.toBeUndefined(); expect(account).toEqual(testAccountInfo1); @@ -5276,7 +5273,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { it("getAccount returns account specified using username", () => { const account = pca.getAccount({ - username: "example@microsoft.com", + username: ID_TOKEN_CLAIMS.preferred_username, }); expect(account?.idToken).not.toBeUndefined(); expect(account).toEqual(testAccountInfo1); @@ -5284,8 +5281,8 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { it("getAccount returns account specified using a combination of homeAccountId and localAccountId", () => { const account = pca.getAccount({ - homeAccountId: TEST_DATA_CLIENT_INFO.TEST_HOME_ACCOUNT_ID, - localAccountId: TEST_CONFIG.OID, + homeAccountId: testAccountInfo1.homeAccountId, + localAccountId: testAccountInfo1.localAccountId, }); expect(account?.idToken).not.toBeUndefined(); expect(account).toEqual(testAccountInfo1); From a28dab520cd77ee3d3f792925dca031ecdf9184b Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Thu, 5 Oct 2023 14:13:49 -0700 Subject: [PATCH 16/91] Update tests to reflect account caching changes for multi-tenant support --- .../test/cache/BrowserCacheManager.spec.ts | 416 ------------------ .../BaseInteractionClient.spec.ts | 25 +- .../NativeInteractionClient.spec.ts | 2 +- .../interaction_client/RedirectClient.spec.ts | 4 + .../test/utils/StringConstants.ts | 10 + .../test/cache/CacheManager.spec.ts | 249 +++++++++-- lib/msal-common/test/cache/MockCache.ts | 26 +- .../test/cache/entities/AccountEntity.spec.ts | 2 +- .../test/client/RefreshTokenClient.spec.ts | 10 +- .../test/client/SilentFlowClient.spec.ts | 4 +- .../test/test_kit/StringConstants.ts | 21 +- 11 files changed, 270 insertions(+), 499 deletions(-) diff --git a/lib/msal-browser/test/cache/BrowserCacheManager.spec.ts b/lib/msal-browser/test/cache/BrowserCacheManager.spec.ts index 338d70f60c..09632c8cf5 100644 --- a/lib/msal-browser/test/cache/BrowserCacheManager.spec.ts +++ b/lib/msal-browser/test/cache/BrowserCacheManager.spec.ts @@ -3413,422 +3413,6 @@ describe("BrowserCacheManager tests", () => { }); }); - describe("getAccountInfoByFilter", () => { - cacheConfig = { - temporaryCacheLocation: BrowserCacheLocation.SessionStorage, - cacheLocation: BrowserCacheLocation.SessionStorage, - storeAuthStateInCookie: false, - secureCookies: false, - cacheMigrationEnabled: false, - claimsBasedCachingEnabled: false, - }; - logger = new Logger({ - loggerCallback: ( - level: LogLevel, - message: string, - containsPii: boolean - ): void => {}, - piiLoggingEnabled: true, - }); - const browserStorage = new BrowserCacheManager( - TEST_CONFIG.MSAL_CLIENT_ID, - cacheConfig, - browserCrypto, - logger - ); - - const accountEntity1 = { - homeAccountId: "test-home-accountId-1", - localAccountId: "test-local-accountId-1", - username: "user-1@example.com", - environment: "test-environment-1", - realm: "test-tenantId-1", - name: "name-1", - idTokenClaims: {}, - authorityType: "MSSTS", - }; - - const accountEntity2 = { - homeAccountId: "test-home-accountId-2", - localAccountId: "test-local-accountId-2", - username: "user-2@example.com", - environment: "test-environment-2", - realm: "test-tenantId-2", - name: "name-2", - idTokenClaims: {}, - authorityType: "MSSTS", - }; - - const account1: AccountInfo = { - authorityType: "MSSTS", - homeAccountId: accountEntity1.homeAccountId, - localAccountId: accountEntity1.localAccountId, - username: accountEntity1.username, - environment: accountEntity1.environment, - tenantId: accountEntity1.realm, - name: accountEntity1.name, - idTokenClaims: accountEntity1.idTokenClaims, - nativeAccountId: undefined, - }; - - const account2: AccountInfo = { - authorityType: "MSSTS", - homeAccountId: accountEntity2.homeAccountId, - localAccountId: accountEntity2.localAccountId, - username: accountEntity2.username, - environment: accountEntity2.environment, - tenantId: accountEntity2.realm, - name: accountEntity2.name, - idTokenClaims: accountEntity2.idTokenClaims, - nativeAccountId: undefined, - }; - const cacheKey1 = AccountEntity.generateAccountCacheKey(account1); - const cacheKey2 = AccountEntity.generateAccountCacheKey(account2); - - beforeEach(() => { - browserStorage.setItem( - cacheKey1, - JSON.stringify(accountEntity1) - ); - browserStorage.setItem( - cacheKey2, - JSON.stringify(accountEntity2) - ); - browserStorage.addAccountKeyToMap(cacheKey1); - browserStorage.addAccountKeyToMap(cacheKey2); - }); - - afterEach(() => { - browserStorage.clear(); - }); - - it("Matches accounts by username", () => { - expect(browserStorage.getAllAccounts()).toHaveLength(2); - const account1Filter = { username: account1.username }; - const account2Filter = { username: account2.username }; - expect( - browserStorage.getAccountInfoByFilter(account1Filter) - ).toHaveLength(1); - expect( - browserStorage.getAccountInfoByFilter(account1Filter) - ).toContainEqual(account1); - expect( - browserStorage.getAccountInfoByFilter(account2Filter) - ).toHaveLength(1); - expect( - browserStorage.getAccountInfoByFilter(account2Filter) - ).toContainEqual(account2); - }); - - it("Matches accounts by homeAccountId", () => { - expect(browserStorage.getAllAccounts()).toHaveLength(2); - const account1Filter = { - homeAccountId: account1.homeAccountId, - }; - const account2Filter = { - homeAccountId: account2.homeAccountId, - }; - expect( - browserStorage.getAccountInfoByFilter(account1Filter) - ).toHaveLength(1); - expect( - browserStorage.getAccountInfoByFilter(account1Filter) - ).toContainEqual(account1); - expect( - browserStorage.getAccountInfoByFilter(account2Filter) - ).toHaveLength(1); - expect( - browserStorage.getAccountInfoByFilter(account2Filter) - ).toContainEqual(account2); - }); - - it("Matches accounts by localAccountId", () => { - expect(browserStorage.getAllAccounts()).toHaveLength(2); - const account1Filter = { - localAccountId: account1.localAccountId, - }; - const account2Filter = { - localAccountId: account2.localAccountId, - }; - expect( - browserStorage.getAccountInfoByFilter(account1Filter) - ).toHaveLength(1); - expect( - browserStorage.getAccountInfoByFilter(account1Filter) - ).toContainEqual(account1); - expect( - browserStorage.getAccountInfoByFilter(account2Filter) - ).toHaveLength(1); - expect( - browserStorage.getAccountInfoByFilter(account2Filter) - ).toContainEqual(account2); - }); - - it("Matches accounts by tenantId", () => { - expect(browserStorage.getAllAccounts()).toHaveLength(2); - const account1Filter = { tenantId: account1.tenantId }; - const account2Filter = { tenantId: account2.tenantId }; - expect( - browserStorage.getAccountInfoByFilter(account1Filter) - ).toHaveLength(1); - expect( - browserStorage.getAccountInfoByFilter(account1Filter) - ).toContainEqual(account1); - expect( - browserStorage.getAccountInfoByFilter(account2Filter) - ).toHaveLength(1); - expect( - browserStorage.getAccountInfoByFilter(account2Filter) - ).toContainEqual(account2); - }); - - it("Matches accounts by environment", () => { - expect(browserStorage.getAllAccounts()).toHaveLength(2); - const account1Filter = { environment: account1.environment }; - const account2Filter = { environment: account2.environment }; - expect( - browserStorage.getAccountInfoByFilter(account1Filter) - ).toHaveLength(1); - expect( - browserStorage.getAccountInfoByFilter(account1Filter) - ).toContainEqual(account1); - expect( - browserStorage.getAccountInfoByFilter(account2Filter) - ).toHaveLength(1); - expect( - browserStorage.getAccountInfoByFilter(account2Filter) - ).toContainEqual(account2); - }); - - it("Matches accounts by all filters", () => { - expect(browserStorage.getAllAccounts()).toHaveLength(2); - const account1Filter = account1; - const account2Filter = account2; - expect( - browserStorage.getAccountInfoByFilter(account1Filter) - ).toHaveLength(1); - expect( - browserStorage.getAccountInfoByFilter(account1Filter) - ).toContainEqual(account1); - expect( - browserStorage.getAccountInfoByFilter(account2Filter) - ).toHaveLength(1); - expect( - browserStorage.getAccountInfoByFilter(account2Filter) - ).toContainEqual(account2); - }); - }); - - describe("getAccountInfoByHints", () => { - cacheConfig = { - temporaryCacheLocation: BrowserCacheLocation.SessionStorage, - cacheLocation: BrowserCacheLocation.SessionStorage, - storeAuthStateInCookie: false, - secureCookies: false, - cacheMigrationEnabled: false, - claimsBasedCachingEnabled: false, - }; - logger = new Logger({ - loggerCallback: ( - level: LogLevel, - message: string, - containsPii: boolean - ): void => {}, - piiLoggingEnabled: true, - }); - const browserStorage = new BrowserCacheManager( - TEST_CONFIG.MSAL_CLIENT_ID, - cacheConfig, - browserCrypto, - logger - ); - - const accountEntity1 = { - homeAccountId: "test-home-accountId-1", - localAccountId: "test-local-accountId-1", - username: "user-1@example.com", - environment: "test-environment-1", - realm: "test-tenantId-1", - name: "name-1", - idTokenClaims: { - sid: "session-1", - }, - authorityType: "MSSTS", - }; - - const accountEntity2 = { - homeAccountId: "test-home-accountId-2", - localAccountId: "test-local-accountId-2", - username: "user-2@example.com", - environment: "test-environment-2", - realm: "test-tenantId-2", - name: "name-2", - idTokenClaims: { - sid: "session-2", - }, - authorityType: "MSSTS", - }; - - const account1: AccountInfo = { - authorityType: "MSSTS", - homeAccountId: accountEntity1.homeAccountId, - localAccountId: accountEntity1.localAccountId, - username: accountEntity1.username, - environment: accountEntity1.environment, - tenantId: accountEntity1.realm, - name: accountEntity1.name, - idTokenClaims: accountEntity1.idTokenClaims, - nativeAccountId: undefined, - }; - - const account2: AccountInfo = { - authorityType: "MSSTS", - homeAccountId: accountEntity2.homeAccountId, - localAccountId: accountEntity2.localAccountId, - username: accountEntity2.username, - environment: accountEntity2.environment, - tenantId: accountEntity2.realm, - name: accountEntity2.name, - idTokenClaims: accountEntity2.idTokenClaims, - nativeAccountId: undefined, - }; - const cacheKey1 = AccountEntity.generateAccountCacheKey(account1); - const cacheKey2 = AccountEntity.generateAccountCacheKey(account2); - - beforeEach(() => { - browserStorage.setItem( - cacheKey1, - JSON.stringify(accountEntity1) - ); - browserStorage.setItem( - cacheKey2, - JSON.stringify(accountEntity2) - ); - browserStorage.addAccountKeyToMap(cacheKey1); - browserStorage.addAccountKeyToMap(cacheKey2); - }); - - afterEach(() => { - browserStorage.clear(); - }); - - it("Matches accounts by username", () => { - expect(browserStorage.getAllAccounts()).toHaveLength(2); - const account1Filter = { username: account1.username }; - const account2Filter = { username: account2.username }; - expect( - browserStorage.getAccountInfoByFilter(account1Filter) - ).toHaveLength(1); - expect( - browserStorage.getAccountInfoByFilter(account1Filter) - ).toContainEqual(account1); - expect( - browserStorage.getAccountInfoByFilter(account2Filter) - ).toHaveLength(1); - expect( - browserStorage.getAccountInfoByFilter(account2Filter) - ).toContainEqual(account2); - }); - - it("Matches accounts by homeAccountId", () => { - expect(browserStorage.getAllAccounts()).toHaveLength(2); - const account1Filter = { - homeAccountId: account1.homeAccountId, - }; - const account2Filter = { - homeAccountId: account2.homeAccountId, - }; - expect( - browserStorage.getAccountInfoByFilter(account1Filter) - ).toHaveLength(1); - expect( - browserStorage.getAccountInfoByFilter(account1Filter) - ).toContainEqual(account1); - expect( - browserStorage.getAccountInfoByFilter(account2Filter) - ).toHaveLength(1); - expect( - browserStorage.getAccountInfoByFilter(account2Filter) - ).toContainEqual(account2); - }); - - it("Matches accounts by localAccountId", () => { - expect(browserStorage.getAllAccounts()).toHaveLength(2); - const account1Filter = { - localAccountId: account1.localAccountId, - }; - const account2Filter = { - localAccountId: account2.localAccountId, - }; - expect( - browserStorage.getAccountInfoByFilter(account1Filter) - ).toHaveLength(1); - expect( - browserStorage.getAccountInfoByFilter(account1Filter) - ).toContainEqual(account1); - expect( - browserStorage.getAccountInfoByFilter(account2Filter) - ).toHaveLength(1); - expect( - browserStorage.getAccountInfoByFilter(account2Filter) - ).toContainEqual(account2); - }); - - it("Matches accounts by tenantId", () => { - expect(browserStorage.getAllAccounts()).toHaveLength(2); - const account1Filter = { tenantId: account1.tenantId }; - const account2Filter = { tenantId: account2.tenantId }; - expect( - browserStorage.getAccountInfoByFilter(account1Filter) - ).toHaveLength(1); - expect( - browserStorage.getAccountInfoByFilter(account1Filter) - ).toContainEqual(account1); - expect( - browserStorage.getAccountInfoByFilter(account2Filter) - ).toHaveLength(1); - expect( - browserStorage.getAccountInfoByFilter(account2Filter) - ).toContainEqual(account2); - }); - - it("Matches accounts by environment", () => { - expect(browserStorage.getAllAccounts()).toHaveLength(2); - const account1Filter = { environment: account1.environment }; - const account2Filter = { environment: account2.environment }; - expect( - browserStorage.getAccountInfoByFilter(account1Filter) - ).toHaveLength(1); - expect( - browserStorage.getAccountInfoByFilter(account1Filter) - ).toContainEqual(account1); - expect( - browserStorage.getAccountInfoByFilter(account2Filter) - ).toHaveLength(1); - expect( - browserStorage.getAccountInfoByFilter(account2Filter) - ).toContainEqual(account2); - }); - - it("Matches accounts by all filters", () => { - expect(browserStorage.getAllAccounts()).toHaveLength(2); - const account1Filter = account1; - const account2Filter = account2; - expect( - browserStorage.getAccountInfoByFilter(account1Filter) - ).toHaveLength(1); - expect( - browserStorage.getAccountInfoByFilter(account1Filter) - ).toContainEqual(account1); - expect( - browserStorage.getAccountInfoByFilter(account2Filter) - ).toHaveLength(1); - expect( - browserStorage.getAccountInfoByFilter(account2Filter) - ).toContainEqual(account2); - }); - }); - describe("getAccountInfoByHints", () => { cacheConfig = { cacheLocation: BrowserCacheLocation.SessionStorage, diff --git a/lib/msal-browser/test/interaction_client/BaseInteractionClient.spec.ts b/lib/msal-browser/test/interaction_client/BaseInteractionClient.spec.ts index dd7ae05aaa..7f00a9604d 100644 --- a/lib/msal-browser/test/interaction_client/BaseInteractionClient.spec.ts +++ b/lib/msal-browser/test/interaction_client/BaseInteractionClient.spec.ts @@ -20,6 +20,8 @@ import { TEST_TOKENS, DEFAULT_TENANT_DISCOVERY_RESPONSE, DEFAULT_OPENID_CONFIG_RESPONSE, + ID_TOKEN_CLAIMS, + ID_TOKEN_ALT_CLAIMS, } from "../utils/StringConstants"; import { BaseInteractionClient } from "../../src/interaction_client/BaseInteractionClient"; import { EndSessionRequest, PublicClientApplication } from "../../src"; @@ -73,20 +75,11 @@ describe("BaseInteractionClient", () => { let testAccountInfo2: AccountInfo; beforeEach(async () => { - const testIdTokenClaims: TokenClaims = { - ver: "2.0", - iss: "https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0", - sub: "AAAAAAAAAAAAAAAAAAAAAIkzqFVrSaSaFHy782bbtaQ", - name: "Abe Lincoln", - preferred_username: "AbeLi@microsoft.com", - oid: "00000000-0000-0000-66f3-3332eca7ea81", - tid: "3338040d-6c67-4c5b-b112-36a304b66dad", - nonce: "123523", - }; + const testIdTokenClaims: TokenClaims = ID_TOKEN_CLAIMS; testAccountInfo1 = { homeAccountId: TEST_DATA_CLIENT_INFO.TEST_HOME_ACCOUNT_ID, - localAccountId: TEST_DATA_CLIENT_INFO.TEST_UID, + localAccountId: testIdTokenClaims.oid || "", environment: "login.windows.net", tenantId: testIdTokenClaims.tid || "", username: testIdTokenClaims.preferred_username || "", @@ -117,19 +110,21 @@ describe("BaseInteractionClient", () => { testAccount1.clientInfo = TEST_DATA_CLIENT_INFO.TEST_CLIENT_INFO_B64ENCODED; + const testIdTokenClaims2: TokenClaims = ID_TOKEN_ALT_CLAIMS; + testAccountInfo2 = { homeAccountId: "different-home-account-id", - localAccountId: "different-local-account-id", + localAccountId: testIdTokenClaims2.oid || "", environment: "login.windows.net", - tenantId: testIdTokenClaims.tid || "", - username: testIdTokenClaims.preferred_username || "", + tenantId: testIdTokenClaims2.tid || "", + username: testIdTokenClaims2.preferred_username || "", }; const idTokenData2 = { realm: testAccountInfo2.tenantId, environment: testAccountInfo2.environment, credentialType: "IdToken", - secret: TEST_TOKENS.IDTOKEN_V2, + secret: TEST_TOKENS.IDTOKEN_V2_ALT, clientId: TEST_CONFIG.MSAL_CLIENT_ID, homeAccountId: testAccountInfo2.homeAccountId, }; diff --git a/lib/msal-browser/test/interaction_client/NativeInteractionClient.spec.ts b/lib/msal-browser/test/interaction_client/NativeInteractionClient.spec.ts index e236ff7558..838ebe89ad 100644 --- a/lib/msal-browser/test/interaction_client/NativeInteractionClient.spec.ts +++ b/lib/msal-browser/test/interaction_client/NativeInteractionClient.spec.ts @@ -179,7 +179,7 @@ describe("NativeInteractionClient Tests", () => { sinon .stub(CacheManager.prototype, "getAccountInfoFilteredBy") - .returns(testAccountInfo); + .returns({ ...testAccountInfo, idTokenClaims: ID_TOKEN_CLAIMS }); sinon .stub(CacheManager.prototype, "readCacheRecord") diff --git a/lib/msal-browser/test/interaction_client/RedirectClient.spec.ts b/lib/msal-browser/test/interaction_client/RedirectClient.spec.ts index ca71bf4d69..0eb545fdd6 100644 --- a/lib/msal-browser/test/interaction_client/RedirectClient.spec.ts +++ b/lib/msal-browser/test/interaction_client/RedirectClient.spec.ts @@ -3655,6 +3655,9 @@ describe("RedirectClient", () => { idTokenClaims: testIdTokenClaims, name: testIdTokenClaims.name || "", nativeAccountId: undefined, + tenants: testIdTokenClaims.tid + ? [testIdTokenClaims.tid] + : undefined, }; const testAccount: AccountEntity = new AccountEntity(); @@ -3668,6 +3671,7 @@ describe("RedirectClient", () => { testAccount.clientInfo = TEST_DATA_CLIENT_INFO.TEST_CLIENT_INFO_B64ENCODED; testAccount.idTokenClaims = testIdTokenClaims; + testAccount.tenants = testAccountInfo.tenants; const validatedLogoutRequest: CommonEndSessionRequest = { correlationId: RANDOM_TEST_GUID, diff --git a/lib/msal-browser/test/utils/StringConstants.ts b/lib/msal-browser/test/utils/StringConstants.ts index 3d9911a2b5..534ca7e4d7 100644 --- a/lib/msal-browser/test/utils/StringConstants.ts +++ b/lib/msal-browser/test/utils/StringConstants.ts @@ -78,6 +78,9 @@ export const TEST_TOKENS = { "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6IjdfWnVmMXR2a3dMeFlhSFMzcTZsVWpVWUlHdyIsImtpZCI6IjdfWnVmMXR2a3dMeFlhSFMzcTZsVWpVWUlHdyJ9.ewogICJhdWQiOiAiYjE0YTc1MDUtOTZlOS00OTI3LTkxZTgtMDYwMWQwZmM5Y2FhIiwKICAiaXNzIjogImh0dHBzOi8vc3RzLndpbmRvd3MubmV0L2ZhMTVkNjkyLWU5YzctNDQ2MC1hNzQzLTI5ZjI5NTZmZDQyOS8iLAogICJpYXQiOiAxNTM2Mjc1MTI0LAogICJuYmYiOiAxNTM2Mjc1MTI0LAogICJleHAiOiAxNTM2Mjc5MDI0LAogICJhaW8iOiAiQVhRQWkvOElBQUFBcXhzdUIrUjREMnJGUXFPRVRPNFlkWGJMRDlrWjh4ZlhhZGVBTTBRMk5rTlQ1aXpmZzN1d2JXU1hodVNTajZVVDVoeTJENldxQXBCNWpLQTZaZ1o5ay9TVTI3dVY5Y2V0WGZMT3RwTnR0Z2s1RGNCdGsrTExzdHovSmcrZ1lSbXY5YlVVNFhscGhUYzZDODZKbWoxRkN3PT0iLAogICJhbXIiOiBbCiAgICAicnNhIgogIF0sCiAgImVtYWlsIjogImFiZWxpQG1pY3Jvc29mdC5jb20iLAogICJmYW1pbHlfbmFtZSI6ICJMaW5jb2xuIiwKICAiZ2l2ZW5fbmFtZSI6ICJBYmUiLAogICJpZHAiOiAiaHR0cHM6Ly9zdHMud2luZG93cy5uZXQvNzJmOTg4YmYtODZmMS00MWFmLTkxYWItMmQ3Y2QwMTFkYjQ3LyIsCiAgImlwYWRkciI6ICIxMzEuMTA3LjIyMi4yMiIsCiAgIm5hbWUiOiAiYWJlbGkiLAogICJub25jZSI6ICIxMjM1MjMiLAogICJvaWQiOiAiMDU4MzNiNmItYWExZC00MmQ0LTllYzAtMWIyYmI5MTk0NDM4IiwKICAicmgiOiAiSSIsCiAgInN1YiI6ICI1X0o5clNzczgtanZ0X0ljdTZ1ZVJOTDh4WGI4TEY0RnNnX0tvb0MyUkpRIiwKICAidGlkIjogImZhMTVkNjkyLWU5YzctNDQ2MC1hNzQzLTI5ZjI5NTZmZDQyOSIsCiAgInVuaXF1ZV9uYW1lIjogIkFiZUxpQG1pY3Jvc29mdC5jb20iLAogICJ1dGkiOiAiTHhlXzQ2R3FUa09wR1NmVGxuNEVBQSIsCiAgInZlciI6ICIxLjAiLAogICJ1cG4iOiAiQWJlTGluY29sbkBjb250b3NvLmNvbSIKfQ==.UJQrCA6qn2bXq57qzGX_-D3HcPHqBMOKDPx4su1yKRLNErVD8xkxJLNLVRdASHqEcpyDctbdHccu6DPpkq5f0ibcaQFhejQNcABidJCTz0Bb2AbdUCTqAzdt9pdgQvMBnVH1xk3SCM6d4BbT4BkLLj10ZLasX7vRknaSjE_C5DI7Fg4WrZPwOhII1dB0HEZ_qpNaYXEiy-o94UJ94zCr07GgrqMsfYQqFR7kn-mn68AjvLcgwSfZvyR_yIK75S_K37vC3QryQ7cNoafDe9upql_6pB2ybMVlgWPs_DmbJ8g0om-sPlwyn74Cc1tW3ze-Xptw_2uVdPgWyqfuWAfq6Q", IDTOKEN_V2: "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IjFMVE16YWtpaGlSbGFfOHoyQkVKVlhlV01xbyJ9.eyJ2ZXIiOiIyLjAiLCJpc3MiOiJodHRwczovL2xvZ2luLm1pY3Jvc29mdG9ubGluZS5jb20vOTE4ODA0MGQtNmM2Ny00YzViLWIxMTItMzZhMzA0YjY2ZGFkL3YyLjAiLCJzdWIiOiJBQUFBQUFBQUFBQUFBQUFBQUFBQUFJa3pxRlZyU2FTYUZIeTc4MmJidGFRIiwiYXVkIjoiNmNiMDQwMTgtYTNmNS00NmE3LWI5OTUtOTQwYzc4ZjVhZWYzIiwiZXhwIjoxNTM2MzYxNDExLCJpYXQiOjE1MzYyNzQ3MTEsIm5iZiI6MTUzNjI3NDcxMSwibmFtZSI6IkFiZSBMaW5jb2xuIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiQWJlTGlAbWljcm9zb2Z0LmNvbSIsIm9pZCI6IjAwMDAwMDAwLTAwMDAtMDAwMC02NmYzLTMzMzJlY2E3ZWE4MSIsInRpZCI6IjMzMzgwNDBkLTZjNjctNGM1Yi1iMTEyLTM2YTMwNGI2NmRhZCIsIm5vbmNlIjoiMTIzNTIzIiwiYWlvIjoiRGYyVVZYTDFpeCFsTUNXTVNPSkJjRmF0emNHZnZGR2hqS3Y4cTVnMHg3MzJkUjVNQjVCaXN2R1FPN1lXQnlqZDhpUURMcSFlR2JJRGFreXA1bW5PcmNkcUhlWVNubHRlcFFtUnA2QUlaOGpZIn0=.1AFWW-Ck5nROwSlltm7GzZvDwUkqvhSQpm55TQsmVo9Y59cLhRXpvB8n-55HCr9Z6G_31_UbeUkoz612I2j_Sm9FFShSDDjoaLQr54CreGIJvjtmS3EkK9a7SJBbcpL1MpUtlfygow39tFjY7EVNW9plWUvRrTgVk7lYLprvfzw-CIqw3gHC-T7IK_m_xkr08INERBtaecwhTeN4chPC4W3jdmw_lIxzC48YoQ0dB1L9-ImX98Egypfrlbm0IBL5spFzL6JDZIRRJOu8vecJvj1mq-IUhGt0MacxX8jdxYLP-KUu2d9MbNKpCKJuZ7p8gwTL5B7NlUdh_dmSviPWrw", + // [SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Fake credential used for testing purposes")] + IDTOKEN_V2_ALT: + "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IjJjNGE2MmYzMGRkNWMxNGE4MGFmMjMxZjY2M2ZmODliIn0.eyJ2ZXIiOiIyLjAiLCJpc3MiOiJodHRwczovL2xvZ2luLm1pY3Jvc29mdG9ubGluZS5jb20vOTE4ODA0MGQtNmM2Ny00YzViLWIxMTItMzZhMzA0YjY2ZGFkL3YyLjAiLCJzdWIiOiJBQUFBQUFBQUFBQUFBQUFBQUFBQUFJa3pxRlZyU2FTYUZIeTc4MmJidGFRIiwiYXVkIjoiNmNiMDQwMTgtYTNmNS00NmE3LWI5OTUtOTQwYzc4ZjVhZWYzIiwiZXhwIjoxNTM2MzYxNDExLCJpYXQiOjE1MzYyNzQ3MTEsIm5iZiI6MTUzNjI3NDcxMSwibmFtZSI6IkFiZSBMaW5jb2xuIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiQWJlTGlUb29AbWljcm9zb2Z0LmNvbSIsIm9pZCI6IjAwMDAwMDAwLTAwMDAtMDAwMC0wMDAwLTAwMDAwMDAwMDAwMCIsInRpZCI6IjcyZjk4OGJmLTg2ZjEtNDFhZi05MWFiLTJkN2NkMDExZGI0NyIsIm5vbmNlIjoiMTIzNTIzIiwiYWlvIjoiRGYyVVZYTDFpeCFsTUNXTVNPSkJjRmF0emNHZnZGR2hqS3Y4cTVnMHg3MzJkUjVNQjVCaXN2R1FPN1lXQnlqZDhpUURMcSFlR2JJRGFreXA1bW5PcmNkcUhlWVNubHRlcFFtUnA2QUlaOGpZIn0.VIsqgZiJfiOx-VkvYlq9X3EKmoRmfmHyyrPtSGww20Ng-eJCyQZqYlXEnsc44tuA9czlFpba9WwB2bCiS5jq4kjcTaQCg7ocxD1Db6PwY2mrAQdKUvDaZ7wSVXgOmR8SkyzqN55EGcMopXDOY12BLx_859lJYBJM-2ICmnscMNEjg2DZ1QvPRz7NX4MoOXWlBwhibxgQ4YEfq5LPOCit3LnSsT2Io1ORQouDPCfe55jRUoQoHyWOuXHA5w8Zr7Su6eHCnXVHv9k8mbAii_qOeLIaDg6Gh8B9Ht-BmBVExzzveUSe2ZLYxMDLj8BOhxlHn156l4bSqeEP40uzFae9uO4uVX4oIrd9p2MdbKm0PUahILtt5HJg4rs2f4xiyPC5Yt22QWPCqE8HFf9p3ZliAWmnAw6LyYIRl6rw3vnZFm031NWcRfSme9CfDbnqdks7qnLfVVgrh-1KoKgi6sTUxpJfXN5-VHw47k0tU7UZyIUDekt-mbXnxoAWy7LhnNQ3", IDTOKEN_V2_NEWCLAIM: "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IjFMVE16YWtpaGlSbGFfOHoyQkVKVlhlV01xbyJ9.ewogICJ2ZXIiOiAiMi4wIiwKICAiaXNzIjogImh0dHBzOi8vbG9naW4ubWljcm9zb2Z0b25saW5lLmNvbS85MTg4MDQwZC02YzY3LTRjNWItYjExMi0zNmEzMDRiNjZkYWQvdjIuMCIsCiAgInN1YiI6ICJBQUFBQUFBQUFBQUFBQUFBQUFBQUFJa3pxRlZyU2FTYUZIeTc4MmJidGFRIiwKICAiYXVkIjogIjZjYjA0MDE4LWEzZjUtNDZhNy1iOTk1LTk0MGM3OGY1YWVmMyIsCiAgImV4cCI6IDE1MzYzNjE0MTEsCiAgImlhdCI6IDE1MzYyNzQ3MTEsCiAgIm5iZiI6IDE1MzYyNzQ3MTEsCiAgIm5hbWUiOiAiQWJlIExpbmNvbG4iLAogICJwcmVmZXJyZWRfdXNlcm5hbWUiOiAiQWJlTGlAbWljcm9zb2Z0LmNvbSIsCiAgIm9pZCI6ICIwMDAwMDAwMC0wMDAwLTAwMDAtNjZmMy0zMzMyZWNhN2VhODEiLAogICJlbWFpbCI6ICJBYmVMaUBtaWNyb3NvZnQuY29tIiwKICAidGlkIjogIjMzMzgwNDBkLTZjNjctNGM1Yi1iMTEyLTM2YTMwNGI2NmRhZCIsCiAgIm5vbmNlIjogIjEyMzUyMyIsCiAgImFpbyI6ICJEZjJVVlhMMWl4IWxNQ1dNU09KQmNGYXR6Y0dmdkZHaGpLdjhxNWcweDczMmRSNU1CNUJpc3ZHUU83WVdCeWpkOGlRRExxIWVHYklEYWt5cDVtbk9yY2RxSGVZU25sdGVwUW1ScDZBSVo4alkiCn0=.1AFWW-Ck5nROwSlltm7GzZvDwUkqvhSQpm55TQsmVo9Y59cLhRXpvB8n-55HCr9Z6G_31_UbeUkoz612I2j_Sm9FFShSDDjoaLQr54CreGIJvjtmS3EkK9a7SJBbcpL1MpUtlfygow39tFjY7EVNW9plWUvRrTgVk7lYLprvfzw-CIqw3gHC-T7IK_m_xkr08INERBtaecwhTeN4chPC4W3jdmw_lIxzC48YoQ0dB1L9-ImX98Egypfrlbm0IBL5spFzL6JDZIRRJOu8vecJvj1mq-IUhGt0MacxX8jdxYLP-KUu2d9MbNKpCKJuZ7p8gwTL5B7NlUdh_dmSviPWrw", LOGIN_AT_STRING: @@ -110,6 +113,13 @@ export const ID_TOKEN_CLAIMS = { aio: "Df2UVXL1ix!lMCWMSOJBcFatzcGfvFGhjKv8q5g0x732dR5MB5BisvGQO7YWByjd8iQDLq!eGbIDakyp5mnOrcdqHeYSnltepQmRp6AIZ8jY", }; +export const ID_TOKEN_ALT_CLAIMS = { + ...ID_TOKEN_CLAIMS, + preferred_username: "AbeLiToo@microsoft.com", + oid: "00000000-0000-0000-0000-000000000000", + tid: "72f988bf-86f1-41af-91ab-2d7cd011db47", +}; + // Test Expiration Vals export const TEST_TOKEN_LIFETIMES = { DEFAULT_EXPIRES_IN: 3599, diff --git a/lib/msal-common/test/cache/CacheManager.spec.ts b/lib/msal-common/test/cache/CacheManager.spec.ts index 397a22bdb5..5d2bd0e67b 100644 --- a/lib/msal-common/test/cache/CacheManager.spec.ts +++ b/lib/msal-common/test/cache/CacheManager.spec.ts @@ -22,6 +22,7 @@ import { TEST_CRYPTO_VALUES, TEST_ACCOUNT_INFO, TEST_TOKEN_LIFETIMES, + ID_TOKEN_ALT_CLAIMS, } from "../test_kit/StringConstants"; import { ClientAuthErrorCodes, @@ -261,65 +262,217 @@ describe("CacheManager.ts test cases", () => { }); }); - it("getAllAccounts (gets all AccountInfo objects)", async () => { - const accounts = mockCache.cacheManager.getAllAccounts(); + describe("getAllAccounts", () => { + const account1 = CACHE_MOCKS.MOCK_ACCOUNT_INFO; + const account2 = CACHE_MOCKS.MOCK_ACCOUNT_INFO_WITH_NATIVE_ACCOUNT_ID; + it("getAllAccounts (gets all AccountInfo objects)", async () => { + const accounts = mockCache.cacheManager.getAllAccounts(); - expect(accounts).not.toBeNull(); - expect(accounts[0].idToken).toEqual(TEST_TOKENS.IDTOKEN_V2); - expect(accounts[0].idTokenClaims).toEqual(ID_TOKEN_CLAIMS); - }); + expect(accounts).not.toBeNull(); + expect(accounts[0].idToken).toEqual(TEST_TOKENS.IDTOKEN_V2); + expect(accounts[0].idTokenClaims).toEqual(ID_TOKEN_CLAIMS); + }); - describe("getAllAccounts with loginHint filter", () => { - it("loginHint filter matching login_hint ID token claim", () => { - // filter by loginHint = login_hint - const loginHint = "testLoginHint"; - const successFilter: AccountFilter = { - loginHint: loginHint, - }; - jest.spyOn(mockCrypto, "base64Decode").mockReturnValueOnce( - JSON.stringify({ login_hint: loginHint }) - ); - let accounts = mockCache.cacheManager.getAllAccounts(successFilter); - expect(accounts.length).toEqual(1); + describe("getAllAccounts with loginHint filter", () => { + it("loginHint filter matching login_hint ID token claim", () => { + // filter by loginHint = login_hint + const loginHint = "testLoginHint"; + const successFilter: AccountFilter = { + loginHint: loginHint, + }; + jest.spyOn(mockCrypto, "base64Decode").mockReturnValueOnce( + JSON.stringify({ login_hint: loginHint }) + ); + let accounts = + mockCache.cacheManager.getAllAccounts(successFilter); + expect(accounts.length).toEqual(1); + + const wrongFilter: AccountFilter = { + loginHint: "WrongHint", + }; + accounts = mockCache.cacheManager.getAllAccounts(wrongFilter); + expect(accounts.length).toBe(0); + }); + + it("loginHint filter matching username", () => { + // filter by loginHint = preferred_username + const username = "janedoe@microsoft.com"; + const successFilter: AccountFilter = { + loginHint: username, + }; + jest.spyOn(mockCrypto, "base64Decode").mockReturnValueOnce( + JSON.stringify({ preferred_username: username }) + ); + let accounts = + mockCache.cacheManager.getAllAccounts(successFilter); + expect(accounts.length).toEqual(1); + + const wrongFilter: AccountFilter = { + loginHint: "WrongHint", + }; + accounts = mockCache.cacheManager.getAllAccounts(wrongFilter); + expect(accounts.length).toBe(0); + }); + + it("loginHint filter matching upn ID token claim", () => { + // filter by loginHint = upn + const upn = "testUpn"; + const successFilter: AccountFilter = { + loginHint: upn, + }; + + jest.spyOn(mockCrypto, "base64Decode").mockReturnValueOnce( + JSON.stringify({ upn: upn }) + ); + let accounts = + mockCache.cacheManager.getAllAccounts(successFilter); + expect(accounts.length).toEqual(1); + + const wrongFilter: AccountFilter = { + loginHint: "WrongHint", + }; + accounts = mockCache.cacheManager.getAllAccounts(wrongFilter); + expect(accounts.length).toBe(0); + }); + }); - const wrongFilter: AccountFilter = { loginHint: "WrongHint" }; - accounts = mockCache.cacheManager.getAllAccounts(wrongFilter); - expect(accounts.length).toBe(0); + it("Matches accounts by username", () => { + expect(mockCache.cacheManager.getAllAccounts()).toHaveLength(2); + const account1Filter = { username: account1.username }; + const account2Filter = { username: account2.username }; + expect( + mockCache.cacheManager.getAllAccounts(account1Filter) + ).toHaveLength(1); + expect( + mockCache.cacheManager.getAllAccounts(account1Filter)[0] + .username + ).toBe(account1.username); + expect( + mockCache.cacheManager.getAllAccounts(account2Filter) + ).toHaveLength(1); + expect( + mockCache.cacheManager.getAllAccounts(account2Filter)[0] + .username + ).toBe(account2.username); }); - it("loginHint filter matching username", () => { - // filter by loginHint = preferred_username - const username = "janedoe@microsoft.com"; - const successFilter: AccountFilter = { - loginHint: username, + it("Matches accounts by homeAccountId", () => { + expect(mockCache.cacheManager.getAllAccounts()).toHaveLength(2); + const account1Filter = { + homeAccountId: account1.homeAccountId, }; - jest.spyOn(mockCrypto, "base64Decode").mockReturnValueOnce( - JSON.stringify({ preferred_username: username }) - ); - let accounts = mockCache.cacheManager.getAllAccounts(successFilter); - expect(accounts.length).toEqual(1); + const account2Filter = { + homeAccountId: account2.homeAccountId, + }; + expect( + mockCache.cacheManager.getAllAccounts(account1Filter) + ).toHaveLength(1); + expect( + mockCache.cacheManager.getAllAccounts(account1Filter)[0] + .homeAccountId + ).toBe(account1.homeAccountId); + expect( + mockCache.cacheManager.getAllAccounts(account2Filter) + ).toHaveLength(1); + expect( + mockCache.cacheManager.getAllAccounts(account2Filter)[0] + .homeAccountId + ).toBe(account2.homeAccountId); + }); - const wrongFilter: AccountFilter = { loginHint: "WrongHint" }; - accounts = mockCache.cacheManager.getAllAccounts(wrongFilter); - expect(accounts.length).toBe(0); + it("Matches accounts by localAccountId", () => { + expect(mockCache.cacheManager.getAllAccounts()).toHaveLength(2); + // Local account ID is sourced from ID token claims so for this test we compare against the decoded ID token claims instead of mock account object + const account1Filter = { + localAccountId: ID_TOKEN_CLAIMS.oid, + }; + const account2Filter = { + localAccountId: ID_TOKEN_ALT_CLAIMS.oid, + }; + expect( + mockCache.cacheManager.getAllAccounts(account1Filter) + ).toHaveLength(1); + expect( + mockCache.cacheManager.getAllAccounts(account1Filter)[0] + .localAccountId + ).toBe(account1Filter.localAccountId); + expect( + mockCache.cacheManager.getAllAccounts(account2Filter) + ).toHaveLength(1); + expect( + mockCache.cacheManager.getAllAccounts(account2Filter)[0] + .localAccountId + ).toBe(account2Filter.localAccountId); }); - it("loginHint filter matching upn ID token claim", () => { - // filter by loginHint = upn - const upn = "testUpn"; - const successFilter: AccountFilter = { - loginHint: upn, + it("Matches accounts by tenantId", () => { + expect(mockCache.cacheManager.getAllAccounts()).toHaveLength(2); + const account1Filter = { + tenantId: account1.tenantId, + }; + const account2Filter = { + tenantId: account2.tenantId, }; + expect( + mockCache.cacheManager.getAllAccounts(account1Filter) + ).toHaveLength(1); + expect( + mockCache.cacheManager.getAllAccounts(account1Filter)[0] + .tenantId + ).toBe(account1Filter.tenantId); + expect( + mockCache.cacheManager.getAllAccounts(account2Filter) + ).toHaveLength(1); + expect( + mockCache.cacheManager.getAllAccounts(account2Filter)[0] + .tenantId + ).toBe(account2Filter.tenantId); + }); - jest.spyOn(mockCrypto, "base64Decode").mockReturnValueOnce( - JSON.stringify({ upn: upn }) - ); - let accounts = mockCache.cacheManager.getAllAccounts(successFilter); - expect(accounts.length).toEqual(1); + it("Matches accounts by environment", () => { + expect(mockCache.cacheManager.getAllAccounts()).toHaveLength(2); + const account1Filter = { environment: account1.environment }; + const account2Filter = { environment: account2.environment }; + expect( + mockCache.cacheManager.getAllAccounts(account1Filter) + ).toHaveLength(1); + expect( + mockCache.cacheManager.getAllAccounts(account1Filter)[0] + .environment + ).toBe(account1.environment); + expect( + mockCache.cacheManager.getAllAccounts(account2Filter) + ).toHaveLength(1); + expect( + mockCache.cacheManager.getAllAccounts(account2Filter)[0] + .environment + ).toBe(account2.environment); + }); - const wrongFilter: AccountFilter = { loginHint: "WrongHint" }; - accounts = mockCache.cacheManager.getAllAccounts(wrongFilter); - expect(accounts.length).toBe(0); + it("Matches accounts by all filters", () => { + expect(mockCache.cacheManager.getAllAccounts()).toHaveLength(2); + const account1Filter = { + ...account1, + localAccountId: ID_TOKEN_CLAIMS.oid, + }; + const account2Filter = { + ...account2, + localAccountId: ID_TOKEN_ALT_CLAIMS.oid, + }; + expect( + mockCache.cacheManager.getAllAccounts(account1Filter) + ).toHaveLength(1); + expect( + mockCache.cacheManager.getAllAccounts(account1Filter)[0] + .localAccountId + ).toBe(account1Filter.localAccountId); + expect( + mockCache.cacheManager.getAllAccounts(account2Filter) + ).toHaveLength(1); + expect( + mockCache.cacheManager.getAllAccounts(account2Filter)[0] + .localAccountId + ).toBe(account2Filter.localAccountId); }); }); @@ -425,7 +578,7 @@ describe("CacheManager.ts test cases", () => { it("realm filter", () => { // filter by realm - const successFilter: AccountFilter = { realm: "microsoft" }; + const successFilter: AccountFilter = { realm: "utid" }; let accounts = mockCache.cacheManager.getAccountsFilteredBy(successFilter); expect(Object.keys(accounts).length).toEqual(1); diff --git a/lib/msal-common/test/cache/MockCache.ts b/lib/msal-common/test/cache/MockCache.ts index 6d04576c23..a35965d513 100644 --- a/lib/msal-common/test/cache/MockCache.ts +++ b/lib/msal-common/test/cache/MockCache.ts @@ -48,7 +48,7 @@ export class MockCache { const accountData = { username: "John Doe", localAccountId: "object1234", - realm: "microsoft", + realm: "utid", environment: "login.microsoftonline.com", homeAccountId: "uid.utid", authorityType: "MSSTS", @@ -58,11 +58,11 @@ export class MockCache { this.cacheManager.setAccount(account); const accountDataWithNativeAccountId = { - username: "John Doe", + username: "Jane Doe", localAccountId: "object1234", - realm: "microsoft", - environment: "login.microsoftonline.com", - homeAccountId: "uid.utid", + realm: "utid2", + environment: "login.windows.net", + homeAccountId: "uid.utid2", authorityType: "MSSTS", clientInfo: "eyJ1aWQiOiJ1aWQiLCAidXRpZCI6InV0aWQifQ==", nativeAccountId: "mocked_native_account_id", @@ -77,7 +77,7 @@ export class MockCache { // create id token entries in the cache createIdTokenEntries(): void { const idTokenData = { - realm: "microsoft", + realm: "utid", environment: "login.microsoftonline.com", credentialType: "IdToken", secret: TEST_TOKENS.IDTOKEN_V2, @@ -86,6 +86,20 @@ export class MockCache { }; const idToken = CacheManager.toObject(new IdTokenEntity(), idTokenData); this.cacheManager.setIdTokenCredential(idToken); + + const idTokenData2 = { + realm: "utid2", + environment: "login.windows.net", + credentialType: "IdToken", + secret: TEST_TOKENS.IDTOKEN_V2_ALT, + clientId: "mock_client_id", + homeAccountId: "uid.utid2", + }; + const idToken2 = CacheManager.toObject( + new IdTokenEntity(), + idTokenData2 + ); + this.cacheManager.setIdTokenCredential(idToken2); } // create access token entries in the cache diff --git a/lib/msal-common/test/cache/entities/AccountEntity.spec.ts b/lib/msal-common/test/cache/entities/AccountEntity.spec.ts index 63cc2bf229..82c49be675 100644 --- a/lib/msal-common/test/cache/entities/AccountEntity.spec.ts +++ b/lib/msal-common/test/cache/entities/AccountEntity.spec.ts @@ -128,7 +128,7 @@ describe("AccountEntity.ts Unit Tests", () => { const ac = new AccountEntity(); Object.assign(ac, mockAccountEntity); expect(ac.generateAccountKey()).toEqual( - "uid.utid-login.microsoftonline.com-microsoft" + "uid.utid-login.microsoftonline.com-utid" ); }); diff --git a/lib/msal-common/test/client/RefreshTokenClient.spec.ts b/lib/msal-common/test/client/RefreshTokenClient.spec.ts index d16154a2ba..c2d354fdf4 100644 --- a/lib/msal-common/test/client/RefreshTokenClient.spec.ts +++ b/lib/msal-common/test/client/RefreshTokenClient.spec.ts @@ -300,7 +300,7 @@ describe("RefreshTokenClient unit tests", () => { const testAccount: AccountInfo = { authorityType: "MSSTS", - homeAccountId: ID_TOKEN_CLAIMS.sub, + homeAccountId: TEST_DATA_CLIENT_INFO.TEST_DECODED_HOME_ACCOUNT_ID, tenantId: ID_TOKEN_CLAIMS.tid, environment: "login.windows.net", username: ID_TOKEN_CLAIMS.preferred_username, @@ -308,6 +308,7 @@ describe("RefreshTokenClient unit tests", () => { localAccountId: ID_TOKEN_CLAIMS.oid, idTokenClaims: ID_TOKEN_CLAIMS, nativeAccountId: undefined, + tenants: [ID_TOKEN_CLAIMS.tid], }; beforeEach(async () => { @@ -321,7 +322,7 @@ describe("RefreshTokenClient unit tests", () => { .stub(Authority.prototype, "getPreferredCache") .returns("login.windows.net"); AUTHENTICATION_RESULT.body.client_info = - TEST_DATA_CLIENT_INFO.TEST_DECODED_CLIENT_INFO; + TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO; sinon .stub(CacheManager.prototype, "getRefreshToken") .returns(testRefreshTokenEntity); @@ -1056,7 +1057,7 @@ describe("RefreshTokenClient unit tests", () => { const testAccount: AccountInfo = { authorityType: "MSSTS", - homeAccountId: ID_TOKEN_CLAIMS.sub, + homeAccountId: TEST_DATA_CLIENT_INFO.TEST_DECODED_HOME_ACCOUNT_ID, tenantId: ID_TOKEN_CLAIMS.tid, environment: "login.windows.net", username: ID_TOKEN_CLAIMS.preferred_username, @@ -1064,6 +1065,7 @@ describe("RefreshTokenClient unit tests", () => { localAccountId: ID_TOKEN_CLAIMS.oid, idTokenClaims: ID_TOKEN_CLAIMS, nativeAccountId: undefined, + tenants: [ID_TOKEN_CLAIMS.tid], }; beforeEach(async () => { @@ -1077,7 +1079,7 @@ describe("RefreshTokenClient unit tests", () => { .stub(Authority.prototype, "getPreferredCache") .returns("login.windows.net"); AUTHENTICATION_RESULT_WITH_FOCI.body.client_info = - TEST_DATA_CLIENT_INFO.TEST_DECODED_CLIENT_INFO; + TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO; sinon .stub( RefreshTokenClient.prototype, diff --git a/lib/msal-common/test/client/SilentFlowClient.spec.ts b/lib/msal-common/test/client/SilentFlowClient.spec.ts index f7f5865518..0c94280d03 100644 --- a/lib/msal-common/test/client/SilentFlowClient.spec.ts +++ b/lib/msal-common/test/client/SilentFlowClient.spec.ts @@ -795,7 +795,7 @@ describe("SilentFlowClient unit tests", () => { ) .resolves(DEFAULT_OPENID_CONFIG_RESPONSE.body); AUTHENTICATION_RESULT.body.client_info = - TEST_DATA_CLIENT_INFO.TEST_DECODED_CLIENT_INFO; + TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO; sinon .stub( RefreshTokenClient.prototype, @@ -1013,7 +1013,7 @@ describe("SilentFlowClient unit tests", () => { ) .resolves(DEFAULT_OPENID_CONFIG_RESPONSE.body); AUTHENTICATION_RESULT.body.client_info = - TEST_DATA_CLIENT_INFO.TEST_DECODED_CLIENT_INFO; + TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO; sinon .stub( RefreshTokenClient.prototype, diff --git a/lib/msal-common/test/test_kit/StringConstants.ts b/lib/msal-common/test/test_kit/StringConstants.ts index e7cb02377a..ff9dbdbfc1 100644 --- a/lib/msal-common/test/test_kit/StringConstants.ts +++ b/lib/msal-common/test/test_kit/StringConstants.ts @@ -19,6 +19,8 @@ export const TEST_TOKENS = { "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6IjdfWnVmMXR2a3dMeFlhSFMzcTZsVWpVWUlHdyIsImtpZCI6IjdfWnVmMXR2a3dMeFlhSFMzcTZsVWpVWUlHdyJ9.eyJhdWQiOiJiMTRhNzUwNS05NmU5LTQ5MjctOTFlOC0wNjAxZDBmYzljYWEiLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC9mYTE1ZDY5Mi1lOWM3LTQ0NjAtYTc0My0yOWYyOTU2ZmQ0MjkvIiwiaWF0IjoxNTM2Mjc1MTI0LCJuYmYiOjE1MzYyNzUxMjQsImV4cCI6MTUzNjI3OTAyNCwiYWlvIjoiQVhRQWkvOElBQUFBcXhzdUIrUjREMnJGUXFPRVRPNFlkWGJMRDlrWjh4ZlhhZGVBTTBRMk5rTlQ1aXpmZzN1d2JXU1hodVNTajZVVDVoeTJENldxQXBCNWpLQTZaZ1o5ay9TVTI3dVY5Y2V0WGZMT3RwTnR0Z2s1RGNCdGsrTExzdHovSmcrZ1lSbXY5YlVVNFhscGhUYzZDODZKbWoxRkN3PT0iLCJhbXIiOlsicnNhIl0sImVtYWlsIjoiYWJlbGlAbWljcm9zb2Z0LmNvbSIsImZhbWlseV9uYW1lIjoiTGluY29sbiIsImdpdmVuX25hbWUiOiJBYmUiLCJpZHAiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC83MmY5ODhiZi04NmYxLTQxYWYtOTFhYi0yZDdjZDAxMWRiNDcvIiwiaXBhZGRyIjoiMTMxLjEwNy4yMjIuMjIiLCJuYW1lIjoiYWJlbGkiLCJub25jZSI6IjEyMzUyMyIsIm9pZCI6IjA1ODMzYjZiLWFhMWQtNDJkNC05ZWMwLTFiMmJiOTE5NDQzOCIsInJoIjoiSSIsInN1YiI6IjVfSjlyU3NzOC1qdnRfSWN1NnVlUk5MOHhYYjhMRjRGc2dfS29vQzJSSlEiLCJ0aWQiOiJmYTE1ZDY5Mi1lOWM3LTQ0NjAtYTc0My0yOWYyOTU2ZmQ0MjkiLCJ1bmlxdWVfbmFtZSI6IkFiZUxpQG1pY3Jvc29mdC5jb20iLCJ1dGkiOiJMeGVfNDZHcVRrT3BHU2ZUbG40RUFBIiwidmVyIjoiMS4wIn0=.UJQrCA6qn2bXq57qzGX_-D3HcPHqBMOKDPx4su1yKRLNErVD8xkxJLNLVRdASHqEcpyDctbdHccu6DPpkq5f0ibcaQFhejQNcABidJCTz0Bb2AbdUCTqAzdt9pdgQvMBnVH1xk3SCM6d4BbT4BkLLj10ZLasX7vRknaSjE_C5DI7Fg4WrZPwOhII1dB0HEZ_qpNaYXEiy-o94UJ94zCr07GgrqMsfYQqFR7kn-mn68AjvLcgwSfZvyR_yIK75S_K37vC3QryQ7cNoafDe9upql_6pB2ybMVlgWPs_DmbJ8g0om-sPlwyn74Cc1tW3ze-Xptw_2uVdPgWyqfuWAfq6Q", IDTOKEN_V2: "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IjFMVE16YWtpaGlSbGFfOHoyQkVKVlhlV01xbyJ9.eyJ2ZXIiOiIyLjAiLCJpc3MiOiJodHRwczovL2xvZ2luLm1pY3Jvc29mdG9ubGluZS5jb20vOTE4ODA0MGQtNmM2Ny00YzViLWIxMTItMzZhMzA0YjY2ZGFkL3YyLjAiLCJzdWIiOiJBQUFBQUFBQUFBQUFBQUFBQUFBQUFJa3pxRlZyU2FTYUZIeTc4MmJidGFRIiwiYXVkIjoiNmNiMDQwMTgtYTNmNS00NmE3LWI5OTUtOTQwYzc4ZjVhZWYzIiwiZXhwIjoxNTM2MzYxNDExLCJpYXQiOjE1MzYyNzQ3MTEsIm5iZiI6MTUzNjI3NDcxMSwibmFtZSI6IkFiZSBMaW5jb2xuIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiQWJlTGlAbWljcm9zb2Z0LmNvbSIsIm9pZCI6IjAwMDAwMDAwLTAwMDAtMDAwMC02NmYzLTMzMzJlY2E3ZWE4MSIsInRpZCI6IjMzMzgwNDBkLTZjNjctNGM1Yi1iMTEyLTM2YTMwNGI2NmRhZCIsIm5vbmNlIjoiMTIzNTIzIiwiYWlvIjoiRGYyVVZYTDFpeCFsTUNXTVNPSkJjRmF0emNHZnZGR2hqS3Y4cTVnMHg3MzJkUjVNQjVCaXN2R1FPN1lXQnlqZDhpUURMcSFlR2JJRGFreXA1bW5PcmNkcUhlWVNubHRlcFFtUnA2QUlaOGpZIn0=.1AFWW-Ck5nROwSlltm7GzZvDwUkqvhSQpm55TQsmVo9Y59cLhRXpvB8n-55HCr9Z6G_31_UbeUkoz612I2j_Sm9FFShSDDjoaLQr54CreGIJvjtmS3EkK9a7SJBbcpL1MpUtlfygow39tFjY7EVNW9plWUvRrTgVk7lYLprvfzw-CIqw3gHC-T7IK_m_xkr08INERBtaecwhTeN4chPC4W3jdmw_lIxzC48YoQ0dB1L9-ImX98Egypfrlbm0IBL5spFzL6JDZIRRJOu8vecJvj1mq-IUhGt0MacxX8jdxYLP-KUu2d9MbNKpCKJuZ7p8gwTL5B7NlUdh_dmSviPWrw", + IDTOKEN_V2_ALT: + "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IjJjNGE2MmYzMGRkNWMxNGE4MGFmMjMxZjY2M2ZmODliIn0.eyJ2ZXIiOiIyLjAiLCJpc3MiOiJodHRwczovL2xvZ2luLm1pY3Jvc29mdG9ubGluZS5jb20vOTE4ODA0MGQtNmM2Ny00YzViLWIxMTItMzZhMzA0YjY2ZGFkL3YyLjAiLCJzdWIiOiJBQUFBQUFBQUFBQUFBQUFBQUFBQUFJa3pxRlZyU2FTYUZIeTc4MmJidGFRIiwiYXVkIjoiNmNiMDQwMTgtYTNmNS00NmE3LWI5OTUtOTQwYzc4ZjVhZWYzIiwiZXhwIjoxNTM2MzYxNDExLCJpYXQiOjE1MzYyNzQ3MTEsIm5iZiI6MTUzNjI3NDcxMSwibmFtZSI6IkFiZSBMaW5jb2xuIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiQWJlTGlUb29AbWljcm9zb2Z0LmNvbSIsIm9pZCI6IjAwMDAwMDAwLTAwMDAtMDAwMC0wMDAwLTAwMDAwMDAwMDAwMCIsInRpZCI6IjcyZjk4OGJmLTg2ZjEtNDFhZi05MWFiLTJkN2NkMDExZGI0NyIsIm5vbmNlIjoiMTIzNTIzIiwiYWlvIjoiRGYyVVZYTDFpeCFsTUNXTVNPSkJjRmF0emNHZnZGR2hqS3Y4cTVnMHg3MzJkUjVNQjVCaXN2R1FPN1lXQnlqZDhpUURMcSFlR2JJRGFreXA1bW5PcmNkcUhlWVNubHRlcFFtUnA2QUlaOGpZIn0.VIsqgZiJfiOx-VkvYlq9X3EKmoRmfmHyyrPtSGww20Ng-eJCyQZqYlXEnsc44tuA9czlFpba9WwB2bCiS5jq4kjcTaQCg7ocxD1Db6PwY2mrAQdKUvDaZ7wSVXgOmR8SkyzqN55EGcMopXDOY12BLx_859lJYBJM-2ICmnscMNEjg2DZ1QvPRz7NX4MoOXWlBwhibxgQ4YEfq5LPOCit3LnSsT2Io1ORQouDPCfe55jRUoQoHyWOuXHA5w8Zr7Su6eHCnXVHv9k8mbAii_qOeLIaDg6Gh8B9Ht-BmBVExzzveUSe2ZLYxMDLj8BOhxlHn156l4bSqeEP40uzFae9uO4uVX4oIrd9p2MdbKm0PUahILtt5HJg4rs2f4xiyPC5Yt22QWPCqE8HFf9p3ZliAWmnAw6LyYIRl6rw3vnZFm031NWcRfSme9CfDbnqdks7qnLfVVgrh-1KoKgi6sTUxpJfXN5-VHw47k0tU7UZyIUDekt-mbXnxoAWy7LhnNQ3", IDTOKEN_V2_NEWCLAIM: "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IjFMVE16YWtpaGlSbGFfOHoyQkVKVlhlV01xbyJ9.ewogICJ2ZXIiOiAiMi4wIiwKICAiaXNzIjogImh0dHBzOi8vbG9naW4ubWljcm9zb2Z0b25saW5lLmNvbS85MTg4MDQwZC02YzY3LTRjNWItYjExMi0zNmEzMDRiNjZkYWQvdjIuMCIsCiAgInN1YiI6ICJBQUFBQUFBQUFBQUFBQUFBQUFBQUFJa3pxRlZyU2FTYUZIeTc4MmJidGFRIiwKICAiYXVkIjogIjZjYjA0MDE4LWEzZjUtNDZhNy1iOTk1LTk0MGM3OGY1YWVmMyIsCiAgImV4cCI6IDE1MzYzNjE0MTEsCiAgImlhdCI6IDE1MzYyNzQ3MTEsCiAgIm5iZiI6IDE1MzYyNzQ3MTEsCiAgIm5hbWUiOiAiQWJlIExpbmNvbG4iLAogICJwcmVmZXJyZWRfdXNlcm5hbWUiOiAiQWJlTGlAbWljcm9zb2Z0LmNvbSIsCiAgIm9pZCI6ICIwMDAwMDAwMC0wMDAwLTAwMDAtNjZmMy0zMzMyZWNhN2VhODEiLAogICJlbWFpbCI6ICJBYmVMaUBtaWNyb3NvZnQuY29tIiwKICAidGlkIjogIjMzMzgwNDBkLTZjNjctNGM1Yi1iMTEyLTM2YTMwNGI2NmRhZCIsCiAgIm5vbmNlIjogIjEyMzUyMyIsCiAgImFpbyI6ICJEZjJVVlhMMWl4IWxNQ1dNU09KQmNGYXR6Y0dmdkZHaGpLdjhxNWcweDczMmRSNU1CNUJpc3ZHUU83WVdCeWpkOGlRRExxIWVHYklEYWt5cDVtbk9yY2RxSGVZU25sdGVwUW1ScDZBSVo4alkiCn0=.1AFWW-Ck5nROwSlltm7GzZvDwUkqvhSQpm55TQsmVo9Y59cLhRXpvB8n-55HCr9Z6G_31_UbeUkoz612I2j_Sm9FFShSDDjoaLQr54CreGIJvjtmS3EkK9a7SJBbcpL1MpUtlfygow39tFjY7EVNW9plWUvRrTgVk7lYLprvfzw-CIqw3gHC-T7IK_m_xkr08INERBtaecwhTeN4chPC4W3jdmw_lIxzC48YoQ0dB1L9-ImX98Egypfrlbm0IBL5spFzL6JDZIRRJOu8vecJvj1mq-IUhGt0MacxX8jdxYLP-KUu2d9MbNKpCKJuZ7p8gwTL5B7NlUdh_dmSviPWrw", LOGIN_AT_STRING: @@ -65,6 +67,13 @@ export const ID_TOKEN_CLAIMS = { aio: "Df2UVXL1ix!lMCWMSOJBcFatzcGfvFGhjKv8q5g0x732dR5MB5BisvGQO7YWByjd8iQDLq!eGbIDakyp5mnOrcdqHeYSnltepQmRp6AIZ8jY", }; +export const ID_TOKEN_ALT_CLAIMS = { + ...ID_TOKEN_CLAIMS, + preferred_username: "AbeLiToo@microsoft.com", + oid: "00000000-0000-0000-0000-000000000000", + tid: "72f988bf-86f1-41af-91ab-2d7cd011db47", +}; + // Test Expiration Vals export const TEST_TOKEN_LIFETIMES = { DEFAULT_EXPIRES_IN: 3599, @@ -665,15 +674,15 @@ export const CACHE_MOCKS = { homeAccountId: "uid.utid", localAccountId: "uid", environment: "login.microsoftonline.com", - tenantId: "microsoft", - username: "mocked_username", + tenantId: "utid", + username: "John Doe", }, MOCK_ACCOUNT_INFO_WITH_NATIVE_ACCOUNT_ID: { - homeAccountId: "uid.utid", + homeAccountId: "uid.utid2", localAccountId: "uid", - environment: "login.microsoftonline.com", - tenantId: "microsoft", - username: "mocked_username", + environment: "login.windows.net", + tenantId: "utid2", + username: "Jane Doe", nativeAccountId: "mocked_native_account_id", }, }; From 48b8ed7e0be17a3a709d39e287256d1432d136ff Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Thu, 5 Oct 2023 14:16:18 -0700 Subject: [PATCH 17/91] Update multi-tenant sample --- .../VanillaJSTestApp2.0/app/multi-tenant/auth.js | 5 +++-- .../VanillaJSTestApp2.0/app/multi-tenant/graph.js | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/auth.js b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/auth.js index 22503ebc1a..1919b0e709 100644 --- a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/auth.js +++ b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/auth.js @@ -40,10 +40,11 @@ async function pickActiveAccountAndTenantProfile(homeAccount) { } async function setActiveAccount(tenantId) { + console.log("Set to: ", tenantId); // Sets the active account to the cached account object matching the tenant profile selected by the user. let activeAccount = myMSALObj.getActiveAccount(); - const tenantProfile = activeAccount.tenantProfiles.get(tenantId); - const newActiveAccount = myMSALObj.getAccountByFilter({ tenantProfile: tenantProfile}); + const newActiveAccount = myMSALObj.getAccount({ tenantId: tenantId }); + console.log(newActiveAccount); if (newActiveAccount) { myMSALObj.setActiveAccount(newActiveAccount); accountId = activeAccount.homeAccountId; diff --git a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/graph.js b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/graph.js index 5b8e4f5e30..bf66627fa1 100644 --- a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/graph.js +++ b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/graph.js @@ -26,6 +26,7 @@ async function seeProfile() { const response = await getTokenRedirect(request, currentAcc).catch(error => { console.log(error); }); + console.log(response); callMSGraph(graphConfig.graphMeEndpoint, response.accessToken, updateUI); } } \ No newline at end of file From 1c1b58247af24c3106399f0509c406c6df388c10 Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Mon, 9 Oct 2023 14:03:33 -0700 Subject: [PATCH 18/91] Use static authority options from configuration and hardcoded metadata to match enviroments in cases where endpoints have not been resolved yet --- .../src/cache/BrowserCacheManager.ts | 7 +- .../src/controllers/StandardController.ts | 29 +---- lib/msal-common/src/authority/Authority.ts | 113 ++++++------------ .../src/authority/AuthorityFactory.ts | 8 +- .../src/authority/AuthorityMetadata.ts | 88 ++++++++++++++ .../src/authority/AuthorityOptions.ts | 10 ++ lib/msal-common/src/cache/CacheManager.ts | 36 +++++- lib/msal-common/src/index.ts | 7 +- .../test/authority/Authority.spec.ts | 3 +- lib/msal-node/src/cache/NodeStorage.ts | 10 +- lib/msal-node/src/client/ClientApplication.ts | 4 +- .../src/client/PublicClientApplication.ts | 22 ---- lib/msal-node/test/cache/TokenCache.spec.ts | 58 +++------ .../cache-unrecognized-entities.json | 2 +- 14 files changed, 213 insertions(+), 184 deletions(-) diff --git a/lib/msal-browser/src/cache/BrowserCacheManager.ts b/lib/msal-browser/src/cache/BrowserCacheManager.ts index 664a937c55..58aa9b2e54 100644 --- a/lib/msal-browser/src/cache/BrowserCacheManager.ts +++ b/lib/msal-browser/src/cache/BrowserCacheManager.ts @@ -35,6 +35,7 @@ import { ClientAuthErrorCodes, PerformanceEvents, IPerformanceClient, + StaticAuthorityOptions, } from "@azure/msal-common"; import { CacheOptions } from "../config/Configuration"; import { @@ -77,6 +78,7 @@ export class BrowserCacheManager extends CacheManager { protected temporaryCacheStorage: IWindowStorage; // Logger instance protected logger: Logger; + // Static authority options // Cookie life calculation (hours * minutes * seconds * ms) protected readonly COOKIE_LIFE_MULTIPLIER = 24 * 60 * 60 * 1000; @@ -85,9 +87,10 @@ export class BrowserCacheManager extends CacheManager { clientId: string, cacheConfig: Required, cryptoImpl: ICrypto, - logger: Logger + logger: Logger, + staticAuthorityOptions?: StaticAuthorityOptions ) { - super(clientId, cryptoImpl, logger); + super(clientId, cryptoImpl, logger, staticAuthorityOptions); this.cacheConfig = cacheConfig; this.logger = logger; this.internalStorage = new MemoryStorage(); diff --git a/lib/msal-browser/src/controllers/StandardController.ts b/lib/msal-browser/src/controllers/StandardController.ts index 1a7fb2ecfa..c9ec99d534 100644 --- a/lib/msal-browser/src/controllers/StandardController.ts +++ b/lib/msal-browser/src/controllers/StandardController.ts @@ -29,8 +29,7 @@ import { createClientAuthError, ClientAuthErrorCodes, AccountFilter, - AuthorityFactory, - AuthorityOptions, + buildStaticAuthorityOptions, } from "@azure/msal-common"; import { BrowserCacheManager, @@ -209,7 +208,8 @@ export class StandardController implements IController { this.config.auth.clientId, this.config.cache, this.browserCrypto, - this.logger + this.logger, + buildStaticAuthorityOptions(this.config.auth) ) : DEFAULT_BROWSER_CACHE_MANAGER( this.config.auth.clientId, @@ -248,8 +248,6 @@ export class StandardController implements IController { // Register listener functions this.trackPageVisibilityWithMeasurement = this.trackPageVisibilityWithMeasurement.bind(this); - // Load local authority metadata into memory - this.loadLocalAuthorityMetadata(); } static async createController( @@ -260,27 +258,6 @@ export class StandardController implements IController { return controller; } - private loadLocalAuthorityMetadata(): void { - const authorityOptions: AuthorityOptions = { - protocolMode: this.config.auth.protocolMode, - OIDCOptions: this.config.auth.OIDCOptions, - knownAuthorities: this.config.auth.knownAuthorities, - cloudDiscoveryMetadata: this.config.auth.cloudDiscoveryMetadata, - authorityMetadata: this.config.auth.authorityMetadata, - }; - - const locallyDiscoveredAuthorityInstance = - AuthorityFactory.createInstance( - this.config.auth.authority, - this.networkClient, - this.browserStorage, - authorityOptions, - this.logger, - this.performanceClient - ); - locallyDiscoveredAuthorityInstance.resolveEndpointsFromLocalSources(); - } - private trackPageVisibility(): void { if (!this.atsAsyncMeasurement) { return; diff --git a/lib/msal-common/src/authority/Authority.ts b/lib/msal-common/src/authority/Authority.ts index 39fd1c8650..5d0d3bd0ac 100644 --- a/lib/msal-common/src/authority/Authority.ts +++ b/lib/msal-common/src/authority/Authority.ts @@ -23,7 +23,8 @@ import { } from "../utils/Constants"; import { EndpointMetadata, - InstanceDiscoveryMetadata, + getCloudDiscoveryMetadataFromHardcodedValues, + getCloudDiscoveryMetadataFromNetworkResponse, InstanceDiscoveryMetadataAliases, } from "./AuthorityMetadata"; import { @@ -33,7 +34,11 @@ import { import { ProtocolMode } from "./ProtocolMode"; import { ICacheManager } from "../cache/interface/ICacheManager"; import { AuthorityMetadataEntity } from "../cache/entities/AuthorityMetadataEntity"; -import { AuthorityOptions, AzureCloudInstance } from "./AuthorityOptions"; +import { + AuthorityOptions, + AzureCloudInstance, + StaticAuthorityOptions, +} from "./AuthorityOptions"; import { CloudInstanceDiscoveryResponse, isCloudInstanceDiscoveryResponse, @@ -383,35 +388,6 @@ export class Authority { return !!this.metadata; } - /** - * Performs locally-sourced, synchronous endpoint discovery to discover aliases, preferred_cache, preferred_network - * and the /authorize, /token and logout endpoints. - */ - public resolveEndpointsFromLocalSources(): void { - this.performanceClient?.addQueueMeasurement( - PerformanceEvents.AuthorityResolveEndpointsFromLocalSources, - this.correlationId - ); - - const metadataEntity = this.getCurrentMetadataEntity(); - - const cloudDiscoverySource = - this.updateCloudDiscoveryMetadataFromLocalSources(metadataEntity); - - this.canonicalAuthority = this.canonicalAuthority.replace( - this.hostnameAndPort, - metadataEntity.preferred_network - ); - const endpointMetadataResult = - this.updateEndpointMetadataFromLocalSources(metadataEntity); - - this.updateCachedMetadata( - metadataEntity, - cloudDiscoverySource, - endpointMetadataResult - ); - } - /** * Perform endpoint discovery to discover aliases, preferred_cache, preferred_network * and the /authorize, /token and logout endpoints. @@ -878,7 +854,9 @@ export class Authority { ); } else { const hardcodedMetadata = - this.getCloudDiscoveryMetadataFromHardcodedValues(); + getCloudDiscoveryMetadataFromHardcodedValues( + this.canonicalAuthority + ); if (hardcodedMetadata) { this.logger.verbose( "Found cloud discovery metadata from hardcoded values." @@ -937,11 +915,10 @@ export class Authority { const parsedResponse = JSON.parse( this.authorityOptions.cloudDiscoveryMetadata ) as CloudInstanceDiscoveryResponse; - const metadata = - Authority.getCloudDiscoveryMetadataFromNetworkResponse( - parsedResponse.metadata, - this.hostnameAndPort - ); + const metadata = getCloudDiscoveryMetadataFromNetworkResponse( + parsedResponse.metadata, + this.hostnameAndPort + ); this.logger.verbose("Parsed the cloud discovery metadata."); if (metadata) { this.logger.verbose( @@ -1047,7 +1024,7 @@ export class Authority { this.logger.verbose( "Attempting to find a match between the developer's authority and the CloudInstanceDiscoveryMetadata returned from the network request." ); - match = Authority.getCloudDiscoveryMetadataFromNetworkResponse( + match = getCloudDiscoveryMetadataFromNetworkResponse( metadata, this.hostnameAndPort ); @@ -1082,24 +1059,6 @@ export class Authority { return match; } - /** - * Get cloud discovery metadata for common authorities - */ - private getCloudDiscoveryMetadataFromHardcodedValues(): CloudDiscoveryMetadata | null { - if (this.canonicalAuthority in InstanceDiscoveryMetadata) { - const hardcodedMetadataResponse = - InstanceDiscoveryMetadata[this.canonicalAuthority]; - const metadata = - Authority.getCloudDiscoveryMetadataFromNetworkResponse( - hardcodedMetadataResponse.metadata, - this.hostnameAndPort - ); - return metadata; - } - - return null; - } - /** * Helper function to determine if this host is included in the knownAuthorities config option */ @@ -1156,25 +1115,6 @@ export class Authority { }; } - /** - * Searches instance discovery network response for the entry that contains the host in the aliases list - * @param response - * @param authority - */ - static getCloudDiscoveryMetadataFromNetworkResponse( - response: CloudDiscoveryMetadata[], - authority: string - ): CloudDiscoveryMetadata | null { - for (let i = 0; i < response.length; i++) { - const metadata = response[i]; - if (metadata.aliases.indexOf(authority) > -1) { - return metadata; - } - } - - return null; - } - /** * helper function to generate environment from authority object */ @@ -1294,9 +1234,7 @@ export class Authority { * @param authority */ static transformCIAMAuthority(authority: string): string { - let ciamAuthority = authority.endsWith(Constants.FORWARD_SLASH) - ? authority - : `${authority}${Constants.FORWARD_SLASH}`; + let ciamAuthority = authority; const authorityUrl = new UrlString(authority); const authorityUrlComponents = authorityUrl.getUrlComponents(); @@ -1315,3 +1253,22 @@ export class Authority { return ciamAuthority; } } + +export function formatAuthorityUri(authorityUri: string): string { + return authorityUri.endsWith(Constants.FORWARD_SLASH) + ? authorityUri + : `${authorityUri}${Constants.FORWARD_SLASH}`; +} + +export function buildStaticAuthorityOptions( + authOptions: Partial +): StaticAuthorityOptions { + return { + canonicalAuthority: authOptions.authority + ? formatAuthorityUri(authOptions.authority) + : undefined, + knownAuthorities: authOptions.knownAuthorities, + cloudDiscoveryMetadata: authOptions.cloudDiscoveryMetadata, + authorityMetadata: authOptions.authorityMetadata, + }; +} diff --git a/lib/msal-common/src/authority/AuthorityFactory.ts b/lib/msal-common/src/authority/AuthorityFactory.ts index 88bb1a61b4..17046ea6d2 100644 --- a/lib/msal-common/src/authority/AuthorityFactory.ts +++ b/lib/msal-common/src/authority/AuthorityFactory.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. */ -import { Authority } from "./Authority"; +import { Authority, formatAuthorityUri } from "./Authority"; import { createClientConfigurationError, ClientConfigurationErrorCodes, @@ -45,9 +45,9 @@ export class AuthorityFactory { PerformanceEvents.AuthorityFactoryCreateDiscoveredInstance, correlationId ); - - const authorityUriFinal = - Authority.transformCIAMAuthority(authorityUri); + const authorityUriFinal = Authority.transformCIAMAuthority( + formatAuthorityUri(authorityUri) + ); // Initialize authority and perform discovery endpoint check. const acquireTokenAuthority: Authority = diff --git a/lib/msal-common/src/authority/AuthorityMetadata.ts b/lib/msal-common/src/authority/AuthorityMetadata.ts index 6cbf6ed5d2..f2ef2de19a 100644 --- a/lib/msal-common/src/authority/AuthorityMetadata.ts +++ b/lib/msal-common/src/authority/AuthorityMetadata.ts @@ -3,6 +3,13 @@ * Licensed under the MIT License. */ +import { + ClientConfigurationErrorCodes, + createClientConfigurationError, +} from "../error/ClientConfigurationError"; +import { UrlString } from "../url/UrlString"; +import { CloudDiscoveryMetadata } from "./CloudDiscoveryMetadata"; + export const rawMetdataJSON = { endpointMetadata: { "https://login.microsoftonline.com/common/": { @@ -951,3 +958,84 @@ for (const key in InstanceDiscoveryMetadata) { } } } + +/** + * Returns aliases for the given canonical authority if found in hardcoded Instance Discovery Metadata or null if not found + * @param canonicalAuthority + * @returns + */ +export function getHardcodedAliasesForCanonicalAuthority( + canonicalAuthority?: string +): string[] | null { + if (canonicalAuthority) { + const instanceDiscoveryMetadata = + getCloudDiscoveryMetadataFromHardcodedValues(canonicalAuthority); + if (instanceDiscoveryMetadata) { + return instanceDiscoveryMetadata.aliases; + } + } + return null; +} + +/** + * Returns aliases for from the raw cloud discovery metadata given in configuration or null if no configuration was provided + * @param rawCloudDiscoveryMetadata + * @returns + */ +export function getAliasesFromConfigMetadata( + rawCloudDiscoveryMetadata?: string +): string[] | null { + if (rawCloudDiscoveryMetadata) { + try { + const metadata = JSON.parse(rawCloudDiscoveryMetadata); + if (metadata) { + return metadata.aliases; + } + } catch (e) { + throw createClientConfigurationError( + ClientConfigurationErrorCodes.invalidCloudDiscoveryMetadata + ); + } + } + return null; +} + +/** + * Searches instance discovery network response for the entry that contains the host in the aliases list + * @param response + * @param authority + */ +export function getCloudDiscoveryMetadataFromNetworkResponse( + response: CloudDiscoveryMetadata[], + authority: string +): CloudDiscoveryMetadata | null { + for (let i = 0; i < response.length; i++) { + const metadata = response[i]; + if (metadata.aliases.includes(authority)) { + return metadata; + } + } + + return null; +} + +/** + * Get cloud discovery metadata for common authorities + */ +export function getCloudDiscoveryMetadataFromHardcodedValues( + canonicalAuthority: string +): CloudDiscoveryMetadata | null { + const canonicalAuthorityUrlComponents = new UrlString( + canonicalAuthority + ).getUrlComponents(); + + if (canonicalAuthority in InstanceDiscoveryMetadata) { + const metadata = getCloudDiscoveryMetadataFromNetworkResponse( + InstanceDiscoveryMetadata[canonicalAuthority].metadata, + canonicalAuthorityUrlComponents.HostNameAndPort + ); + return metadata; + } + + return null; +} diff --git a/lib/msal-common/src/authority/AuthorityOptions.ts b/lib/msal-common/src/authority/AuthorityOptions.ts index fde3795ad0..5c97d1d1d7 100644 --- a/lib/msal-common/src/authority/AuthorityOptions.ts +++ b/lib/msal-common/src/authority/AuthorityOptions.ts @@ -15,6 +15,16 @@ export type AuthorityOptions = { authorityMetadata: string; skipAuthorityMetadataCache?: boolean; azureRegionConfiguration?: AzureRegionConfiguration; + authority?: string; +}; + +export type StaticAuthorityOptions = Partial< + Pick< + AuthorityOptions, + "knownAuthorities" | "cloudDiscoveryMetadata" | "authorityMetadata" + > +> & { + canonicalAuthority?: string; }; export const AzureCloudInstance = { diff --git a/lib/msal-common/src/cache/CacheManager.ts b/lib/msal-common/src/cache/CacheManager.ts index 8ab7704c86..eda506119b 100644 --- a/lib/msal-common/src/cache/CacheManager.ts +++ b/lib/msal-common/src/cache/CacheManager.ts @@ -42,6 +42,11 @@ import { BaseAuthRequest } from "../request/BaseAuthRequest"; import { Logger } from "../logger/Logger"; import { name, version } from "../packageMetadata"; import { StoreInCache } from "../request/StoreInCache"; +import { + getAliasesFromConfigMetadata, + getHardcodedAliasesForCanonicalAuthority, +} from "../authority/AuthorityMetadata"; +import { StaticAuthorityOptions } from "../authority/AuthorityOptions"; /** * Interface class which implement cache storage functions used by MSAL to perform validity checks, and store tokens. @@ -52,11 +57,18 @@ export abstract class CacheManager implements ICacheManager { protected cryptoImpl: ICrypto; // Instance of logger for functions defined in the msal-common layer private commonLogger: Logger; - - constructor(clientId: string, cryptoImpl: ICrypto, logger: Logger) { + private staticAuthorityOptions?: StaticAuthorityOptions; + + constructor( + clientId: string, + cryptoImpl: ICrypto, + logger: Logger, + staticAuthorityOptions?: StaticAuthorityOptions + ) { this.clientId = clientId; this.cryptoImpl = cryptoImpl; this.commonLogger = logger.clone(name, version); + this.staticAuthorityOptions = staticAuthorityOptions; } /** @@ -230,8 +242,8 @@ export abstract class CacheManager implements ICacheManager { .map((accountEntity) => { return this.getAccountInfoFromEntity(accountEntity); }) - .filter((accoutnInfo) => { - return accoutnInfo.idTokenClaims; + .filter((accountInfo) => { + return accountInfo.idTokenClaims; }); } @@ -1375,6 +1387,21 @@ export abstract class CacheManager implements ICacheManager { entity: AccountEntity | CredentialEntity | AppMetadataEntity, environment: string ): boolean { + // Check static authority options first for cases where authority metadata has not been resolved and cached yet + if (this.staticAuthorityOptions) { + const staticAliases = + getAliasesFromConfigMetadata( + this.staticAuthorityOptions.cloudDiscoveryMetadata + ) || + getHardcodedAliasesForCanonicalAuthority( + this.staticAuthorityOptions.canonicalAuthority + ) || + this.staticAuthorityOptions.knownAuthorities; + if (staticAliases && staticAliases.includes(entity.environment)) { + return true; + } + } + // Query metadata cache if no static authority configuration has aliases that match enviroment const cloudMetadata = this.getAuthorityMetadataByAlias(environment); if ( cloudMetadata && @@ -1382,7 +1409,6 @@ export abstract class CacheManager implements ICacheManager { ) { return true; } - return false; } diff --git a/lib/msal-common/src/index.ts b/lib/msal-common/src/index.ts index 472da643da..fb53df6c69 100644 --- a/lib/msal-common/src/index.ts +++ b/lib/msal-common/src/index.ts @@ -39,10 +39,15 @@ export { buildClientInfoFromHomeAccountId, } from "./account/ClientInfo"; // Authority -export { Authority } from "./authority/Authority"; +export { + Authority, + formatAuthorityUri, + buildStaticAuthorityOptions, +} from "./authority/Authority"; export { AuthorityOptions, AzureCloudInstance, + StaticAuthorityOptions, } from "./authority/AuthorityOptions"; export { AuthorityFactory } from "./authority/AuthorityFactory"; export { AuthorityType } from "./authority/AuthorityType"; diff --git a/lib/msal-common/test/authority/Authority.spec.ts b/lib/msal-common/test/authority/Authority.spec.ts index b977c8bf92..9d3f6301ed 100644 --- a/lib/msal-common/test/authority/Authority.spec.ts +++ b/lib/msal-common/test/authority/Authority.spec.ts @@ -29,6 +29,7 @@ import { OpenIdConfigResponse } from "../../src/authority/OpenIdConfigResponse"; import { Logger, LogLevel, UrlString } from "../../src"; import { RegionDiscovery } from "../../src/authority/RegionDiscovery"; import { InstanceDiscoveryMetadata } from "../../src/authority/AuthorityMetadata"; +import * as authorityMetadata from "../../src/authority/AuthorityMetadata"; let mockStorage: MockStorageClass; @@ -1772,7 +1773,7 @@ describe("Authority.ts Class Unit Tests", () => { ); getCloudDiscoveryMetadataFromHarcodedValuesSpy = jest.spyOn( - Authority.prototype as any, + authorityMetadata, "getCloudDiscoveryMetadataFromHardcodedValues" ); diff --git a/lib/msal-node/src/cache/NodeStorage.ts b/lib/msal-node/src/cache/NodeStorage.ts index 070d70a5b1..074918f35e 100644 --- a/lib/msal-node/src/cache/NodeStorage.ts +++ b/lib/msal-node/src/cache/NodeStorage.ts @@ -18,6 +18,7 @@ import { ICrypto, AuthorityMetadataEntity, ValidCredentialType, + StaticAuthorityOptions, } from "@azure/msal-common"; import { Deserializer } from "./serializer/Deserializer.js"; @@ -38,8 +39,13 @@ export class NodeStorage extends CacheManager { private cache: CacheKVStore = {}; private changeEmitters: Array = []; - constructor(logger: Logger, clientId: string, cryptoImpl: ICrypto) { - super(clientId, cryptoImpl, logger); + constructor( + logger: Logger, + clientId: string, + cryptoImpl: ICrypto, + staticAuthorityOptions?: StaticAuthorityOptions + ) { + super(clientId, cryptoImpl, logger, staticAuthorityOptions); this.logger = logger; } diff --git a/lib/msal-node/src/client/ClientApplication.ts b/lib/msal-node/src/client/ClientApplication.ts index 4d4dc1aa5a..82c24f8d88 100644 --- a/lib/msal-node/src/client/ClientApplication.ts +++ b/lib/msal-node/src/client/ClientApplication.ts @@ -32,6 +32,7 @@ import { StringUtils, createClientAuthError, ClientAuthErrorCodes, + buildStaticAuthorityOptions, } from "@azure/msal-common"; import { Configuration, @@ -95,7 +96,8 @@ export abstract class ClientApplication { this.storage = new NodeStorage( this.logger, this.config.auth.clientId, - this.cryptoProvider + this.cryptoProvider, + buildStaticAuthorityOptions(this.config.auth) ); this.tokenCache = new TokenCache( this.storage, diff --git a/lib/msal-node/src/client/PublicClientApplication.ts b/lib/msal-node/src/client/PublicClientApplication.ts index 97abaf75c8..c2e55f364b 100644 --- a/lib/msal-node/src/client/PublicClientApplication.ts +++ b/lib/msal-node/src/client/PublicClientApplication.ts @@ -22,8 +22,6 @@ import { AccountInfo, INativeBrokerPlugin, ServerAuthorizationCodeResponse, - AuthorityOptions, - AuthorityFactory, } from "@azure/msal-common"; import { Configuration } from "../config/Configuration.js"; import { ClientApplication } from "./ClientApplication.js"; @@ -68,7 +66,6 @@ export class PublicClientApplication */ constructor(configuration: Configuration) { super(configuration); - this.loadLocalAuthorityMetadata(); if (this.config.broker.nativeBrokerPlugin) { if (this.config.broker.nativeBrokerPlugin.isBrokerAvailable) { this.nativeBrokerPlugin = this.config.broker.nativeBrokerPlugin; @@ -83,25 +80,6 @@ export class PublicClientApplication } } - private loadLocalAuthorityMetadata(): void { - const authorityOptions: AuthorityOptions = { - protocolMode: this.config.auth.protocolMode, - knownAuthorities: this.config.auth.knownAuthorities, - cloudDiscoveryMetadata: this.config.auth.cloudDiscoveryMetadata, - authorityMetadata: this.config.auth.authorityMetadata, - }; - - const locallyDiscoveredAuthorityInstance = - AuthorityFactory.createInstance( - this.config.auth.authority, - this.config.system.networkClient, - this.storage, - authorityOptions, - this.logger - ); - locallyDiscoveredAuthorityInstance.resolveEndpointsFromLocalSources(); - } - /** * Acquires a token from the authority using OAuth2.0 device code flow. * This flow is designed for devices that do not have access to a browser or have input constraints. diff --git a/lib/msal-node/test/cache/TokenCache.spec.ts b/lib/msal-node/test/cache/TokenCache.spec.ts index ec2badc94a..5310c3901d 100644 --- a/lib/msal-node/test/cache/TokenCache.spec.ts +++ b/lib/msal-node/test/cache/TokenCache.spec.ts @@ -3,6 +3,7 @@ import { Logger, TokenCacheContext, ICachePlugin, + buildStaticAuthorityOptions, } from "@azure/msal-common"; import { NodeStorage } from "../../src/cache/NodeStorage"; import { TokenCache } from "../../src/cache/TokenCache"; @@ -10,7 +11,7 @@ import { promises as fs } from "fs"; import { version, name } from "../../package.json"; import { DEFAULT_CRYPTO_IMPLEMENTATION, - TEST_CONSTANTS, + ID_TOKEN_CLAIMS, } from "../utils/TestConstants"; import { Deserializer } from "../../src/cache/serializer/Deserializer"; import { JsonCache } from "../../src"; @@ -20,6 +21,7 @@ const msalCommon: MSALCommonModule = jest.requireActual("@azure/msal-common"); describe("TokenCache tests", () => { let logger: Logger; + let storage: NodeStorage; beforeEach(() => { const loggerOptions = { @@ -30,15 +32,23 @@ describe("TokenCache tests", () => { logLevel: LogLevel.Info, }; logger = new Logger(loggerOptions!, name, version); + storage = new NodeStorage( + logger, + "mock_client_id", + { + ...DEFAULT_CRYPTO_IMPLEMENTATION, + base64Decode: (): string => { + return JSON.stringify(ID_TOKEN_CLAIMS); + }, + }, + buildStaticAuthorityOptions({ + authority: "https://login.microsoftonline.com/common", + }) + ); jest.restoreAllMocks(); }); it("Constructor tests builds default token cache", async () => { - let storage: NodeStorage = new NodeStorage( - logger, - TEST_CONSTANTS.CLIENT_ID, - DEFAULT_CRYPTO_IMPLEMENTATION - ); const tokenCache = new TokenCache(storage, logger); expect(tokenCache).toBeInstanceOf(TokenCache); expect(tokenCache.hasChanged()).toEqual(false); @@ -47,11 +57,6 @@ describe("TokenCache tests", () => { it("TokenCache serialize/deserialize", () => { const cache = require("./cache-test-files/default-cache.json"); - const storage: NodeStorage = new NodeStorage( - logger, - TEST_CONSTANTS.CLIENT_ID, - DEFAULT_CRYPTO_IMPLEMENTATION - ); const tokenCache = new TokenCache(storage, logger); tokenCache.deserialize(JSON.stringify(cache)); @@ -64,11 +69,6 @@ describe("TokenCache tests", () => { it("TokenCache should not fail when attempting to deserialize an empty string", () => { const cache = ""; - const storage: NodeStorage = new NodeStorage( - logger, - TEST_CONSTANTS.CLIENT_ID, - DEFAULT_CRYPTO_IMPLEMENTATION - ); const tokenCache = new TokenCache(storage, logger); tokenCache.deserialize(cache); @@ -77,11 +77,6 @@ describe("TokenCache tests", () => { it("TokenCache serialize/deserialize, does not remove unrecognized entities", () => { const cache = require("./cache-test-files/cache-unrecognized-entities.json"); - const storage: NodeStorage = new NodeStorage( - logger, - TEST_CONSTANTS.CLIENT_ID, - DEFAULT_CRYPTO_IMPLEMENTATION - ); const tokenCache = new TokenCache(storage, logger); tokenCache.deserialize(JSON.stringify(cache)); @@ -96,11 +91,7 @@ describe("TokenCache tests", () => { // TokenCache should not remove unrecognized entities from JSON file, even if they // are deeply nested, and should write them back out const cache = require("./cache-test-files/cache-unrecognized-entities.json"); - const storage: NodeStorage = new NodeStorage( - logger, - TEST_CONSTANTS.CLIENT_ID, - DEFAULT_CRYPTO_IMPLEMENTATION - ); + const tokenCache = new TokenCache(storage, logger); tokenCache.deserialize(JSON.stringify(cache)); @@ -138,11 +129,6 @@ describe("TokenCache tests", () => { afterCacheAccess, }; - const storage = new NodeStorage( - logger, - TEST_CONSTANTS.CLIENT_ID, - DEFAULT_CRYPTO_IMPLEMENTATION - ); const tokenCache = new TokenCache(storage, logger, cachePlugin); const mockTokenCacheContextInstance = { @@ -177,11 +163,6 @@ describe("TokenCache tests", () => { }); it("should return an empty KV store if TokenCache is empty", () => { - const storage: NodeStorage = new NodeStorage( - logger, - TEST_CONSTANTS.CLIENT_ID, - DEFAULT_CRYPTO_IMPLEMENTATION - ); const tokenCache = new TokenCache(storage, logger); expect(tokenCache.getKVStore()).toEqual({}); @@ -189,11 +170,6 @@ describe("TokenCache tests", () => { it("should return stored entities in KV store", () => { const cache: JsonCache = require("./cache-test-files/default-cache.json"); - const storage: NodeStorage = new NodeStorage( - logger, - TEST_CONSTANTS.CLIENT_ID, - DEFAULT_CRYPTO_IMPLEMENTATION - ); const tokenCache = new TokenCache(storage, logger); tokenCache.deserialize(JSON.stringify(cache)); diff --git a/lib/msal-node/test/cache/cache-test-files/cache-unrecognized-entities.json b/lib/msal-node/test/cache/cache-test-files/cache-unrecognized-entities.json index bc071451df..65badfd0a4 100644 --- a/lib/msal-node/test/cache/cache-test-files/cache-unrecognized-entities.json +++ b/lib/msal-node/test/cache/cache-test-files/cache-unrecognized-entities.json @@ -66,7 +66,7 @@ "realm": "microsoft", "environment": "login.microsoftonline.com", "credential_type": "IdToken", - "secret": "header.eyJvaWQiOiAib2JqZWN0MTIzNCIsICJwcmVmZXJyZWRfdXNlcm5hbWUiOiAiSm9obiBEb2UiLCAic3ViIjogInN1YiJ9.signature", + "secret": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IjFMVE16YWtpaGlSbGFfOHoyQkVKVlhlV01xbyJ9.eyJ2ZXIiOiIyLjAiLCJpc3MiOiJodHRwczovL2xvZ2luLm1pY3Jvc29mdG9ubGluZS5jb20vOTE4ODA0MGQtNmM2Ny00YzViLWIxMTItMzZhMzA0YjY2ZGFkL3YyLjAiLCJzdWIiOiJBQUFBQUFBQUFBQUFBQUFBQUFBQUFJa3pxRlZyU2FTYUZIeTc4MmJidGFRIiwiYXVkIjoiNmNiMDQwMTgtYTNmNS00NmE3LWI5OTUtOTQwYzc4ZjVhZWYzIiwiZXhwIjoxNTM2MzYxNDExLCJpYXQiOjE1MzYyNzQ3MTEsIm5iZiI6MTUzNjI3NDcxMSwibmFtZSI6IkFiZSBMaW5jb2xuIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiQWJlTGlAbWljcm9zb2Z0LmNvbSIsIm9pZCI6IjAwMDAwMDAwLTAwMDAtMDAwMC02NmYzLTMzMzJlY2E3ZWE4MSIsInRpZCI6IjMzMzgwNDBkLTZjNjctNGM1Yi1iMTEyLTM2YTMwNGI2NmRhZCIsIm5vbmNlIjoiMTIzNTIzIiwiYWlvIjoiRGYyVVZYTDFpeCFsTUNXTVNPSkJjRmF0emNHZnZGR2hqS3Y4cTVnMHg3MzJkUjVNQjVCaXN2R1FPN1lXQnlqZDhpUURMcSFlR2JJRGFreXA1bW5PcmNkcUhlWVNubHRlcFFtUnA2QUlaOGpZIn0=.1AFWW-Ck5nROwSlltm7GzZvDwUkqvhSQpm55TQsmVo9Y59cLhRXpvB8n-55HCr9Z6G_31_UbeUkoz612I2j_Sm9FFShSDDjoaLQr54CreGIJvjtmS3EkK9a7SJBbcpL1MpUtlfygow39tFjY7EVNW9plWUvRrTgVk7lYLprvfzw-CIqw3gHC-T7IK_m_xkr08INERBtaecwhTeN4chPC4W3jdmw_lIxzC48YoQ0dB1L9-ImX98Egypfrlbm0IBL5spFzL6JDZIRRJOu8vecJvj1mq-IUhGt0MacxX8jdxYLP-KUu2d9MbNKpCKJuZ7p8gwTL5B7NlUdh_dmSviPWrw", "client_id": "mock_client_id", "home_account_id": "uid.utid" } From 2f7621c421b09ca0bd966473774f166b5515af03 Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Tue, 10 Oct 2023 09:59:56 -0700 Subject: [PATCH 19/91] Update lib/msal-browser/test/interaction_client/BaseInteractionClient.spec.ts Co-authored-by: Thomas Norling --- .../test/interaction_client/BaseInteractionClient.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/msal-browser/test/interaction_client/BaseInteractionClient.spec.ts b/lib/msal-browser/test/interaction_client/BaseInteractionClient.spec.ts index dd7ae05aaa..a4b7ec2a46 100644 --- a/lib/msal-browser/test/interaction_client/BaseInteractionClient.spec.ts +++ b/lib/msal-browser/test/interaction_client/BaseInteractionClient.spec.ts @@ -181,7 +181,7 @@ describe("BaseInteractionClient", () => { afterEach(() => { window.sessionStorage.clear(); - jest.clearAllMocks(); + jest.restoreAllMocks(); }); it("Removes all accounts from cache if no account provided", async () => { From 69ebbea8ef71e45167de9653ac2e7088fb721eec Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Tue, 10 Oct 2023 16:30:41 -0700 Subject: [PATCH 20/91] Refactor active account logic and native account lookup --- .../src/cache/BrowserCacheManager.ts | 107 +-- .../src/controllers/StandardController.ts | 8 +- .../NativeInteractionClient.ts | 14 +- .../test/app/PublicClientApplication.spec.ts | 19 +- .../test/cache/BrowserCacheManager.spec.ts | 636 ------------------ .../test/cache/TokenCache.spec.ts | 1 - .../NativeInteractionClient.spec.ts | 11 +- .../interaction_client/RedirectClient.spec.ts | 35 +- .../SilentCacheClient.spec.ts | 8 +- .../test/utils/StringConstants.ts | 2 +- lib/msal-common/src/account/AccountInfo.ts | 7 + lib/msal-common/src/cache/CacheManager.ts | 119 +++- lib/msal-common/src/cache/utils/CacheTypes.ts | 1 + lib/msal-common/src/index.ts | 6 +- .../test/cache/CacheManager.spec.ts | 45 -- lib/msal-node/src/cache/NodeStorage.ts | 2 + .../test/cache/serializer/Serializer.spec.ts | 1 - .../client/PublicClientApplication.spec.ts | 28 +- lib/msal-node/test/utils/TestConstants.ts | 1 + 19 files changed, 192 insertions(+), 859 deletions(-) diff --git a/lib/msal-browser/src/cache/BrowserCacheManager.ts b/lib/msal-browser/src/cache/BrowserCacheManager.ts index 58aa9b2e54..9ab0294b20 100644 --- a/lib/msal-browser/src/cache/BrowserCacheManager.ts +++ b/lib/msal-browser/src/cache/BrowserCacheManager.ts @@ -434,6 +434,8 @@ export class BrowserCacheManager extends CacheManager { */ setAccount(account: AccountEntity): void { this.logger.trace("BrowserCacheManager.setAccount called"); + // Remove ID token claims before saving account entity + account.idTokenClaims = undefined; const key = account.generateAccountKey(); this.setItem(key, JSON.stringify(account)); this.addAccountKeyToMap(key); @@ -1031,10 +1033,9 @@ export class BrowserCacheManager extends CacheManager { ); return null; } - const activeAccount = - this.getAccountInfoByFilter({ - localAccountId: activeAccountValueLocal, - })[0] || null; + const activeAccount = this.getAccountInfoFilteredBy({ + localAccountId: activeAccountValueLocal, + }); if (activeAccount) { this.logger.trace( "BrowserCacheManager.getActiveAccount: Legacy active account cache schema found" @@ -1054,12 +1055,10 @@ export class BrowserCacheManager extends CacheManager { this.logger.trace( "BrowserCacheManager.getActiveAccount: Active account filters schema found" ); - return ( - this.getAccountInfoByFilter({ - homeAccountId: activeAccountValueObj.homeAccountId, - localAccountId: activeAccountValueObj.localAccountId, - })[0] || null - ); + return this.getAccountInfoFilteredBy({ + homeAccountId: activeAccountValueObj.homeAccountId, + localAccountId: activeAccountValueObj.localAccountId, + }); } this.logger.trace( "BrowserCacheManager.getActiveAccount: No active account found" @@ -1101,94 +1100,6 @@ export class BrowserCacheManager extends CacheManager { } } - /** - * Gets a list of accounts that match all of the filters provided - * @param account - */ - getAccountInfoByFilter( - accountFilter: Partial> - ): AccountInfo[] { - const allAccounts = this.getAllAccounts(); - this.logger.trace( - `BrowserCacheManager.getAccountInfoByFilter: total ${allAccounts.length} accounts found` - ); - - return allAccounts.filter((accountObj) => { - if ( - accountFilter.username && - accountFilter.username.toLowerCase() !== - accountObj.username.toLowerCase() - ) { - return false; - } - - if ( - accountFilter.homeAccountId && - accountFilter.homeAccountId !== accountObj.homeAccountId - ) { - return false; - } - - if ( - accountFilter.localAccountId && - accountFilter.localAccountId !== accountObj.localAccountId - ) { - return false; - } - - if ( - accountFilter.tenantId && - accountFilter.tenantId !== accountObj.tenantId - ) { - return false; - } - - if ( - accountFilter.environment && - accountFilter.environment !== accountObj.environment - ) { - return false; - } - - return true; - }); - } - - /** - * Checks the cache for accounts matching loginHint or SID - * @param loginHint - * @param sid - */ - getAccountInfoByHints( - loginHint?: string, - sid?: string - ): AccountInfo | null { - const matchingAccounts = this.getAllAccounts().filter((accountInfo) => { - if (sid) { - const accountSid = - accountInfo.idTokenClaims && - accountInfo.idTokenClaims["sid"]; - return sid === accountSid; - } - - if (loginHint) { - return loginHint === accountInfo.username; - } - - return false; - }); - - if (matchingAccounts.length === 1) { - return matchingAccounts[0]; - } else if (matchingAccounts.length > 1) { - throw createClientAuthError( - ClientAuthErrorCodes.multipleMatchingAccounts - ); - } - - return null; - } - /** * fetch throttling entity from the platform cache * @param throttlingCacheKey diff --git a/lib/msal-browser/src/controllers/StandardController.ts b/lib/msal-browser/src/controllers/StandardController.ts index c9ec99d534..0e35c20a8c 100644 --- a/lib/msal-browser/src/controllers/StandardController.ts +++ b/lib/msal-browser/src/controllers/StandardController.ts @@ -1561,10 +1561,10 @@ export class StandardController implements IController { ): string { const account = request.account || - this.browserStorage.getAccountInfoByHints( - request.loginHint, - request.sid - ) || + this.browserStorage.getAccountInfoFilteredBy({ + loginHint: request.loginHint, + sid: request.sid, + }) || this.getActiveAccount(); return (account && account.nativeAccountId) || ""; diff --git a/lib/msal-browser/src/interaction_client/NativeInteractionClient.ts b/lib/msal-browser/src/interaction_client/NativeInteractionClient.ts index 674f4b6149..5808336539 100644 --- a/lib/msal-browser/src/interaction_client/NativeInteractionClient.ts +++ b/lib/msal-browser/src/interaction_client/NativeInteractionClient.ts @@ -33,6 +33,7 @@ import { invokeAsync, createAuthError, AuthErrorCodes, + updateAccountInfo, } from "@azure/msal-common"; import { BaseInteractionClient } from "./BaseInteractionClient"; import { BrowserConfiguration } from "../config/Configuration"; @@ -238,9 +239,10 @@ export class NativeInteractionClient extends BaseInteractionClient { throw createClientAuthError(ClientAuthErrorCodes.noAccountFound); } // fetch the account from browser cache - const account = this.browserStorage.getAccountInfoFilteredBy({ + const account = this.browserStorage.getBaseAccountInfo({ nativeAccountId, }); + if (!account) { throw createClientAuthError(ClientAuthErrorCodes.noAccountFound); } @@ -254,9 +256,17 @@ export class NativeInteractionClient extends BaseInteractionClient { const result = await this.silentCacheClient.acquireToken( silentRequest ); + + const fullAccount = + result.idToken && result.idTokenClaims + ? updateAccountInfo(account, { + idTokenClaims: result.idTokenClaims as TokenClaims, + }) + : account; + return { ...result, - account, + account: fullAccount, }; } catch (e) { throw e; diff --git a/lib/msal-browser/test/app/PublicClientApplication.spec.ts b/lib/msal-browser/test/app/PublicClientApplication.spec.ts index 56a1e66dd8..bd7cf78b73 100644 --- a/lib/msal-browser/test/app/PublicClientApplication.spec.ts +++ b/lib/msal-browser/test/app/PublicClientApplication.spec.ts @@ -5029,7 +5029,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { realm: testAccountInfo3.tenantId, environment: testAccountInfo3.environment, credentialType: "IdToken", - secret: TEST_TOKENS.IDTOKEN_V2, + secret: TEST_TOKENS.ID_TOKEN_V2_WITH_LOGIN_HINT, clientId: TEST_CONFIG.MSAL_CLIENT_ID, homeAccountId: testAccountInfo3.homeAccountId, login_hint: "testLoginHint", @@ -5066,7 +5066,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { realm: testAccountInfo4.tenantId, environment: testAccountInfo4.environment, credentialType: "IdToken", - secret: TEST_TOKENS.IDTOKEN_V2, + secret: TEST_TOKENS.ID_TOKEN_V2_WITH_UPN, clientId: TEST_CONFIG.MSAL_CLIENT_ID, homeAccountId: testAccountInfo4.homeAccountId, upn: "testUpn", @@ -5297,12 +5297,12 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { // Account 1 const testAccountInfo1: AccountInfo = { authorityType: "MSSTS", - homeAccountId: TEST_DATA_CLIENT_INFO.TEST_HOME_ACCOUNT_ID, + homeAccountId: `${ID_TOKEN_CLAIMS.oid}.${ID_TOKEN_CLAIMS.tid}`, environment: "login.windows.net", - tenantId: TEST_DATA_CLIENT_INFO.TEST_UTID, + tenantId: ID_TOKEN_CLAIMS.tid, username: "example@microsoft.com", name: "Abe Lincoln", - localAccountId: TEST_CONFIG.OID, + localAccountId: ID_TOKEN_CLAIMS.oid, idToken: TEST_TOKENS.IDTOKEN_V2, idTokenClaims: ID_TOKEN_CLAIMS, nativeAccountId: undefined, @@ -5310,7 +5310,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { const testAccount1: AccountEntity = new AccountEntity(); testAccount1.homeAccountId = testAccountInfo1.homeAccountId; - testAccount1.localAccountId = TEST_CONFIG.OID; + testAccount1.localAccountId = testAccountInfo1.localAccountId; testAccount1.environment = testAccountInfo1.environment; testAccount1.realm = testAccountInfo1.tenantId; testAccount1.username = testAccountInfo1.username; @@ -5318,7 +5318,6 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { testAccount1.authorityType = "MSSTS"; testAccount1.clientInfo = TEST_DATA_CLIENT_INFO.TEST_CLIENT_INFO_B64ENCODED; - testAccount1.idTokenClaims = ID_TOKEN_CLAIMS; const idTokenData1 = { realm: testAccountInfo1.tenantId, @@ -5474,6 +5473,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { // @ts-ignore const localStorage = pcaLocal.browserStorage; localStorage.setAccount(testAccount1); + localStorage.setIdTokenCredential(idToken1); localStorage.setItem( localStorage.generateCacheKey( PersistentCacheKeys.ACTIVE_ACCOUNT @@ -5733,8 +5733,8 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { describe("hydrateCache tests", () => { const testAccount: AccountInfo = { authorityType: "MSSTS", - homeAccountId: TEST_DATA_CLIENT_INFO.TEST_HOME_ACCOUNT_ID, - localAccountId: TEST_DATA_CLIENT_INFO.TEST_UID, + homeAccountId: `${ID_TOKEN_CLAIMS.oid}.${ID_TOKEN_CLAIMS.tid}`, + localAccountId: ID_TOKEN_CLAIMS.oid, environment: "login.windows.net", tenantId: ID_TOKEN_CLAIMS.tid, username: ID_TOKEN_CLAIMS.preferred_username, @@ -5828,7 +5828,6 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { await pca.hydrateCache(nativeResult, nativeRequest); const result = await pca.acquireTokenSilent(nativeRequest); // Get tokens from the cache - // Verify tokens were returned from internal memory expect(result.accessToken).toEqual(nativeResult.accessToken); expect(result.idToken).toEqual(nativeResult.idToken); diff --git a/lib/msal-browser/test/cache/BrowserCacheManager.spec.ts b/lib/msal-browser/test/cache/BrowserCacheManager.spec.ts index 338d70f60c..dc4c3b6b29 100644 --- a/lib/msal-browser/test/cache/BrowserCacheManager.spec.ts +++ b/lib/msal-browser/test/cache/BrowserCacheManager.spec.ts @@ -3412,641 +3412,5 @@ describe("BrowserCacheManager tests", () => { refreshToken: ["refreshToken2"], }); }); - - describe("getAccountInfoByFilter", () => { - cacheConfig = { - temporaryCacheLocation: BrowserCacheLocation.SessionStorage, - cacheLocation: BrowserCacheLocation.SessionStorage, - storeAuthStateInCookie: false, - secureCookies: false, - cacheMigrationEnabled: false, - claimsBasedCachingEnabled: false, - }; - logger = new Logger({ - loggerCallback: ( - level: LogLevel, - message: string, - containsPii: boolean - ): void => {}, - piiLoggingEnabled: true, - }); - const browserStorage = new BrowserCacheManager( - TEST_CONFIG.MSAL_CLIENT_ID, - cacheConfig, - browserCrypto, - logger - ); - - const accountEntity1 = { - homeAccountId: "test-home-accountId-1", - localAccountId: "test-local-accountId-1", - username: "user-1@example.com", - environment: "test-environment-1", - realm: "test-tenantId-1", - name: "name-1", - idTokenClaims: {}, - authorityType: "MSSTS", - }; - - const accountEntity2 = { - homeAccountId: "test-home-accountId-2", - localAccountId: "test-local-accountId-2", - username: "user-2@example.com", - environment: "test-environment-2", - realm: "test-tenantId-2", - name: "name-2", - idTokenClaims: {}, - authorityType: "MSSTS", - }; - - const account1: AccountInfo = { - authorityType: "MSSTS", - homeAccountId: accountEntity1.homeAccountId, - localAccountId: accountEntity1.localAccountId, - username: accountEntity1.username, - environment: accountEntity1.environment, - tenantId: accountEntity1.realm, - name: accountEntity1.name, - idTokenClaims: accountEntity1.idTokenClaims, - nativeAccountId: undefined, - }; - - const account2: AccountInfo = { - authorityType: "MSSTS", - homeAccountId: accountEntity2.homeAccountId, - localAccountId: accountEntity2.localAccountId, - username: accountEntity2.username, - environment: accountEntity2.environment, - tenantId: accountEntity2.realm, - name: accountEntity2.name, - idTokenClaims: accountEntity2.idTokenClaims, - nativeAccountId: undefined, - }; - const cacheKey1 = AccountEntity.generateAccountCacheKey(account1); - const cacheKey2 = AccountEntity.generateAccountCacheKey(account2); - - beforeEach(() => { - browserStorage.setItem( - cacheKey1, - JSON.stringify(accountEntity1) - ); - browserStorage.setItem( - cacheKey2, - JSON.stringify(accountEntity2) - ); - browserStorage.addAccountKeyToMap(cacheKey1); - browserStorage.addAccountKeyToMap(cacheKey2); - }); - - afterEach(() => { - browserStorage.clear(); - }); - - it("Matches accounts by username", () => { - expect(browserStorage.getAllAccounts()).toHaveLength(2); - const account1Filter = { username: account1.username }; - const account2Filter = { username: account2.username }; - expect( - browserStorage.getAccountInfoByFilter(account1Filter) - ).toHaveLength(1); - expect( - browserStorage.getAccountInfoByFilter(account1Filter) - ).toContainEqual(account1); - expect( - browserStorage.getAccountInfoByFilter(account2Filter) - ).toHaveLength(1); - expect( - browserStorage.getAccountInfoByFilter(account2Filter) - ).toContainEqual(account2); - }); - - it("Matches accounts by homeAccountId", () => { - expect(browserStorage.getAllAccounts()).toHaveLength(2); - const account1Filter = { - homeAccountId: account1.homeAccountId, - }; - const account2Filter = { - homeAccountId: account2.homeAccountId, - }; - expect( - browserStorage.getAccountInfoByFilter(account1Filter) - ).toHaveLength(1); - expect( - browserStorage.getAccountInfoByFilter(account1Filter) - ).toContainEqual(account1); - expect( - browserStorage.getAccountInfoByFilter(account2Filter) - ).toHaveLength(1); - expect( - browserStorage.getAccountInfoByFilter(account2Filter) - ).toContainEqual(account2); - }); - - it("Matches accounts by localAccountId", () => { - expect(browserStorage.getAllAccounts()).toHaveLength(2); - const account1Filter = { - localAccountId: account1.localAccountId, - }; - const account2Filter = { - localAccountId: account2.localAccountId, - }; - expect( - browserStorage.getAccountInfoByFilter(account1Filter) - ).toHaveLength(1); - expect( - browserStorage.getAccountInfoByFilter(account1Filter) - ).toContainEqual(account1); - expect( - browserStorage.getAccountInfoByFilter(account2Filter) - ).toHaveLength(1); - expect( - browserStorage.getAccountInfoByFilter(account2Filter) - ).toContainEqual(account2); - }); - - it("Matches accounts by tenantId", () => { - expect(browserStorage.getAllAccounts()).toHaveLength(2); - const account1Filter = { tenantId: account1.tenantId }; - const account2Filter = { tenantId: account2.tenantId }; - expect( - browserStorage.getAccountInfoByFilter(account1Filter) - ).toHaveLength(1); - expect( - browserStorage.getAccountInfoByFilter(account1Filter) - ).toContainEqual(account1); - expect( - browserStorage.getAccountInfoByFilter(account2Filter) - ).toHaveLength(1); - expect( - browserStorage.getAccountInfoByFilter(account2Filter) - ).toContainEqual(account2); - }); - - it("Matches accounts by environment", () => { - expect(browserStorage.getAllAccounts()).toHaveLength(2); - const account1Filter = { environment: account1.environment }; - const account2Filter = { environment: account2.environment }; - expect( - browserStorage.getAccountInfoByFilter(account1Filter) - ).toHaveLength(1); - expect( - browserStorage.getAccountInfoByFilter(account1Filter) - ).toContainEqual(account1); - expect( - browserStorage.getAccountInfoByFilter(account2Filter) - ).toHaveLength(1); - expect( - browserStorage.getAccountInfoByFilter(account2Filter) - ).toContainEqual(account2); - }); - - it("Matches accounts by all filters", () => { - expect(browserStorage.getAllAccounts()).toHaveLength(2); - const account1Filter = account1; - const account2Filter = account2; - expect( - browserStorage.getAccountInfoByFilter(account1Filter) - ).toHaveLength(1); - expect( - browserStorage.getAccountInfoByFilter(account1Filter) - ).toContainEqual(account1); - expect( - browserStorage.getAccountInfoByFilter(account2Filter) - ).toHaveLength(1); - expect( - browserStorage.getAccountInfoByFilter(account2Filter) - ).toContainEqual(account2); - }); - }); - - describe("getAccountInfoByHints", () => { - cacheConfig = { - temporaryCacheLocation: BrowserCacheLocation.SessionStorage, - cacheLocation: BrowserCacheLocation.SessionStorage, - storeAuthStateInCookie: false, - secureCookies: false, - cacheMigrationEnabled: false, - claimsBasedCachingEnabled: false, - }; - logger = new Logger({ - loggerCallback: ( - level: LogLevel, - message: string, - containsPii: boolean - ): void => {}, - piiLoggingEnabled: true, - }); - const browserStorage = new BrowserCacheManager( - TEST_CONFIG.MSAL_CLIENT_ID, - cacheConfig, - browserCrypto, - logger - ); - - const accountEntity1 = { - homeAccountId: "test-home-accountId-1", - localAccountId: "test-local-accountId-1", - username: "user-1@example.com", - environment: "test-environment-1", - realm: "test-tenantId-1", - name: "name-1", - idTokenClaims: { - sid: "session-1", - }, - authorityType: "MSSTS", - }; - - const accountEntity2 = { - homeAccountId: "test-home-accountId-2", - localAccountId: "test-local-accountId-2", - username: "user-2@example.com", - environment: "test-environment-2", - realm: "test-tenantId-2", - name: "name-2", - idTokenClaims: { - sid: "session-2", - }, - authorityType: "MSSTS", - }; - - const account1: AccountInfo = { - authorityType: "MSSTS", - homeAccountId: accountEntity1.homeAccountId, - localAccountId: accountEntity1.localAccountId, - username: accountEntity1.username, - environment: accountEntity1.environment, - tenantId: accountEntity1.realm, - name: accountEntity1.name, - idTokenClaims: accountEntity1.idTokenClaims, - nativeAccountId: undefined, - }; - - const account2: AccountInfo = { - authorityType: "MSSTS", - homeAccountId: accountEntity2.homeAccountId, - localAccountId: accountEntity2.localAccountId, - username: accountEntity2.username, - environment: accountEntity2.environment, - tenantId: accountEntity2.realm, - name: accountEntity2.name, - idTokenClaims: accountEntity2.idTokenClaims, - nativeAccountId: undefined, - }; - const cacheKey1 = AccountEntity.generateAccountCacheKey(account1); - const cacheKey2 = AccountEntity.generateAccountCacheKey(account2); - - beforeEach(() => { - browserStorage.setItem( - cacheKey1, - JSON.stringify(accountEntity1) - ); - browserStorage.setItem( - cacheKey2, - JSON.stringify(accountEntity2) - ); - browserStorage.addAccountKeyToMap(cacheKey1); - browserStorage.addAccountKeyToMap(cacheKey2); - }); - - afterEach(() => { - browserStorage.clear(); - }); - - it("Matches accounts by username", () => { - expect(browserStorage.getAllAccounts()).toHaveLength(2); - const account1Filter = { username: account1.username }; - const account2Filter = { username: account2.username }; - expect( - browserStorage.getAccountInfoByFilter(account1Filter) - ).toHaveLength(1); - expect( - browserStorage.getAccountInfoByFilter(account1Filter) - ).toContainEqual(account1); - expect( - browserStorage.getAccountInfoByFilter(account2Filter) - ).toHaveLength(1); - expect( - browserStorage.getAccountInfoByFilter(account2Filter) - ).toContainEqual(account2); - }); - - it("Matches accounts by homeAccountId", () => { - expect(browserStorage.getAllAccounts()).toHaveLength(2); - const account1Filter = { - homeAccountId: account1.homeAccountId, - }; - const account2Filter = { - homeAccountId: account2.homeAccountId, - }; - expect( - browserStorage.getAccountInfoByFilter(account1Filter) - ).toHaveLength(1); - expect( - browserStorage.getAccountInfoByFilter(account1Filter) - ).toContainEqual(account1); - expect( - browserStorage.getAccountInfoByFilter(account2Filter) - ).toHaveLength(1); - expect( - browserStorage.getAccountInfoByFilter(account2Filter) - ).toContainEqual(account2); - }); - - it("Matches accounts by localAccountId", () => { - expect(browserStorage.getAllAccounts()).toHaveLength(2); - const account1Filter = { - localAccountId: account1.localAccountId, - }; - const account2Filter = { - localAccountId: account2.localAccountId, - }; - expect( - browserStorage.getAccountInfoByFilter(account1Filter) - ).toHaveLength(1); - expect( - browserStorage.getAccountInfoByFilter(account1Filter) - ).toContainEqual(account1); - expect( - browserStorage.getAccountInfoByFilter(account2Filter) - ).toHaveLength(1); - expect( - browserStorage.getAccountInfoByFilter(account2Filter) - ).toContainEqual(account2); - }); - - it("Matches accounts by tenantId", () => { - expect(browserStorage.getAllAccounts()).toHaveLength(2); - const account1Filter = { tenantId: account1.tenantId }; - const account2Filter = { tenantId: account2.tenantId }; - expect( - browserStorage.getAccountInfoByFilter(account1Filter) - ).toHaveLength(1); - expect( - browserStorage.getAccountInfoByFilter(account1Filter) - ).toContainEqual(account1); - expect( - browserStorage.getAccountInfoByFilter(account2Filter) - ).toHaveLength(1); - expect( - browserStorage.getAccountInfoByFilter(account2Filter) - ).toContainEqual(account2); - }); - - it("Matches accounts by environment", () => { - expect(browserStorage.getAllAccounts()).toHaveLength(2); - const account1Filter = { environment: account1.environment }; - const account2Filter = { environment: account2.environment }; - expect( - browserStorage.getAccountInfoByFilter(account1Filter) - ).toHaveLength(1); - expect( - browserStorage.getAccountInfoByFilter(account1Filter) - ).toContainEqual(account1); - expect( - browserStorage.getAccountInfoByFilter(account2Filter) - ).toHaveLength(1); - expect( - browserStorage.getAccountInfoByFilter(account2Filter) - ).toContainEqual(account2); - }); - - it("Matches accounts by all filters", () => { - expect(browserStorage.getAllAccounts()).toHaveLength(2); - const account1Filter = account1; - const account2Filter = account2; - expect( - browserStorage.getAccountInfoByFilter(account1Filter) - ).toHaveLength(1); - expect( - browserStorage.getAccountInfoByFilter(account1Filter) - ).toContainEqual(account1); - expect( - browserStorage.getAccountInfoByFilter(account2Filter) - ).toHaveLength(1); - expect( - browserStorage.getAccountInfoByFilter(account2Filter) - ).toContainEqual(account2); - }); - }); - - describe("getAccountInfoByHints", () => { - cacheConfig = { - cacheLocation: BrowserCacheLocation.SessionStorage, - temporaryCacheLocation: BrowserCacheLocation.SessionStorage, - storeAuthStateInCookie: false, - secureCookies: false, - cacheMigrationEnabled: false, - claimsBasedCachingEnabled: false, - }; - logger = new Logger({ - loggerCallback: ( - level: LogLevel, - message: string, - containsPii: boolean - ): void => {}, - piiLoggingEnabled: true, - }); - const browserStorage = new BrowserCacheManager( - TEST_CONFIG.MSAL_CLIENT_ID, - cacheConfig, - browserCrypto, - logger - ); - - const accountEntity1 = { - homeAccountId: "test-home-accountId-1", - localAccountId: "test-local-accountId-1", - username: "user-1@example.com", - environment: "test-environment-1", - realm: "test-tenantId-1", - name: "name-1", - idTokenClaims: { - sid: "session-1", - }, - authorityType: "MSSTS", - }; - - const accountEntity2 = { - homeAccountId: "test-home-accountId-2", - localAccountId: "test-local-accountId-2", - username: "user-2@example.com", - environment: "test-environment-2", - realm: "test-tenantId-2", - name: "name-2", - idTokenClaims: { - sid: "session-2", - }, - authorityType: "MSSTS", - }; - - const account1: AccountInfo = { - authorityType: "MSSTS", - homeAccountId: accountEntity1.homeAccountId, - localAccountId: accountEntity1.localAccountId, - username: accountEntity1.username, - environment: accountEntity1.environment, - tenantId: accountEntity1.realm, - name: accountEntity1.name, - idTokenClaims: accountEntity1.idTokenClaims, - nativeAccountId: undefined, - }; - - const account2: AccountInfo = { - authorityType: "MSSTS", - homeAccountId: accountEntity2.homeAccountId, - localAccountId: accountEntity2.localAccountId, - username: accountEntity2.username, - environment: accountEntity2.environment, - tenantId: accountEntity2.realm, - name: accountEntity2.name, - idTokenClaims: accountEntity2.idTokenClaims, - nativeAccountId: undefined, - }; - const cacheKey1 = AccountEntity.generateAccountCacheKey(account1); - const cacheKey2 = AccountEntity.generateAccountCacheKey(account2); - - beforeEach(() => { - browserStorage.setItem( - cacheKey1, - JSON.stringify(accountEntity1) - ); - browserStorage.setItem( - cacheKey2, - JSON.stringify(accountEntity2) - ); - browserStorage.addAccountKeyToMap(cacheKey1); - browserStorage.addAccountKeyToMap(cacheKey2); - }); - - afterEach(() => { - browserStorage.clear(); - }); - - it("Matches account by loginHint", () => { - expect( - browserStorage.getAccountInfoByHints(account1.username) - ).toEqual(account1); - expect( - browserStorage.getAccountInfoByHints(account2.username) - ).toEqual(account2); - }); - - it("Matches account by sid", () => { - expect( - browserStorage.getAccountInfoByHints( - undefined, - account1.idTokenClaims!.sid - ) - ).toEqual(account1); - expect( - browserStorage.getAccountInfoByHints( - undefined, - account2.idTokenClaims!.sid - ) - ).toEqual(account2); - }); - - it("Throws if multiple accounts match by loginHint", (done) => { - const accountEntity3 = { - homeAccountId: "test-home-accountId-3", - localAccountId: "test-local-accountId-3", - username: accountEntity1.username, // Keep this the same as account 1 - environment: "test-environment-3", - realm: "test-tenantId-3", - name: "name-3", - idTokenClaims: accountEntity1.idTokenClaims, // Keep this the same as account 1 - authorityType: "MSSTS", - }; - - const account3: AccountInfo = { - homeAccountId: accountEntity3.homeAccountId, - localAccountId: accountEntity3.localAccountId, - username: accountEntity3.username, - environment: accountEntity3.environment, - tenantId: accountEntity3.realm, - name: accountEntity3.name, - idTokenClaims: accountEntity3.idTokenClaims, - nativeAccountId: undefined, - }; - - const cacheKey3 = - AccountEntity.generateAccountCacheKey(account3); - browserStorage.setItem( - cacheKey3, - JSON.stringify(accountEntity3) - ); - browserStorage.addAccountKeyToMap(cacheKey3); - - try { - browserStorage.getAccountInfoByHints( - accountEntity3.username - ); - } catch (e) { - expect((e as AuthError).errorCode).toEqual( - ClientAuthErrorMessage.multipleMatchingAccounts.code - ); - expect((e as AuthError).errorMessage).toEqual( - ClientAuthErrorMessage.multipleMatchingAccounts.desc - ); - done(); - } - }); - - it("Throws if multiple accounts match by sid", (done) => { - const accountEntity3 = { - homeAccountId: "test-home-accountId-3", - localAccountId: "test-local-accountId-3", - username: accountEntity1.username, // Keep this the same as account 1 - environment: "test-environment-3", - realm: "test-tenantId-3", - name: "name-3", - idTokenClaims: accountEntity1.idTokenClaims, // Keep this the same as account 1 - authorityType: "MSSTS", - }; - - const account3: AccountInfo = { - homeAccountId: accountEntity3.homeAccountId, - localAccountId: accountEntity3.localAccountId, - username: accountEntity3.username, - environment: accountEntity3.environment, - tenantId: accountEntity3.realm, - name: accountEntity3.name, - idTokenClaims: accountEntity3.idTokenClaims, - nativeAccountId: undefined, - }; - - const cacheKey3 = - AccountEntity.generateAccountCacheKey(account3); - browserStorage.setItem( - cacheKey3, - JSON.stringify(accountEntity3) - ); - browserStorage.addAccountKeyToMap(cacheKey3); - - try { - browserStorage.getAccountInfoByHints( - undefined, - accountEntity3.idTokenClaims!.sid - ); - } catch (e) { - expect((e as AuthError).errorCode).toEqual( - ClientAuthErrorMessage.multipleMatchingAccounts.code - ); - expect((e as AuthError).errorMessage).toEqual( - ClientAuthErrorMessage.multipleMatchingAccounts.desc - ); - done(); - } - }); - - it("Returns null if no accounts match", () => { - expect( - browserStorage.getAccountInfoByHints("fakeUser@contoso.com") - ).toBe(null); - expect( - browserStorage.getAccountInfoByHints(undefined, "fake-sid") - ).toBe(null); - }); - }); }); }); diff --git a/lib/msal-browser/test/cache/TokenCache.spec.ts b/lib/msal-browser/test/cache/TokenCache.spec.ts index 85371eae55..b5c01f9a01 100644 --- a/lib/msal-browser/test/cache/TokenCache.spec.ts +++ b/lib/msal-browser/test/cache/TokenCache.spec.ts @@ -245,7 +245,6 @@ describe("TokenCache tests", () => { tenantId: TEST_CONFIG.MSAL_TENANT_ID, username: "AbeLi@microsoft.com", localAccountId: TEST_DATA_CLIENT_INFO.TEST_LOCAL_ACCOUNT_ID, - idTokenClaims: testIdTokenClaims, name: testIdTokenClaims.name, nativeAccountId: undefined, }; diff --git a/lib/msal-browser/test/interaction_client/NativeInteractionClient.spec.ts b/lib/msal-browser/test/interaction_client/NativeInteractionClient.spec.ts index e236ff7558..1ad8a4cef5 100644 --- a/lib/msal-browser/test/interaction_client/NativeInteractionClient.spec.ts +++ b/lib/msal-browser/test/interaction_client/NativeInteractionClient.spec.ts @@ -61,7 +61,10 @@ testAccountEntity.name = ID_TOKEN_CLAIMS.name; testAccountEntity.authorityType = "MSSTS"; testAccountEntity.nativeAccountId = "nativeAccountId"; -const testAccountInfo: AccountInfo = testAccountEntity.getAccountInfo(); +const testAccountInfo: AccountInfo = { + ...testAccountEntity.getAccountInfo(), + idTokenClaims: ID_TOKEN_CLAIMS, +}; const testIdToken: IdTokenEntity = new IdTokenEntity(); testIdToken.homeAccountId = `${ID_TOKEN_CLAIMS.oid}.${ID_TOKEN_CLAIMS.tid}`; @@ -178,7 +181,7 @@ describe("NativeInteractionClient Tests", () => { }; sinon - .stub(CacheManager.prototype, "getAccountInfoFilteredBy") + .stub(CacheManager.prototype, "getBaseAccountInfo") .returns(testAccountInfo); sinon @@ -197,9 +200,7 @@ describe("NativeInteractionClient Tests", () => { expect(response.authority).toEqual(TEST_CONFIG.validAuthority); expect(response.scopes).toEqual(TEST_CONFIG.DEFAULT_SCOPES); expect(response.correlationId).toEqual(RANDOM_TEST_GUID); - expect(response.account).toEqual( - testAccountEntity.getAccountInfo() - ); + expect(response.account).toEqual(testAccountInfo); expect(response.tokenType).toEqual(AuthenticationScheme.BEARER); }); }); diff --git a/lib/msal-browser/test/interaction_client/RedirectClient.spec.ts b/lib/msal-browser/test/interaction_client/RedirectClient.spec.ts index ca71bf4d69..ac340d44f8 100644 --- a/lib/msal-browser/test/interaction_client/RedirectClient.spec.ts +++ b/lib/msal-browser/test/interaction_client/RedirectClient.spec.ts @@ -48,6 +48,8 @@ import { NetworkManager, createClientConfigurationError, ClientConfigurationErrorCodes, + IdTokenEntity, + CredentialType, } from "@azure/msal-common"; import { BrowserUtils } from "../../src/utils/BrowserUtils"; import { @@ -3634,26 +3636,16 @@ describe("RedirectClient", () => { }); it("clears active account entry from the cache", async () => { - const testIdTokenClaims: TokenClaims = { - ver: "2.0", - iss: "https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0", - sub: "AAAAAAAAAAAAAAAAAAAAAIkzqFVrSaSaFHy782bbtaQ", - name: "Abe Lincoln", - preferred_username: "AbeLi@microsoft.com", - oid: "00000000-0000-0000-66f3-3332eca7ea81", - tid: "3338040d-6c67-4c5b-b112-36a304b66dad", - nonce: "123523", - }; - const testAccountInfo: AccountInfo = { authorityType: "MSSTS", - homeAccountId: TEST_DATA_CLIENT_INFO.TEST_HOME_ACCOUNT_ID, + homeAccountId: `${ID_TOKEN_CLAIMS.oid}.${ID_TOKEN_CLAIMS.tid}`, localAccountId: TEST_DATA_CLIENT_INFO.TEST_UID, environment: "login.windows.net", - tenantId: testIdTokenClaims.tid || "", - username: testIdTokenClaims.preferred_username || "", - idTokenClaims: testIdTokenClaims, - name: testIdTokenClaims.name || "", + tenantId: ID_TOKEN_CLAIMS.tid, + username: ID_TOKEN_CLAIMS.preferred_username, + idToken: TEST_TOKENS.IDTOKEN_V2, + idTokenClaims: ID_TOKEN_CLAIMS, + name: ID_TOKEN_CLAIMS.name, nativeAccountId: undefined, }; @@ -3667,7 +3659,14 @@ describe("RedirectClient", () => { testAccount.authorityType = "MSSTS"; testAccount.clientInfo = TEST_DATA_CLIENT_INFO.TEST_CLIENT_INFO_B64ENCODED; - testAccount.idTokenClaims = testIdTokenClaims; + + const testIdToken: IdTokenEntity = new IdTokenEntity(); + testIdToken.homeAccountId = `${ID_TOKEN_CLAIMS.oid}.${ID_TOKEN_CLAIMS.tid}`; + testIdToken.clientId = TEST_CONFIG.MSAL_CLIENT_ID; + testIdToken.environment = testAccount.environment; + testIdToken.realm = ID_TOKEN_CLAIMS.tid; + testIdToken.secret = TEST_TOKENS.IDTOKEN_V2; + testIdToken.credentialType = CredentialType.ID_TOKEN; const validatedLogoutRequest: CommonEndSessionRequest = { correlationId: RANDOM_TEST_GUID, @@ -3687,6 +3686,8 @@ describe("RedirectClient", () => { ); browserStorage.setAccount(testAccount); + browserStorage.setIdTokenCredential(testIdToken); + pca.setActiveAccount(testAccountInfo); expect(pca.getActiveAccount()).toStrictEqual(testAccountInfo); diff --git a/lib/msal-browser/test/interaction_client/SilentCacheClient.spec.ts b/lib/msal-browser/test/interaction_client/SilentCacheClient.spec.ts index 366944f659..491f1d98d9 100644 --- a/lib/msal-browser/test/interaction_client/SilentCacheClient.spec.ts +++ b/lib/msal-browser/test/interaction_client/SilentCacheClient.spec.ts @@ -158,8 +158,14 @@ describe("SilentCacheClient", () => { it("logout clears browser cache", async () => { // @ts-ignore pca.browserStorage.setAccount(testAccountEntity); + // @ts-ignore + pca.browserStorage.setIdTokenCredential(testIdToken); + pca.setActiveAccount(testAccount); - expect(pca.getActiveAccount()).toEqual(testAccount); + expect(pca.getActiveAccount()).toEqual({ + ...testAccount, + idToken: TEST_TOKENS.IDTOKEN_V2, + }); silentCacheClient.logout({ account: testAccount }); //@ts-ignore expect(pca.getActiveAccount()).toEqual(null); diff --git a/lib/msal-browser/test/utils/StringConstants.ts b/lib/msal-browser/test/utils/StringConstants.ts index 3d9911a2b5..30e8e20264 100644 --- a/lib/msal-browser/test/utils/StringConstants.ts +++ b/lib/msal-browser/test/utils/StringConstants.ts @@ -88,7 +88,7 @@ export const TEST_TOKENS = { REFRESH_TOKEN: "thisIsARefreshT0ken", // [SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Fake credential used for testing purposes")] ID_TOKEN_V2_WITH_LOGIN_HINT: - "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJBQUFBQUFBQUFBQUFBQUFBQUFBQUFJa3pxRlZyU2FTYUZIeTc4MmJidGFRIiwidmVyIjoiMi4wIiwiYWlvIjoiRGYyVVZYTDFpeCFsTUNXTVNPSkJjRmF0emNHZnZGR2hqS3Y4cTVnMHg3MzJkUjVNQjVCaXN2R1FPN1lXQnlqZDhpUURMcSFlR2JJRGFreXA1bW5PcmNkcUhlWVNubHRlcFFtUnA2QUlaOGpZIiwiaXNzIjoiaHR0cHM6Ly9sb2dpbi5taWNyb3NvZnRvbmxpbmUuY29tLzkxODgwNDBkLTZjNjctNGM1Yi1iMTEyLTM2YTMwNGI2NmRhZC92Mi4wIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiQWJlTGlAbWljcm9zb2Z0LmNvbSIsIm9pZCI6IjAwMDAwMDAwLTAwMDAtMDAwMC02NmYzLTMzMzJlY2E3ZWE4MSIsIm5vbmNlIjoiMTIzNTIzIiwidGlkIjoiMzMzODA0MGQtNmM2Ny00YzViLWIxMTItMzZhMzA0YjY2ZGFkIiwiYXVkIjoiNmNiMDQwMTgtYTNmNS00NmE3LWI5OTUtOTQwYzc4ZjVhZWYzIiwibG9naW5faGludCI6InRlc3RMb2dpbkhpbnQiLCJuYmYiOiIxNTM2MzYxNDExIiwibmFtZSI6IkFiZSBMaW5jb2xuIiwiZXhwIjoiMTUzNjM2MTQxMSIsImlhdCI6IjE1MzYzNjE0MTEifQ.7u4BULsOrJqETf-mY1YxtBow9AooKngwaPM_oQihbPo", + "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImQyOWRkNWZkODFjOThjYTY0NjVmOTdlMTA4MDEwNmYyIn0.eyJzdWIiOiJBQUFBQUFBQUFBQUFBQUFBQUFBQUFJa3pxRlZyU2FTYUZIeTc4MmJidGFRIiwidmVyIjoiMi4wIiwiYWlvIjoiRGYyVVZYTDFpeCFsTUNXTVNPSkJjRmF0emNHZnZGR2hqS3Y4cTVnMHg3MzJkUjVNQjVCaXN2R1FPN1lXQnlqZDhpUURMcSFlR2JJRGFreXA1bW5PcmNkcUhlWVNubHRlcFFtUnA2QUlaOGpZIiwiaXNzIjoiaHR0cHM6Ly9sb2dpbi5taWNyb3NvZnRvbmxpbmUuY29tLzkxODgwNDBkLTZjNjctNGM1Yi1iMTEyLTM2YTMwNGI2NmRhZC92Mi4wIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiVW5pcXVlIFVzZXJuYW1lIiwib2lkIjoiMDAwMDAwMDAtMDAwMC0wMDAwLTY2ZjMtMzMzMmVjYTdlYTgxIiwibm9uY2UiOiIxMjM1MjMiLCJ0aWQiOiIzMzM4MDQwZC02YzY3LTRjNWItYjExMi0zNmEzMDRiNjZkYWQiLCJhdWQiOiI2Y2IwNDAxOC1hM2Y1LTQ2YTctYjk5NS05NDBjNzhmNWFlZjMiLCJsb2dpbl9oaW50IjoidGVzdExvZ2luSGludCIsIm5iZiI6IjE1MzYzNjE0MTEiLCJuYW1lIjoiQWJlIExpbmNvbG4iLCJleHAiOiIxNTM2MzYxNDExIiwiaWF0IjoiMTUzNjM2MTQxMSJ9.GTmMQI1ZrGcCG4UHENiMrgW-0oXBAFHumroKZbTrzLd74e2PJnOnQCqqmuRtyAFfplb8gWgQWODK-F5rfYTokN-bjvk4XTvcwrMKqNfEYoCBBl-gTWzbz_1qIdeRIhAX956LwPGUpZWM0qMeDiI3UKaI9cCOC5keNNHb6N8TxAylxVn-CmoYv8QqFyXrHWvp6zAHeJc-SQsNjkacIFZrSqVfbzuD50ftLOnu9t7S-FmCWuPAgncjs-GtpDbJ9LOt-uRYgHwp8Sp3i-cUi3m0FxNsF4W_EUWnbfgu3gD7b5tscfgBc0gGXOO-cGVk5u7lkoHFFGKLr4oqxHhFV43X_w", // [SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Fake credential used for testing purposes")] ID_TOKEN_V2_WITH_UPN: "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJBQUFBQUFBQUFBQUFBQUFBQUFBQUFJa3pxRlZyU2FTYUZIeTc4MmJidGFRIiwidmVyIjoiMi4wIiwiYWlvIjoiRGYyVVZYTDFpeCFsTUNXTVNPSkJjRmF0emNHZnZGR2hqS3Y4cTVnMHg3MzJkUjVNQjVCaXN2R1FPN1lXQnlqZDhpUURMcSFlR2JJRGFreXA1bW5PcmNkcUhlWVNubHRlcFFtUnA2QUlaOGpZIiwiaXNzIjoiaHR0cHM6Ly9sb2dpbi5taWNyb3NvZnRvbmxpbmUuY29tLzkxODgwNDBkLTZjNjctNGM1Yi1iMTEyLTM2YTMwNGI2NmRhZC92Mi4wIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiQWJlTGlAbWljcm9zb2Z0LmNvbSIsIm9pZCI6IjAwMDAwMDAwLTAwMDAtMDAwMC02NmYzLTMzMzJlY2E3ZWE4MSIsIm5vbmNlIjoiMTIzNTIzIiwidGlkIjoiMzMzODA0MGQtNmM2Ny00YzViLWIxMTItMzZhMzA0YjY2ZGFkIiwiYXVkIjoiNmNiMDQwMTgtYTNmNS00NmE3LWI5OTUtOTQwYzc4ZjVhZWYzIiwibmJmIjoiMTUzNjM2MTQxMSIsIm5hbWUiOiJBYmUgTGluY29sbiIsImV4cCI6IjE1MzYzNjE0MTEiLCJpYXQiOiIxNTM2MzYxNDExIiwidXBuIjoidGVzdFVwbiJ9._8gXqY4qn4A8v_et5fESz-82BU4ad2F_v2msO9CcvIA", diff --git a/lib/msal-common/src/account/AccountInfo.ts b/lib/msal-common/src/account/AccountInfo.ts index 3471373ae9..cd459d9489 100644 --- a/lib/msal-common/src/account/AccountInfo.ts +++ b/lib/msal-common/src/account/AccountInfo.ts @@ -41,3 +41,10 @@ export type ActiveAccountFilters = { homeAccountId: string; localAccountId: string; }; + +export function updateAccountInfo( + accountInfo: AccountInfo, + updatedAttributes: Partial +): AccountInfo { + return { ...accountInfo, ...updatedAttributes }; +} diff --git a/lib/msal-common/src/cache/CacheManager.ts b/lib/msal-common/src/cache/CacheManager.ts index eda506119b..5c5a059a40 100644 --- a/lib/msal-common/src/cache/CacheManager.ts +++ b/lib/msal-common/src/cache/CacheManager.ts @@ -47,6 +47,7 @@ import { getHardcodedAliasesForCanonicalAuthority, } from "../authority/AuthorityMetadata"; import { StaticAuthorityOptions } from "../authority/AuthorityOptions"; +import { TokenClaims } from "../account/TokenClaims"; /** * Interface class which implement cache storage functions used by MSAL to perform validity checks, and store tokens. @@ -238,49 +239,93 @@ export abstract class CacheManager implements ICacheManager { * @returns Array of AccountInfo objects in cache */ getAllAccounts(accountFilter?: AccountFilter): AccountInfo[] { - return this.getAccountsFilteredBy(accountFilter || {}) - .map((accountEntity) => { - return this.getAccountInfoFromEntity(accountEntity); - }) - .filter((accountInfo) => { - return accountInfo.idTokenClaims; - }); + const validAccounts: AccountInfo[] = []; + this.getAccountsFilteredBy(accountFilter || {}).forEach( + (accountEntity: AccountEntity) => { + const accountInfo = this.getAccountInfoFromEntity( + accountEntity, + accountFilter + ); + if (accountInfo) { + validAccounts.push(accountInfo); + } + } + ); + return validAccounts; } /** * Gets accountInfo object based on provided filters */ getAccountInfoFilteredBy(accountFilter: AccountFilter): AccountInfo | null { - const allAccounts = this.getAccountsFilteredBy(accountFilter); + const allAccounts = this.getAllAccounts(accountFilter); if (allAccounts.length > 0) { - const validAccounts = allAccounts - .map((accountEntity) => { - return this.getAccountInfoFromEntity(accountEntity); - }) - .filter((accountInfo) => { - return accountInfo.idTokenClaims; - }); - return validAccounts[0]; + return allAccounts[0]; + } else { + return null; + } + } + + /** + * Returns a single matching + * @param accountFilter + * @returns + */ + getBaseAccountInfo(accountFilter: AccountFilter): AccountInfo | null { + const accountEntities = this.getAccountsFilteredBy(accountFilter); + if (accountEntities.length > 0) { + return accountEntities[0].getAccountInfo(); } else { return null; } } private getAccountInfoFromEntity( - accountEntity: AccountEntity - ): AccountInfo { + accountEntity: AccountEntity, + accountFilter?: AccountFilter + ): AccountInfo | null { const accountInfo = accountEntity.getAccountInfo(); const idToken = this.getIdToken(accountInfo); if (idToken) { - accountInfo.idToken = idToken.secret; - accountInfo.idTokenClaims = extractTokenClaims( + const idTokenClaims = extractTokenClaims( idToken.secret, this.cryptoImpl.base64Decode ); + + if ( + this.idTokenClaimsMatchAccountFilter( + idTokenClaims, + accountFilter + ) + ) { + accountInfo.idToken = idToken.secret; + accountInfo.idTokenClaims = idTokenClaims; + return accountInfo; + } } - return accountInfo; + return null; } + private idTokenClaimsMatchAccountFilter( + idTokenClaims: TokenClaims, + accountFilter?: AccountFilter + ): boolean { + if (accountFilter) { + if ( + !!accountFilter.loginHint && + !this.matchLoginHint(idTokenClaims, accountFilter.loginHint) + ) { + return false; + } + if ( + !!accountFilter.sid && + !this.matchSid(idTokenClaims, accountFilter.sid) + ) { + return false; + } + } + return true; + } /** * saves a cache record * @param cacheRecord @@ -296,8 +341,6 @@ export abstract class CacheManager implements ICacheManager { } if (!!cacheRecord.account) { - // Remove ID token claims before saving account entity - cacheRecord.account.idTokenClaims = undefined; this.setAccount(cacheRecord.account); } @@ -373,7 +416,6 @@ export abstract class CacheManager implements ICacheManager { getAccountsFilteredBy(accountFilter: AccountFilter): AccountEntity[] { const allAccountKeys = this.getAccountKeys(); const matchingAccounts: AccountEntity[] = []; - allAccountKeys.forEach((cacheKey) => { if ( !this.isAccountKey( @@ -445,13 +487,6 @@ export abstract class CacheManager implements ICacheManager { return; } - if ( - !!accountFilter.loginHint && - !this.matchLoginHint(entity, accountFilter.loginHint) - ) { - return; - } - if ( !!accountFilter.authorityType && !this.matchAuthorityType(entity, accountFilter.authorityType) @@ -953,7 +988,6 @@ export abstract class CacheManager implements ICacheManager { ) { return; } - const idToken = this.getIdTokenCredential(key); if (idToken && this.credentialMatchesFilter(idToken, filter)) { idTokens.push(idToken); @@ -1487,22 +1521,35 @@ export abstract class CacheManager implements ICacheManager { * @param loginHint * @returns */ - private matchLoginHint(entity: AccountEntity, loginHint: string): boolean { - if (entity.idTokenClaims?.login_hint === loginHint) { + private matchLoginHint( + idTokenClaims: TokenClaims, + loginHint: string + ): boolean { + if (idTokenClaims?.login_hint === loginHint) { return true; } - if (entity.username === loginHint) { + if (idTokenClaims.preferred_username === loginHint) { return true; } - if (entity.idTokenClaims?.upn === loginHint) { + if (idTokenClaims?.upn === loginHint) { return true; } return false; } + /** + * Helper to match sid + * @param idTokenClaims + * @param sid + * @returns true if the sid claim is present and matches the filter + */ + private matchSid(idTokenClaims: TokenClaims, sid: string): boolean { + return !!(idTokenClaims?.sid && idTokenClaims.sid === sid); + } + private matchAuthorityType( entity: AccountEntity, authorityType: string diff --git a/lib/msal-common/src/cache/utils/CacheTypes.ts b/lib/msal-common/src/cache/utils/CacheTypes.ts index ca6f41f5c8..c09b01dea8 100644 --- a/lib/msal-common/src/cache/utils/CacheTypes.ts +++ b/lib/msal-common/src/cache/utils/CacheTypes.ts @@ -59,6 +59,7 @@ export type AccountFilter = Omit< > & { realm?: string; loginHint?: string; + sid?: string; }; /** diff --git a/lib/msal-common/src/index.ts b/lib/msal-common/src/index.ts index fb53df6c69..aaef203951 100644 --- a/lib/msal-common/src/index.ts +++ b/lib/msal-common/src/index.ts @@ -28,7 +28,11 @@ export { } from "./config/AppTokenProvider"; export { ClientConfiguration } from "./config/ClientConfiguration"; // Account -export { AccountInfo, ActiveAccountFilters } from "./account/AccountInfo"; +export { + AccountInfo, + ActiveAccountFilters, + updateAccountInfo, +} from "./account/AccountInfo"; export * as AuthToken from "./account/AuthToken"; export { TokenClaims } from "./account/TokenClaims"; export { TokenClaims as IdTokenClaims } from "./account/TokenClaims"; diff --git a/lib/msal-common/test/cache/CacheManager.spec.ts b/lib/msal-common/test/cache/CacheManager.spec.ts index 59d5c31ee9..3576cbeebb 100644 --- a/lib/msal-common/test/cache/CacheManager.spec.ts +++ b/lib/msal-common/test/cache/CacheManager.spec.ts @@ -396,51 +396,6 @@ describe("CacheManager.ts test cases", () => { mockCache.cacheManager.getAccountsFilteredBy(wrongFilter); expect(Object.keys(accounts).length).toEqual(0); }); - - it("loginHint filter matching login_hint ID token claim", () => { - // filter by loginHint - const successFilter: AccountFilter = { - loginHint: "testLoginHint", - }; - let accounts = - mockCache.cacheManager.getAccountsFilteredBy(successFilter); - expect(accounts.length).toEqual(1); - - const wrongFilter: AccountFilter = { loginHint: "WrongHint" }; - accounts = - mockCache.cacheManager.getAccountsFilteredBy(wrongFilter); - expect(accounts.length).toBe(0); - }); - - it("loginHint filter matching username", () => { - // filter by loginHint - const successFilter: AccountFilter = { - loginHint: "Jane Doe", - }; - let accounts = - mockCache.cacheManager.getAccountsFilteredBy(successFilter); - expect(accounts.length).toEqual(1); - - const wrongFilter: AccountFilter = { loginHint: "WrongHint" }; - accounts = - mockCache.cacheManager.getAccountsFilteredBy(wrongFilter); - expect(accounts.length).toBe(0); - }); - - it("loginHint filter matching upn ID token claim", () => { - // filter by loginHint - const successFilter: AccountFilter = { - loginHint: "testUpn", - }; - let accounts = - mockCache.cacheManager.getAccountsFilteredBy(successFilter); - expect(accounts.length).toEqual(1); - - const wrongFilter: AccountFilter = { loginHint: "WrongHint" }; - accounts = - mockCache.cacheManager.getAccountsFilteredBy(wrongFilter); - expect(accounts.length).toBe(0); - }); }); describe("isCredentialKey", () => { diff --git a/lib/msal-node/src/cache/NodeStorage.ts b/lib/msal-node/src/cache/NodeStorage.ts index 074918f35e..6dc7c2bffc 100644 --- a/lib/msal-node/src/cache/NodeStorage.ts +++ b/lib/msal-node/src/cache/NodeStorage.ts @@ -232,6 +232,8 @@ export class NodeStorage extends CacheManager { */ setAccount(account: AccountEntity): void { const accountKey = account.generateAccountKey(); + // Remove ID token claims before saving account entity + account.idTokenClaims = undefined; this.setItem(accountKey, account); } diff --git a/lib/msal-node/test/cache/serializer/Serializer.spec.ts b/lib/msal-node/test/cache/serializer/Serializer.spec.ts index 6022ff69fe..ef66b97eb3 100644 --- a/lib/msal-node/test/cache/serializer/Serializer.spec.ts +++ b/lib/msal-node/test/cache/serializer/Serializer.spec.ts @@ -34,7 +34,6 @@ describe("Serializer test cases", () => { // serialize the mock IdToken and Test equivalency with the cache.json provided const serializedIdT = Serializer.serializeIdTokens(idt); - console.log(MockCache.idTKey); expect(serializedIdT[MockCache.idTKey]).toMatchObject( jsonCache.IdToken[MockCache.idTKey] ); diff --git a/lib/msal-node/test/client/PublicClientApplication.spec.ts b/lib/msal-node/test/client/PublicClientApplication.spec.ts index 90d65cd8c8..7282bf43e5 100644 --- a/lib/msal-node/test/client/PublicClientApplication.spec.ts +++ b/lib/msal-node/test/client/PublicClientApplication.spec.ts @@ -22,6 +22,7 @@ import { InteractionRequiredAuthError, AccountEntity, AuthToken, + IdTokenEntity, } from "@azure/msal-common"; import { Configuration, @@ -71,7 +72,7 @@ describe("PublicClientApplication", () => { let appConfig: Configuration = { auth: { clientId: TEST_CONSTANTS.CLIENT_ID, - authority: TEST_CONSTANTS.AUTHORITY, + authority: TEST_CONSTANTS.DEFAULT_AUTHORITY, }, }; @@ -745,6 +746,17 @@ describe("PublicClientApplication", () => { // @ts-ignore authApp.storage.setAccount(accountEntity); + const idTokenEntity = IdTokenEntity.createIdTokenEntity( + mockAccountInfo.homeAccountId, + mockAccountInfo.environment, + mockAuthenticationResult.idToken, + TEST_CONSTANTS.CLIENT_ID, + ID_TOKEN_CLAIMS.tid + ); + + // @ts-ignore + authApp.storage.setIdTokenCredential(idTokenEntity); + const accountsBefore = await authApp.getAllAccounts(); expect(accountsBefore.length).toBe(1); @@ -820,6 +832,20 @@ describe("PublicClientApplication", () => { // @ts-ignore authApp.storage.setAccount(accountEntity); + // @ts-ignore + authApp.storage.setAccount(accountEntity); + + const idTokenEntity = IdTokenEntity.createIdTokenEntity( + mockAccountInfo.homeAccountId, + mockAccountInfo.environment, + mockAuthenticationResult.idToken, + TEST_CONSTANTS.CLIENT_ID, + ID_TOKEN_CLAIMS.tid + ); + + // @ts-ignore + authApp.storage.setIdTokenCredential(idTokenEntity); + const accounts = await authApp.getAllAccounts(); expect(accounts).toStrictEqual([mockAccountInfo]); }); diff --git a/lib/msal-node/test/utils/TestConstants.ts b/lib/msal-node/test/utils/TestConstants.ts index 8d92acaec1..ad618e340f 100644 --- a/lib/msal-node/test/utils/TestConstants.ts +++ b/lib/msal-node/test/utils/TestConstants.ts @@ -230,6 +230,7 @@ export const mockAccountInfo: AccountInfo = { tenantId: ID_TOKEN_CLAIMS.tid, username: ID_TOKEN_CLAIMS.preferred_username, idTokenClaims: ID_TOKEN_CLAIMS, + idToken: TEST_CONSTANTS.ID_TOKEN, name: ID_TOKEN_CLAIMS.name, nativeAccountId: undefined, }; From c7ffc414acddbd1be6b9206fdfe23885fa000222 Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Tue, 10 Oct 2023 19:54:15 -0700 Subject: [PATCH 21/91] Fix cloud discovery metadata static config processing --- lib/msal-common/src/authority/Authority.ts | 16 +++++++++++- .../src/authority/AuthorityMetadata.ts | 25 +++++++++++-------- .../src/authority/AuthorityOptions.ts | 7 +++--- lib/msal-common/src/cache/CacheManager.ts | 1 + 4 files changed, 33 insertions(+), 16 deletions(-) diff --git a/lib/msal-common/src/authority/Authority.ts b/lib/msal-common/src/authority/Authority.ts index 5d0d3bd0ac..b1282e7eef 100644 --- a/lib/msal-common/src/authority/Authority.ts +++ b/lib/msal-common/src/authority/Authority.ts @@ -1263,12 +1263,26 @@ export function formatAuthorityUri(authorityUri: string): string { export function buildStaticAuthorityOptions( authOptions: Partial ): StaticAuthorityOptions { + const rawCloudDiscoveryMetadata = authOptions.cloudDiscoveryMetadata; + let cloudDiscoveryMetadata: CloudDiscoveryMetadata[] | undefined = + undefined; + if (rawCloudDiscoveryMetadata) { + try { + cloudDiscoveryMetadata = JSON.parse( + rawCloudDiscoveryMetadata + ).metadata; + } catch (e) { + throw createClientConfigurationError( + ClientConfigurationErrorCodes.invalidCloudDiscoveryMetadata + ); + } + } return { canonicalAuthority: authOptions.authority ? formatAuthorityUri(authOptions.authority) : undefined, knownAuthorities: authOptions.knownAuthorities, - cloudDiscoveryMetadata: authOptions.cloudDiscoveryMetadata, + cloudDiscoveryMetadata: cloudDiscoveryMetadata, authorityMetadata: authOptions.authorityMetadata, }; } diff --git a/lib/msal-common/src/authority/AuthorityMetadata.ts b/lib/msal-common/src/authority/AuthorityMetadata.ts index f2ef2de19a..79fb31beb3 100644 --- a/lib/msal-common/src/authority/AuthorityMetadata.ts +++ b/lib/msal-common/src/authority/AuthorityMetadata.ts @@ -983,20 +983,23 @@ export function getHardcodedAliasesForCanonicalAuthority( * @returns */ export function getAliasesFromConfigMetadata( - rawCloudDiscoveryMetadata?: string + canonicalAuthority?: string, + cloudDiscoveryMetadata?: CloudDiscoveryMetadata[] ): string[] | null { - if (rawCloudDiscoveryMetadata) { - try { - const metadata = JSON.parse(rawCloudDiscoveryMetadata); - if (metadata) { - return metadata.aliases; - } - } catch (e) { - throw createClientConfigurationError( - ClientConfigurationErrorCodes.invalidCloudDiscoveryMetadata - ); + if (canonicalAuthority && cloudDiscoveryMetadata) { + const canonicalAuthorityUrlComponents = new UrlString( + canonicalAuthority + ).getUrlComponents(); + const metadata = getCloudDiscoveryMetadataFromNetworkResponse( + cloudDiscoveryMetadata, + canonicalAuthorityUrlComponents.HostNameAndPort + ); + + if (metadata) { + return metadata.aliases; } } + return null; } diff --git a/lib/msal-common/src/authority/AuthorityOptions.ts b/lib/msal-common/src/authority/AuthorityOptions.ts index 5c97d1d1d7..acd24ab116 100644 --- a/lib/msal-common/src/authority/AuthorityOptions.ts +++ b/lib/msal-common/src/authority/AuthorityOptions.ts @@ -6,6 +6,7 @@ import { ProtocolMode } from "./ProtocolMode"; import { OIDCOptions } from "./OIDCOptions"; import { AzureRegionConfiguration } from "./AzureRegionConfiguration"; +import { CloudDiscoveryMetadata } from "./CloudDiscoveryMetadata"; export type AuthorityOptions = { protocolMode: ProtocolMode; @@ -19,12 +20,10 @@ export type AuthorityOptions = { }; export type StaticAuthorityOptions = Partial< - Pick< - AuthorityOptions, - "knownAuthorities" | "cloudDiscoveryMetadata" | "authorityMetadata" - > + Pick > & { canonicalAuthority?: string; + cloudDiscoveryMetadata?: CloudDiscoveryMetadata[]; }; export const AzureCloudInstance = { diff --git a/lib/msal-common/src/cache/CacheManager.ts b/lib/msal-common/src/cache/CacheManager.ts index 5c5a059a40..57a6cfe65f 100644 --- a/lib/msal-common/src/cache/CacheManager.ts +++ b/lib/msal-common/src/cache/CacheManager.ts @@ -1425,6 +1425,7 @@ export abstract class CacheManager implements ICacheManager { if (this.staticAuthorityOptions) { const staticAliases = getAliasesFromConfigMetadata( + this.staticAuthorityOptions.canonicalAuthority, this.staticAuthorityOptions.cloudDiscoveryMetadata ) || getHardcodedAliasesForCanonicalAuthority( From fff619817a8fde538ca6ecdda0f9892d1ae03fc5 Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Wed, 11 Oct 2023 12:36:40 -0700 Subject: [PATCH 22/91] Make sure id token claims are excluded from accounts to be cached but included in accounts returned in auth result --- .../src/cache/BrowserCacheManager.ts | 4 +-- .../NativeInteractionClient.ts | 11 +++++++- .../src/authority/AuthorityMetadata.ts | 4 --- .../src/cache/entities/AccountEntity.ts | 2 -- .../src/response/ResponseHandler.ts | 17 +++++++++--- .../test/cache/entities/AccountEntity.spec.ts | 26 +++++++++++-------- lib/msal-node/src/cache/NodeStorage.ts | 7 +++-- 7 files changed, 46 insertions(+), 25 deletions(-) diff --git a/lib/msal-browser/src/cache/BrowserCacheManager.ts b/lib/msal-browser/src/cache/BrowserCacheManager.ts index 9ab0294b20..584a09f523 100644 --- a/lib/msal-browser/src/cache/BrowserCacheManager.ts +++ b/lib/msal-browser/src/cache/BrowserCacheManager.ts @@ -435,9 +435,9 @@ export class BrowserCacheManager extends CacheManager { setAccount(account: AccountEntity): void { this.logger.trace("BrowserCacheManager.setAccount called"); // Remove ID token claims before saving account entity - account.idTokenClaims = undefined; + const baseAccount = { ...account, idTokenClaims: undefined }; const key = account.generateAccountKey(); - this.setItem(key, JSON.stringify(account)); + this.setItem(key, JSON.stringify(baseAccount)); this.addAccountKeyToMap(key); } diff --git a/lib/msal-browser/src/interaction_client/NativeInteractionClient.ts b/lib/msal-browser/src/interaction_client/NativeInteractionClient.ts index 5808336539..cd31e35a6d 100644 --- a/lib/msal-browser/src/interaction_client/NativeInteractionClient.ts +++ b/lib/msal-browser/src/interaction_client/NativeInteractionClient.ts @@ -582,6 +582,15 @@ export class NativeInteractionClient extends BaseInteractionClient { idTokenClaims.tid || Constants.EMPTY_STRING; + const fullAccountEntity: AccountEntity = idTokenClaims + ? Object.assign(new AccountEntity(), { + ...accountEntity, + idTokenClaims: idTokenClaims, + }) + : accountEntity; + + const accountInfo = fullAccountEntity.getAccountInfo(); + // generate PoP token as needed const responseAccessToken = await this.generatePopAccessToken( response, @@ -597,7 +606,7 @@ export class NativeInteractionClient extends BaseInteractionClient { uniqueId: uid, tenantId: tid, scopes: responseScopes.asArray(), - account: accountEntity.getAccountInfo(), + account: accountInfo, idToken: response.id_token, idTokenClaims: idTokenClaims, accessToken: responseAccessToken, diff --git a/lib/msal-common/src/authority/AuthorityMetadata.ts b/lib/msal-common/src/authority/AuthorityMetadata.ts index 79fb31beb3..6b64a8dc29 100644 --- a/lib/msal-common/src/authority/AuthorityMetadata.ts +++ b/lib/msal-common/src/authority/AuthorityMetadata.ts @@ -3,10 +3,6 @@ * Licensed under the MIT License. */ -import { - ClientConfigurationErrorCodes, - createClientConfigurationError, -} from "../error/ClientConfigurationError"; import { UrlString } from "../url/UrlString"; import { CloudDiscoveryMetadata } from "./CloudDiscoveryMetadata"; diff --git a/lib/msal-common/src/cache/entities/AccountEntity.ts b/lib/msal-common/src/cache/entities/AccountEntity.ts index 95f13aeafe..47edb5c879 100644 --- a/lib/msal-common/src/cache/entities/AccountEntity.ts +++ b/lib/msal-common/src/cache/entities/AccountEntity.ts @@ -153,8 +153,6 @@ export class AccountEntity { account.realm = accountDetails.idTokenClaims.tid || Constants.EMPTY_STRING; - account.idTokenClaims = accountDetails.idTokenClaims; - // How do you account for MSA CID here? account.localAccountId = accountDetails.idTokenClaims.oid || diff --git a/lib/msal-common/src/response/ResponseHandler.ts b/lib/msal-common/src/response/ResponseHandler.ts index ecd3c69b21..8d71073507 100644 --- a/lib/msal-common/src/response/ResponseHandler.ts +++ b/lib/msal-common/src/response/ResponseHandler.ts @@ -44,6 +44,7 @@ import { IPerformanceClient } from "../telemetry/performance/IPerformanceClient" import { PerformanceEvents } from "../telemetry/performance/PerformanceEvent"; import { checkMaxAge, extractTokenClaims } from "../account/AuthToken"; import { TokenClaims } from "../account/TokenClaims"; +import { AccountInfo } from "../account/AccountInfo"; /** * Class that handles response parsing. @@ -599,14 +600,24 @@ export class ResponseHandler { serverTokenResponse?.spa_accountid; } + let accountInfo: AccountInfo | null = null; + if (cacheRecord.account && idTokenClaims) { + const fullAccountEntity: AccountEntity = Object.assign( + new AccountEntity(), + { + ...cacheRecord.account, + idTokenClaims: idTokenClaims, + } + ); + accountInfo = fullAccountEntity.getAccountInfo(); + } + return { authority: authority.canonicalAuthority, uniqueId: uid, tenantId: tid, scopes: responseScopes, - account: cacheRecord.account - ? cacheRecord.account.getAccountInfo() - : null, + account: accountInfo, idToken: cacheRecord?.idToken?.secret || "", idTokenClaims: idTokenClaims || {}, accessToken: accessToken, diff --git a/lib/msal-common/test/cache/entities/AccountEntity.spec.ts b/lib/msal-common/test/cache/entities/AccountEntity.spec.ts index 63cc2bf229..2c61fe56b4 100644 --- a/lib/msal-common/test/cache/entities/AccountEntity.spec.ts +++ b/lib/msal-common/test/cache/entities/AccountEntity.spec.ts @@ -25,6 +25,7 @@ import { ProtocolMode } from "../../../src/authority/ProtocolMode"; import { LogLevel, Logger } from "../../../src/logger/Logger"; import { Authority } from "../../../src/authority/Authority"; import { AuthorityType } from "../../../src/authority/AuthorityType"; +import { TokenClaims } from "../../../src"; const cryptoInterface: ICrypto = { createNewGuid(): string { @@ -170,7 +171,6 @@ describe("AccountEntity.ts Unit Tests", () => { expect(acc.realm).toBe(idTokenClaims.tid); expect(acc.username).toBe("AbeLi@microsoft.com"); expect(acc.localAccountId).toEqual(idTokenClaims.oid); - expect(acc.idTokenClaims).toBe(idTokenClaims); }); it("create an Account with sub instead of oid as localAccountId", () => { @@ -210,7 +210,6 @@ describe("AccountEntity.ts Unit Tests", () => { expect(acc.realm).toBe(idTokenClaims.tid); expect(acc.username).toBe("AbeLi@microsoft.com"); expect(acc.localAccountId).toEqual(idTokenClaims.sub); - expect(acc.idTokenClaims).toBe(idTokenClaims); }); it("create an Account with emails claim instead of preferred_username claim", () => { @@ -249,7 +248,6 @@ describe("AccountEntity.ts Unit Tests", () => { expect(acc.realm).toBe(idTokenClaims.tid); expect(acc.username).toBe("AbeLi@microsoft.com"); expect(acc.localAccountId).toEqual(idTokenClaims.oid); - expect(acc.idTokenClaims).toBe(idTokenClaims); }); it("create an Account no preferred_username or emails claim", () => { @@ -297,7 +295,6 @@ describe("AccountEntity.ts Unit Tests", () => { expect(acc.realm).toBe(idTokenClaims.tid); expect(acc.username).toBe(""); expect(acc.localAccountId).toEqual(idTokenClaims.oid); - expect(acc.idTokenClaims).toBe(idTokenClaims); }); it("creates a generic account", () => { @@ -350,7 +347,6 @@ describe("AccountEntity.ts Unit Tests", () => { expect(acc.localAccountId).toBe(idTokenClaims.oid); expect(acc.authorityType).toBe(CacheAccountType.GENERIC_ACCOUNT_TYPE); expect(AccountEntity.isAccountEntity(acc)).toEqual(true); - expect(acc.idTokenClaims).toBe(idTokenClaims); }); it("verify if an object is an account entity", () => { @@ -363,9 +359,10 @@ describe("AccountEntity.ts Unit Tests", () => { describe("accountInfoIsEqual()", () => { let acc: AccountEntity; + let idTokenClaims: TokenClaims; beforeEach(() => { // Set up stubs - const idTokenClaims = { + idTokenClaims = { ver: "2.0", iat: 1536361411, iss: `${TEST_URIS.DEFAULT_INSTANCE}9188040d-6c67-4c5b-b112-36a304b66dad/v2.0`, @@ -407,7 +404,10 @@ describe("AccountEntity.ts Unit Tests", () => { }); it("returns false if two account info objects represent the same user but have different iat claims", () => { - const acc1: AccountInfo = acc.getAccountInfo(); + const acc1: AccountInfo = { + ...acc.getAccountInfo(), + idTokenClaims: idTokenClaims, + }; const acc2: AccountInfo = { username: acc1.username, homeAccountId: acc1.homeAccountId, @@ -449,7 +449,10 @@ describe("AccountEntity.ts Unit Tests", () => { }); it("returns false if two account info objects represent the same user but have different nonce claims", () => { - const acc1: AccountInfo = acc.getAccountInfo(); + const acc1: AccountInfo = { + ...acc.getAccountInfo(), + idTokenClaims: idTokenClaims, + }; const acc2: AccountInfo = { username: acc1.username, homeAccountId: acc1.homeAccountId, @@ -491,7 +494,10 @@ describe("AccountEntity.ts Unit Tests", () => { }); it("returns false if required AccountInfo parameters are not equal", () => { - const acc1: AccountInfo = acc.getAccountInfo(); + const acc1: AccountInfo = { + ...acc.getAccountInfo(), + idTokenClaims: idTokenClaims, + }; const acc2: AccountInfo = { ...acc1 }; const acc3: AccountInfo = { ...acc1 }; const acc4: AccountInfo = { ...acc1 }; @@ -628,7 +634,6 @@ describe("AccountEntity.ts Unit Tests for ADFS", () => { expect(acc.localAccountId).toBe(idTokenClaims.oid); expect(acc.authorityType).toBe(CacheAccountType.ADFS_ACCOUNT_TYPE); expect(AccountEntity.isAccountEntity(acc)).toEqual(true); - expect(acc.idTokenClaims).toBe(idTokenClaims); }); it("creates a generic ADFS account without OID", () => { @@ -681,6 +686,5 @@ describe("AccountEntity.ts Unit Tests for ADFS", () => { expect(acc.authorityType).toBe(CacheAccountType.ADFS_ACCOUNT_TYPE); expect(acc.localAccountId).toBe(idTokenClaims.sub); expect(AccountEntity.isAccountEntity(acc)).toEqual(true); - expect(acc.idTokenClaims).toBe(idTokenClaims); }); }); diff --git a/lib/msal-node/src/cache/NodeStorage.ts b/lib/msal-node/src/cache/NodeStorage.ts index 6dc7c2bffc..f3d08daf90 100644 --- a/lib/msal-node/src/cache/NodeStorage.ts +++ b/lib/msal-node/src/cache/NodeStorage.ts @@ -233,8 +233,11 @@ export class NodeStorage extends CacheManager { setAccount(account: AccountEntity): void { const accountKey = account.generateAccountKey(); // Remove ID token claims before saving account entity - account.idTokenClaims = undefined; - this.setItem(accountKey, account); + const baseAccount = Object.assign(new AccountEntity(), { + ...account, + idTokenClaims: undefined, + }); + this.setItem(accountKey, baseAccount); } /** From 7fff7fc742c10b49d4c7ef862174d72b145c5311 Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Wed, 11 Oct 2023 12:54:09 -0700 Subject: [PATCH 23/91] Clean up static authority options code --- lib/msal-browser/src/cache/BrowserCacheManager.ts | 1 - lib/msal-common/src/authority/Authority.ts | 1 - lib/msal-common/src/authority/AuthorityOptions.ts | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/msal-browser/src/cache/BrowserCacheManager.ts b/lib/msal-browser/src/cache/BrowserCacheManager.ts index 584a09f523..2ddf576136 100644 --- a/lib/msal-browser/src/cache/BrowserCacheManager.ts +++ b/lib/msal-browser/src/cache/BrowserCacheManager.ts @@ -78,7 +78,6 @@ export class BrowserCacheManager extends CacheManager { protected temporaryCacheStorage: IWindowStorage; // Logger instance protected logger: Logger; - // Static authority options // Cookie life calculation (hours * minutes * seconds * ms) protected readonly COOKIE_LIFE_MULTIPLIER = 24 * 60 * 60 * 1000; diff --git a/lib/msal-common/src/authority/Authority.ts b/lib/msal-common/src/authority/Authority.ts index b1282e7eef..1d2133918f 100644 --- a/lib/msal-common/src/authority/Authority.ts +++ b/lib/msal-common/src/authority/Authority.ts @@ -1283,6 +1283,5 @@ export function buildStaticAuthorityOptions( : undefined, knownAuthorities: authOptions.knownAuthorities, cloudDiscoveryMetadata: cloudDiscoveryMetadata, - authorityMetadata: authOptions.authorityMetadata, }; } diff --git a/lib/msal-common/src/authority/AuthorityOptions.ts b/lib/msal-common/src/authority/AuthorityOptions.ts index acd24ab116..4b494f68c0 100644 --- a/lib/msal-common/src/authority/AuthorityOptions.ts +++ b/lib/msal-common/src/authority/AuthorityOptions.ts @@ -20,7 +20,7 @@ export type AuthorityOptions = { }; export type StaticAuthorityOptions = Partial< - Pick + Pick > & { canonicalAuthority?: string; cloudDiscoveryMetadata?: CloudDiscoveryMetadata[]; From aacea064a55c41da6b08e773cb2e4e5452b5d951 Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Wed, 11 Oct 2023 13:16:18 -0700 Subject: [PATCH 24/91] Undo pre-caching idtokenclaims removal for accounts --- lib/msal-browser/src/cache/BrowserCacheManager.ts | 4 +--- lib/msal-common/src/cache/entities/AccountEntity.ts | 1 - lib/msal-node/src/cache/NodeStorage.ts | 7 +------ 3 files changed, 2 insertions(+), 10 deletions(-) diff --git a/lib/msal-browser/src/cache/BrowserCacheManager.ts b/lib/msal-browser/src/cache/BrowserCacheManager.ts index 2ddf576136..fafae59400 100644 --- a/lib/msal-browser/src/cache/BrowserCacheManager.ts +++ b/lib/msal-browser/src/cache/BrowserCacheManager.ts @@ -433,10 +433,8 @@ export class BrowserCacheManager extends CacheManager { */ setAccount(account: AccountEntity): void { this.logger.trace("BrowserCacheManager.setAccount called"); - // Remove ID token claims before saving account entity - const baseAccount = { ...account, idTokenClaims: undefined }; const key = account.generateAccountKey(); - this.setItem(key, JSON.stringify(baseAccount)); + this.setItem(key, JSON.stringify(account)); this.addAccountKeyToMap(key); } diff --git a/lib/msal-common/src/cache/entities/AccountEntity.ts b/lib/msal-common/src/cache/entities/AccountEntity.ts index 47edb5c879..b050563768 100644 --- a/lib/msal-common/src/cache/entities/AccountEntity.ts +++ b/lib/msal-common/src/cache/entities/AccountEntity.ts @@ -205,7 +205,6 @@ export class AccountEntity { account.username = accountInfo.username; account.name = accountInfo.name; - account.idTokenClaims = accountInfo.idTokenClaims; account.cloudGraphHostName = cloudGraphHostName; account.msGraphHost = msGraphHost; diff --git a/lib/msal-node/src/cache/NodeStorage.ts b/lib/msal-node/src/cache/NodeStorage.ts index f3d08daf90..074918f35e 100644 --- a/lib/msal-node/src/cache/NodeStorage.ts +++ b/lib/msal-node/src/cache/NodeStorage.ts @@ -232,12 +232,7 @@ export class NodeStorage extends CacheManager { */ setAccount(account: AccountEntity): void { const accountKey = account.generateAccountKey(); - // Remove ID token claims before saving account entity - const baseAccount = Object.assign(new AccountEntity(), { - ...account, - idTokenClaims: undefined, - }); - this.setItem(accountKey, baseAccount); + this.setItem(accountKey, account); } /** From 5a40a6635294d2bea24cf7b8d5e391f5212cfe97 Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Wed, 11 Oct 2023 14:23:52 -0700 Subject: [PATCH 25/91] Update lib/msal-common/src/response/ResponseHandler.ts Co-authored-by: Thomas Norling --- lib/msal-common/src/response/ResponseHandler.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/lib/msal-common/src/response/ResponseHandler.ts b/lib/msal-common/src/response/ResponseHandler.ts index 8d71073507..48490b8c9c 100644 --- a/lib/msal-common/src/response/ResponseHandler.ts +++ b/lib/msal-common/src/response/ResponseHandler.ts @@ -602,15 +602,7 @@ export class ResponseHandler { let accountInfo: AccountInfo | null = null; if (cacheRecord.account && idTokenClaims) { - const fullAccountEntity: AccountEntity = Object.assign( - new AccountEntity(), - { - ...cacheRecord.account, - idTokenClaims: idTokenClaims, - } - ); - accountInfo = fullAccountEntity.getAccountInfo(); - } + accountInfo = { ...cacheRecord.account.getAccountInfo(), idTokenClaims }; return { authority: authority.canonicalAuthority, From 9001ceecc60095d7d73212f4393497212b58f8ec Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Wed, 11 Oct 2023 14:39:48 -0700 Subject: [PATCH 26/91] Add environment filtering tests with static metadata and refactor response handler account building --- .../NativeInteractionClient.ts | 11 +- .../test/app/PublicClientApplication.spec.ts | 9 + .../test/utils/StringConstants.ts | 2 +- lib/msal-common/src/account/AccountInfo.ts | 7 - lib/msal-common/src/cache/CacheManager.ts | 7 +- lib/msal-common/src/index.ts | 6 +- .../src/response/ResponseHandler.ts | 17 +- .../test/cache/CacheManager.spec.ts | 229 ++++++++++++++---- lib/msal-common/test/cache/MockCache.ts | 10 +- .../test/test_kit/StringConstants.ts | 3 + 10 files changed, 221 insertions(+), 80 deletions(-) diff --git a/lib/msal-browser/src/interaction_client/NativeInteractionClient.ts b/lib/msal-browser/src/interaction_client/NativeInteractionClient.ts index cd31e35a6d..96a187fdf3 100644 --- a/lib/msal-browser/src/interaction_client/NativeInteractionClient.ts +++ b/lib/msal-browser/src/interaction_client/NativeInteractionClient.ts @@ -33,7 +33,6 @@ import { invokeAsync, createAuthError, AuthErrorCodes, - updateAccountInfo, } from "@azure/msal-common"; import { BaseInteractionClient } from "./BaseInteractionClient"; import { BrowserConfiguration } from "../config/Configuration"; @@ -257,12 +256,10 @@ export class NativeInteractionClient extends BaseInteractionClient { silentRequest ); - const fullAccount = - result.idToken && result.idTokenClaims - ? updateAccountInfo(account, { - idTokenClaims: result.idTokenClaims as TokenClaims, - }) - : account; + const fullAccount = { + ...account, + idTokenClaims: result?.idTokenClaims as TokenClaims, + }; return { ...result, diff --git a/lib/msal-browser/test/app/PublicClientApplication.spec.ts b/lib/msal-browser/test/app/PublicClientApplication.spec.ts index bd7cf78b73..24fd3582c2 100644 --- a/lib/msal-browser/test/app/PublicClientApplication.spec.ts +++ b/lib/msal-browser/test/app/PublicClientApplication.spec.ts @@ -5256,6 +5256,15 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { testAccountInfo4.homeAccountId ); }); + it("getAccount returns account specified using sid", () => { + const account = pca.getAccount({ + sid: "testSid", + }); + expect(account?.idToken).not.toBeUndefined(); + expect(account?.homeAccountId).toEqual( + testAccountInfo3.homeAccountId + ); + }); }); it("getAccount returns account specified using homeAccountId", () => { diff --git a/lib/msal-browser/test/utils/StringConstants.ts b/lib/msal-browser/test/utils/StringConstants.ts index 30e8e20264..f571fa18f3 100644 --- a/lib/msal-browser/test/utils/StringConstants.ts +++ b/lib/msal-browser/test/utils/StringConstants.ts @@ -88,7 +88,7 @@ export const TEST_TOKENS = { REFRESH_TOKEN: "thisIsARefreshT0ken", // [SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Fake credential used for testing purposes")] ID_TOKEN_V2_WITH_LOGIN_HINT: - "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImQyOWRkNWZkODFjOThjYTY0NjVmOTdlMTA4MDEwNmYyIn0.eyJzdWIiOiJBQUFBQUFBQUFBQUFBQUFBQUFBQUFJa3pxRlZyU2FTYUZIeTc4MmJidGFRIiwidmVyIjoiMi4wIiwiYWlvIjoiRGYyVVZYTDFpeCFsTUNXTVNPSkJjRmF0emNHZnZGR2hqS3Y4cTVnMHg3MzJkUjVNQjVCaXN2R1FPN1lXQnlqZDhpUURMcSFlR2JJRGFreXA1bW5PcmNkcUhlWVNubHRlcFFtUnA2QUlaOGpZIiwiaXNzIjoiaHR0cHM6Ly9sb2dpbi5taWNyb3NvZnRvbmxpbmUuY29tLzkxODgwNDBkLTZjNjctNGM1Yi1iMTEyLTM2YTMwNGI2NmRhZC92Mi4wIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiVW5pcXVlIFVzZXJuYW1lIiwib2lkIjoiMDAwMDAwMDAtMDAwMC0wMDAwLTY2ZjMtMzMzMmVjYTdlYTgxIiwibm9uY2UiOiIxMjM1MjMiLCJ0aWQiOiIzMzM4MDQwZC02YzY3LTRjNWItYjExMi0zNmEzMDRiNjZkYWQiLCJhdWQiOiI2Y2IwNDAxOC1hM2Y1LTQ2YTctYjk5NS05NDBjNzhmNWFlZjMiLCJsb2dpbl9oaW50IjoidGVzdExvZ2luSGludCIsIm5iZiI6IjE1MzYzNjE0MTEiLCJuYW1lIjoiQWJlIExpbmNvbG4iLCJleHAiOiIxNTM2MzYxNDExIiwiaWF0IjoiMTUzNjM2MTQxMSJ9.GTmMQI1ZrGcCG4UHENiMrgW-0oXBAFHumroKZbTrzLd74e2PJnOnQCqqmuRtyAFfplb8gWgQWODK-F5rfYTokN-bjvk4XTvcwrMKqNfEYoCBBl-gTWzbz_1qIdeRIhAX956LwPGUpZWM0qMeDiI3UKaI9cCOC5keNNHb6N8TxAylxVn-CmoYv8QqFyXrHWvp6zAHeJc-SQsNjkacIFZrSqVfbzuD50ftLOnu9t7S-FmCWuPAgncjs-GtpDbJ9LOt-uRYgHwp8Sp3i-cUi3m0FxNsF4W_EUWnbfgu3gD7b5tscfgBc0gGXOO-cGVk5u7lkoHFFGKLr4oqxHhFV43X_w", + "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6ImEzYmRhYjIzYTVlMDI4NmM2NmM1MjRiZWFmNDQzZGJhIn0.eyJzdWIiOiJBQUFBQUFBQUFBQUFBQUFBQUFBQUFJa3pxRlZyU2FTYUZIeTc4MmJidGFRIiwidmVyIjoiMi4wIiwiYWlvIjoiRGYyVVZYTDFpeCFsTUNXTVNPSkJjRmF0emNHZnZGR2hqS3Y4cTVnMHg3MzJkUjVNQjVCaXN2R1FPN1lXQnlqZDhpUURMcSFlR2JJRGFreXA1bW5PcmNkcUhlWVNubHRlcFFtUnA2QUlaOGpZIiwiaXNzIjoiaHR0cHM6Ly9sb2dpbi5taWNyb3NvZnRvbmxpbmUuY29tLzkxODgwNDBkLTZjNjctNGM1Yi1iMTEyLTM2YTMwNGI2NmRhZC92Mi4wIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiVW5pcXVlIFVzZXJuYW1lIiwib2lkIjoiMDAwMDAwMDAtMDAwMC0wMDAwLTY2ZjMtMzMzMmVjYTdlYTgxIiwibm9uY2UiOiIxMjM1MjMiLCJ0aWQiOiIzMzM4MDQwZC02YzY3LTRjNWItYjExMi0zNmEzMDRiNjZkYWQiLCJhdWQiOiI2Y2IwNDAxOC1hM2Y1LTQ2YTctYjk5NS05NDBjNzhmNWFlZjMiLCJsb2dpbl9oaW50IjoidGVzdExvZ2luSGludCIsInNpZCI6InRlc3RTaWQiLCJuYmYiOiIxNTM2MzYxNDExIiwibmFtZSI6IkFiZSBMaW5jb2xuIiwiZXhwIjoiMTUzNjM2MTQxMSIsImlhdCI6IjE1MzYzNjE0MTEifQ.ZfEosstCNsNiOnFd7WXJWMSkIKzticb97qZVwoprztMBeT_szHFhZM1RZCnAbmHlC8J8b7RPpwm09RdjN31iDA", // [SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Fake credential used for testing purposes")] ID_TOKEN_V2_WITH_UPN: "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJBQUFBQUFBQUFBQUFBQUFBQUFBQUFJa3pxRlZyU2FTYUZIeTc4MmJidGFRIiwidmVyIjoiMi4wIiwiYWlvIjoiRGYyVVZYTDFpeCFsTUNXTVNPSkJjRmF0emNHZnZGR2hqS3Y4cTVnMHg3MzJkUjVNQjVCaXN2R1FPN1lXQnlqZDhpUURMcSFlR2JJRGFreXA1bW5PcmNkcUhlWVNubHRlcFFtUnA2QUlaOGpZIiwiaXNzIjoiaHR0cHM6Ly9sb2dpbi5taWNyb3NvZnRvbmxpbmUuY29tLzkxODgwNDBkLTZjNjctNGM1Yi1iMTEyLTM2YTMwNGI2NmRhZC92Mi4wIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiQWJlTGlAbWljcm9zb2Z0LmNvbSIsIm9pZCI6IjAwMDAwMDAwLTAwMDAtMDAwMC02NmYzLTMzMzJlY2E3ZWE4MSIsIm5vbmNlIjoiMTIzNTIzIiwidGlkIjoiMzMzODA0MGQtNmM2Ny00YzViLWIxMTItMzZhMzA0YjY2ZGFkIiwiYXVkIjoiNmNiMDQwMTgtYTNmNS00NmE3LWI5OTUtOTQwYzc4ZjVhZWYzIiwibmJmIjoiMTUzNjM2MTQxMSIsIm5hbWUiOiJBYmUgTGluY29sbiIsImV4cCI6IjE1MzYzNjE0MTEiLCJpYXQiOiIxNTM2MzYxNDExIiwidXBuIjoidGVzdFVwbiJ9._8gXqY4qn4A8v_et5fESz-82BU4ad2F_v2msO9CcvIA", diff --git a/lib/msal-common/src/account/AccountInfo.ts b/lib/msal-common/src/account/AccountInfo.ts index cd459d9489..3471373ae9 100644 --- a/lib/msal-common/src/account/AccountInfo.ts +++ b/lib/msal-common/src/account/AccountInfo.ts @@ -41,10 +41,3 @@ export type ActiveAccountFilters = { homeAccountId: string; localAccountId: string; }; - -export function updateAccountInfo( - accountInfo: AccountInfo, - updatedAttributes: Partial -): AccountInfo { - return { ...accountInfo, ...updatedAttributes }; -} diff --git a/lib/msal-common/src/cache/CacheManager.ts b/lib/msal-common/src/cache/CacheManager.ts index 57a6cfe65f..c439048568 100644 --- a/lib/msal-common/src/cache/CacheManager.ts +++ b/lib/msal-common/src/cache/CacheManager.ts @@ -1432,10 +1432,15 @@ export abstract class CacheManager implements ICacheManager { this.staticAuthorityOptions.canonicalAuthority ) || this.staticAuthorityOptions.knownAuthorities; - if (staticAliases && staticAliases.includes(entity.environment)) { + if ( + staticAliases && + staticAliases.includes(environment) && + staticAliases.includes(entity.environment) + ) { return true; } } + // Query metadata cache if no static authority configuration has aliases that match enviroment const cloudMetadata = this.getAuthorityMetadataByAlias(environment); if ( diff --git a/lib/msal-common/src/index.ts b/lib/msal-common/src/index.ts index aaef203951..fb53df6c69 100644 --- a/lib/msal-common/src/index.ts +++ b/lib/msal-common/src/index.ts @@ -28,11 +28,7 @@ export { } from "./config/AppTokenProvider"; export { ClientConfiguration } from "./config/ClientConfiguration"; // Account -export { - AccountInfo, - ActiveAccountFilters, - updateAccountInfo, -} from "./account/AccountInfo"; +export { AccountInfo, ActiveAccountFilters } from "./account/AccountInfo"; export * as AuthToken from "./account/AuthToken"; export { TokenClaims } from "./account/TokenClaims"; export { TokenClaims as IdTokenClaims } from "./account/TokenClaims"; diff --git a/lib/msal-common/src/response/ResponseHandler.ts b/lib/msal-common/src/response/ResponseHandler.ts index 8d71073507..e2373d646c 100644 --- a/lib/msal-common/src/response/ResponseHandler.ts +++ b/lib/msal-common/src/response/ResponseHandler.ts @@ -600,17 +600,12 @@ export class ResponseHandler { serverTokenResponse?.spa_accountid; } - let accountInfo: AccountInfo | null = null; - if (cacheRecord.account && idTokenClaims) { - const fullAccountEntity: AccountEntity = Object.assign( - new AccountEntity(), - { - ...cacheRecord.account, - idTokenClaims: idTokenClaims, - } - ); - accountInfo = fullAccountEntity.getAccountInfo(); - } + const accountInfo: AccountInfo | null = cacheRecord.account + ? { + ...cacheRecord.account.getAccountInfo(), + idTokenClaims, + } + : null; return { authority: authority.canonicalAuthority, diff --git a/lib/msal-common/test/cache/CacheManager.spec.ts b/lib/msal-common/test/cache/CacheManager.spec.ts index 3576cbeebb..50fe7dd0a6 100644 --- a/lib/msal-common/test/cache/CacheManager.spec.ts +++ b/lib/msal-common/test/cache/CacheManager.spec.ts @@ -37,9 +37,15 @@ import { AppMetadataEntity } from "../../src/cache/entities/AppMetadataEntity"; import { RefreshTokenEntity } from "../../src/cache/entities/RefreshTokenEntity"; import { IdTokenEntity } from "../../src/cache/entities/IdTokenEntity"; import { CommonSilentFlowRequest, ScopeSet } from "../../src"; +import * as authorityMetadata from "../../src/authority/AuthorityMetadata"; describe("CacheManager.ts test cases", () => { - const mockCache = new MockCache(CACHE_MOCKS.MOCK_CLIENT_ID, mockCrypto); + const mockCache = new MockCache(CACHE_MOCKS.MOCK_CLIENT_ID, mockCrypto, { + canonicalAuthority: TEST_CONFIG.validAuthority, + cloudDiscoveryMetadata: JSON.parse(TEST_CONFIG.CLOUD_DISCOVERY_METADATA) + .metadata, + knownAuthorities: [TEST_CONFIG.validAuthorityHost], + }); let authorityMetadataStub: sinon.SinonStub; beforeEach(() => { mockCache.initializeCache(); @@ -528,52 +534,183 @@ describe("CacheManager.ts test cases", () => { ).toBe(false); }); - it("environment filter", () => { - // filter by environment - expect( - mockCache.cacheManager.credentialMatchesFilter(testIdToken, { - environment: testIdToken.environment, - }) - ).toBe(true); - expect( - mockCache.cacheManager.credentialMatchesFilter( - testAccessToken, - { - environment: testAccessToken.environment, - } - ) - ).toBe(true); - expect( - mockCache.cacheManager.credentialMatchesFilter( - testRefreshToken, - { - environment: testRefreshToken.environment, - } - ) - ).toBe(true); + describe("environment filter", () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + it("with configured static cloud discovery metadata", () => { + // filter by environment + expect( + mockCache.cacheManager.credentialMatchesFilter( + testIdToken, + { + environment: testIdToken.environment, + } + ) + ).toBe(true); + expect( + mockCache.cacheManager.credentialMatchesFilter( + testAccessToken, + { + environment: testAccessToken.environment, + } + ) + ).toBe(true); + expect( + mockCache.cacheManager.credentialMatchesFilter( + testRefreshToken, + { + environment: testRefreshToken.environment, + } + ) + ).toBe(true); + + // Test failure cases + expect( + mockCache.cacheManager.credentialMatchesFilter( + testIdToken, + { + environment: "wrong.contoso.com", + } + ) + ).toBe(false); + expect( + mockCache.cacheManager.credentialMatchesFilter( + testAccessToken, + { + environment: "wrong.contoso.com", + } + ) + ).toBe(false); + expect( + mockCache.cacheManager.credentialMatchesFilter( + testRefreshToken, + { + environment: "wrong.contoso.com", + } + ) + ).toBe(false); + }); - // Test failure cases - expect( - mockCache.cacheManager.credentialMatchesFilter(testIdToken, { - environment: "wrong.contoso.com", - }) - ).toBe(false); - expect( - mockCache.cacheManager.credentialMatchesFilter( - testAccessToken, - { - environment: "wrong.contoso.com", - } - ) - ).toBe(false); - expect( - mockCache.cacheManager.credentialMatchesFilter( - testRefreshToken, - { - environment: "wrong.contoso.com", - } - ) - ).toBe(false); + it("with hardcoded cloud discovery metadata", () => { + jest.spyOn( + authorityMetadata, + "getAliasesFromConfigMetadata" + ).mockReturnValue(null); + // filter by environment + expect( + mockCache.cacheManager.credentialMatchesFilter( + testIdToken, + { + environment: testIdToken.environment, + } + ) + ).toBe(true); + expect( + mockCache.cacheManager.credentialMatchesFilter( + testAccessToken, + { + environment: testAccessToken.environment, + } + ) + ).toBe(true); + expect( + mockCache.cacheManager.credentialMatchesFilter( + testRefreshToken, + { + environment: testRefreshToken.environment, + } + ) + ).toBe(true); + + // Test failure cases + expect( + mockCache.cacheManager.credentialMatchesFilter( + testIdToken, + { + environment: "wrong.contoso.com", + } + ) + ).toBe(false); + expect( + mockCache.cacheManager.credentialMatchesFilter( + testAccessToken, + { + environment: "wrong.contoso.com", + } + ) + ).toBe(false); + expect( + mockCache.cacheManager.credentialMatchesFilter( + testRefreshToken, + { + environment: "wrong.contoso.com", + } + ) + ).toBe(false); + }); + + it("with knownAuthorities", () => { + jest.spyOn( + authorityMetadata, + "getAliasesFromConfigMetadata" + ).mockReturnValue(null); + jest.spyOn( + authorityMetadata, + "getHardcodedAliasesForCanonicalAuthority" + ).mockReturnValue(null); + // filter by environment + expect( + mockCache.cacheManager.credentialMatchesFilter( + testIdToken, + { + environment: testIdToken.environment, + } + ) + ).toBe(true); + expect( + mockCache.cacheManager.credentialMatchesFilter( + testAccessToken, + { + environment: testAccessToken.environment, + } + ) + ).toBe(true); + expect( + mockCache.cacheManager.credentialMatchesFilter( + testRefreshToken, + { + environment: testRefreshToken.environment, + } + ) + ).toBe(true); + + // Test failure cases + expect( + mockCache.cacheManager.credentialMatchesFilter( + testIdToken, + { + environment: "wrong.contoso.com", + } + ) + ).toBe(false); + expect( + mockCache.cacheManager.credentialMatchesFilter( + testAccessToken, + { + environment: "wrong.contoso.com", + } + ) + ).toBe(false); + expect( + mockCache.cacheManager.credentialMatchesFilter( + testRefreshToken, + { + environment: "wrong.contoso.com", + } + ) + ).toBe(false); + }); }); it("realm filter", () => { diff --git a/lib/msal-common/test/cache/MockCache.ts b/lib/msal-common/test/cache/MockCache.ts index 2f5254fbeb..49f6f1d230 100644 --- a/lib/msal-common/test/cache/MockCache.ts +++ b/lib/msal-common/test/cache/MockCache.ts @@ -13,6 +13,7 @@ import { IdTokenEntity, RefreshTokenEntity, Logger, + StaticAuthorityOptions, } from "../../src"; import { MockStorageClass } from "../client/ClientTestUtils"; import { TEST_TOKENS, TEST_CRYPTO_VALUES } from "../test_kit/StringConstants"; @@ -20,11 +21,16 @@ import { TEST_TOKENS, TEST_CRYPTO_VALUES } from "../test_kit/StringConstants"; export class MockCache { cacheManager: MockStorageClass; - constructor(clientId: string, cryptoImpl: ICrypto) { + constructor( + clientId: string, + cryptoImpl: ICrypto, + staticAuthorityOptions?: StaticAuthorityOptions + ) { this.cacheManager = new MockStorageClass( clientId, cryptoImpl, - new Logger({}) + new Logger({}), + staticAuthorityOptions ); } diff --git a/lib/msal-common/test/test_kit/StringConstants.ts b/lib/msal-common/test/test_kit/StringConstants.ts index e7cb02377a..b407d2247e 100644 --- a/lib/msal-common/test/test_kit/StringConstants.ts +++ b/lib/msal-common/test/test_kit/StringConstants.ts @@ -148,6 +148,7 @@ export const TEST_CONFIG = { MSAL_CLIENT_SECRET: "ThisIsASecret", MSAL_TENANT_ID: "3338040d-6c67-4c5b-b112-36a304b66dad", validAuthority: TEST_URIS.DEFAULT_INSTANCE + "common", + validAuthorityHost: "login.microsoftonline.com", alternateValidAuthority: TEST_URIS.ALTERNATE_INSTANCE + "common", ADFS_VALID_AUTHORITY: "https://on.prem/adfs", DSTS_VALID_AUTHORITY: "https://domain.dsts.subdomain/dstsv2/tenant", @@ -183,6 +184,8 @@ export const TEST_CONFIG = { DEFAULT_TOKEN_RENEWAL_OFFSET: 300, TEST_CONFIG_ASSERTION: "DefaultAssertion", TEST_REQUEST_ASSERTION: "RequestAssertion", + CLOUD_DISCOVERY_METADATA: + '{"tenant_discovery_endpoint":"https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration","api-version":"1.1","metadata":[{"preferred_network":"login.microsoftonline.com","preferred_cache":"login.windows.net","aliases":["login.microsoftonline.com","login.windows.net","login.microsoft.com","sts.windows.net"]},{"preferred_network":"login.partner.microsoftonline.cn","preferred_cache":"login.partner.microsoftonline.cn","aliases":["login.partner.microsoftonline.cn","login.chinacloudapi.cn"]},{"preferred_network":"login.microsoftonline.de","preferred_cache":"login.microsoftonline.de","aliases":["login.microsoftonline.de"]},{"preferred_network":"login.microsoftonline.us","preferred_cache":"login.microsoftonline.us","aliases":["login.microsoftonline.us","login.usgovcloudapi.net"]},{"preferred_network":"login-us.microsoftonline.com","preferred_cache":"login-us.microsoftonline.com","aliases":["login-us.microsoftonline.com"]}]}', }; export const RANDOM_TEST_GUID = "11553a9b-7116-48b1-9d48-f6d4a8ff8371"; From 9f9af08861cce2c60029e00df74322a85350c54b Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Thu, 12 Oct 2023 11:55:13 -0700 Subject: [PATCH 27/91] Add tenant list assignment to AccountEntity.createFromAccountInfo --- .../test/app/PublicClientApplication.spec.ts | 46 +++++-------------- .../src/cache/entities/AccountEntity.ts | 1 + 2 files changed, 13 insertions(+), 34 deletions(-) diff --git a/lib/msal-browser/test/app/PublicClientApplication.spec.ts b/lib/msal-browser/test/app/PublicClientApplication.spec.ts index 373107dd98..8d07805f4f 100644 --- a/lib/msal-browser/test/app/PublicClientApplication.spec.ts +++ b/lib/msal-browser/test/app/PublicClientApplication.spec.ts @@ -4942,23 +4942,18 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { authorityType: "MSSTS", homeAccountId: TEST_DATA_CLIENT_INFO.TEST_HOME_ACCOUNT_ID, environment: "login.windows.net", - tenantId: TEST_DATA_CLIENT_INFO.TEST_UTID, + tenantId: ID_TOKEN_CLAIMS.tid, username: ID_TOKEN_CLAIMS.preferred_username, name: "Abe Lincoln", localAccountId: ID_TOKEN_CLAIMS.oid, idToken: TEST_TOKENS.IDTOKEN_V2, idTokenClaims: ID_TOKEN_CLAIMS, nativeAccountId: undefined, + tenants: [ID_TOKEN_CLAIMS.tid], }; - const testAccount1: AccountEntity = new AccountEntity(); - testAccount1.homeAccountId = testAccountInfo1.homeAccountId; - testAccount1.localAccountId = TEST_CONFIG.OID; - testAccount1.environment = testAccountInfo1.environment; - testAccount1.realm = testAccountInfo1.tenantId; - testAccount1.username = testAccountInfo1.username; - testAccount1.name = testAccountInfo1.name; - testAccount1.authorityType = "MSSTS"; + const testAccount1: AccountEntity = + AccountEntity.createFromAccountInfo(testAccountInfo1); testAccount1.clientInfo = TEST_DATA_CLIENT_INFO.TEST_CLIENT_INFO_B64ENCODED; @@ -4976,23 +4971,18 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { authorityType: "MSSTS", homeAccountId: "different-home-account-id", environment: "login.windows.net", - tenantId: TEST_DATA_CLIENT_INFO.TEST_UTID, + tenantId: ID_TOKEN_ALT_CLAIMS.tid, username: "anotherExample@microsoft.com", name: "Abe Lincoln", localAccountId: ID_TOKEN_ALT_CLAIMS.oid, idToken: TEST_TOKENS.IDTOKEN_V2_ALT, idTokenClaims: ID_TOKEN_ALT_CLAIMS, nativeAccountId: undefined, + tenants: [ID_TOKEN_ALT_CLAIMS.tid], }; - const testAccount2: AccountEntity = new AccountEntity(); - testAccount2.homeAccountId = testAccountInfo2.homeAccountId; - testAccount2.localAccountId = ID_TOKEN_ALT_CLAIMS.oid; - testAccount2.environment = testAccountInfo2.environment; - testAccount2.realm = testAccountInfo2.tenantId; - testAccount2.username = testAccountInfo2.username; - testAccount2.name = testAccountInfo2.name; - testAccount2.authorityType = "MSSTS"; + const testAccount2: AccountEntity = + AccountEntity.createFromAccountInfo(testAccountInfo2); testAccount2.clientInfo = TEST_DATA_CLIENT_INFO.TEST_CLIENT_INFO_B64ENCODED; @@ -5018,14 +5008,8 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { nativeAccountId: undefined, }; - const testAccount3: AccountEntity = new AccountEntity(); - testAccount3.homeAccountId = testAccountInfo3.homeAccountId; - testAccount3.localAccountId = TEST_CONFIG.OID; - testAccount3.environment = testAccountInfo3.environment; - testAccount3.realm = testAccountInfo3.tenantId; - testAccount3.username = testAccountInfo3.username; - testAccount3.name = testAccountInfo3.name; - testAccount3.authorityType = "ADFS"; + const testAccount3: AccountEntity = + AccountEntity.createFromAccountInfo(testAccountInfo3); testAccount3.clientInfo = TEST_DATA_CLIENT_INFO.TEST_CLIENT_INFO_B64ENCODED; @@ -5053,14 +5037,8 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { nativeAccountId: undefined, }; - const testAccount4: AccountEntity = new AccountEntity(); - testAccount4.homeAccountId = testAccountInfo4.homeAccountId; - testAccount4.localAccountId = TEST_CONFIG.OID; - testAccount4.environment = testAccountInfo4.environment; - testAccount4.realm = testAccountInfo4.tenantId; - testAccount4.username = testAccountInfo4.username; - testAccount4.name = testAccountInfo4.name; - testAccount4.authorityType = "MSA"; + const testAccount4: AccountEntity = + AccountEntity.createFromAccountInfo(testAccountInfo4); testAccount4.clientInfo = TEST_DATA_CLIENT_INFO.TEST_CLIENT_INFO_B64ENCODED; diff --git a/lib/msal-common/src/cache/entities/AccountEntity.ts b/lib/msal-common/src/cache/entities/AccountEntity.ts index 02791a7432..24ecfef5c9 100644 --- a/lib/msal-common/src/cache/entities/AccountEntity.ts +++ b/lib/msal-common/src/cache/entities/AccountEntity.ts @@ -237,6 +237,7 @@ export class AccountEntity { account.cloudGraphHostName = cloudGraphHostName; account.msGraphHost = msGraphHost; + account.tenants = accountInfo.tenants; return account; } From 08111cd18c76e6247322ac2bdee11e8be7c81bae Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Thu, 12 Oct 2023 12:30:51 -0700 Subject: [PATCH 28/91] Update node serialization/deserialization to include tenants list --- .../src/cache/serializer/Deserializer.ts | 1 + .../src/cache/serializer/Serializer.ts | 1 + .../src/cache/serializer/SerializerTypes.ts | 1 + .../cache-unrecognized-entities.json | 19 +++++++-------- lib/msal-node/test/cache/cacheConstants.ts | 9 ++++---- .../cache/serializer/Deserializer.spec.ts | 5 ++-- .../test/cache/serializer/cache.json | 23 ++++++++++--------- 7 files changed, 33 insertions(+), 26 deletions(-) diff --git a/lib/msal-node/src/cache/serializer/Deserializer.ts b/lib/msal-node/src/cache/serializer/Deserializer.ts index 5b86b08b9b..a6adfa480d 100644 --- a/lib/msal-node/src/cache/serializer/Deserializer.ts +++ b/lib/msal-node/src/cache/serializer/Deserializer.ts @@ -61,6 +61,7 @@ export class Deserializer { clientInfo: serializedAcc.client_info, lastModificationTime: serializedAcc.last_modification_time, lastModificationApp: serializedAcc.last_modification_app, + tenants: serializedAcc.tenants, }; const account: AccountEntity = new AccountEntity(); CacheManager.toObject(account, mappedAcc); diff --git a/lib/msal-node/src/cache/serializer/Serializer.ts b/lib/msal-node/src/cache/serializer/Serializer.ts index 756f537ec0..89380113c5 100644 --- a/lib/msal-node/src/cache/serializer/Serializer.ts +++ b/lib/msal-node/src/cache/serializer/Serializer.ts @@ -50,6 +50,7 @@ export class Serializer { client_info: accountEntity.clientInfo, last_modification_time: accountEntity.lastModificationTime, last_modification_app: accountEntity.lastModificationApp, + tenants: accountEntity.tenants, }; }); diff --git a/lib/msal-node/src/cache/serializer/SerializerTypes.ts b/lib/msal-node/src/cache/serializer/SerializerTypes.ts index aec8abe13a..517a8f3529 100644 --- a/lib/msal-node/src/cache/serializer/SerializerTypes.ts +++ b/lib/msal-node/src/cache/serializer/SerializerTypes.ts @@ -57,6 +57,7 @@ export type SerializedAccountEntity = { client_info?: string; last_modification_time?: string; last_modification_app?: string; + tenants?: string[]; }; /** diff --git a/lib/msal-node/test/cache/cache-test-files/cache-unrecognized-entities.json b/lib/msal-node/test/cache/cache-test-files/cache-unrecognized-entities.json index 65badfd0a4..3612e24fd7 100644 --- a/lib/msal-node/test/cache/cache-test-files/cache-unrecognized-entities.json +++ b/lib/msal-node/test/cache/cache-test-files/cache-unrecognized-entities.json @@ -5,17 +5,18 @@ } }, "Account": { - "uid.utid-login.microsoftonline.com-microsoft": { + "uid.utid-login.microsoftonline.com-utid": { "unrecognized_entity": { "abc123": "123" }, "username": "John Doe", "local_account_id": "object1234", - "realm": "microsoft", + "realm": "utid", "environment": "login.microsoftonline.com", "home_account_id": "uid.utid", "authority_type": "MSSTS", - "client_info": "eyJ1aWQiOiJ1aWQiLCAidXRpZCI6InV0aWQifQ==" + "client_info": "eyJ1aWQiOiJ1aWQiLCAidXRpZCI6InV0aWQifQ==", + "tenants": ["microsoft"] } }, "RefreshToken": { @@ -36,11 +37,11 @@ } }, "AccessToken": { - "uid.utid-login.microsoftonline.com-accesstoken-mock_client_id-microsoft-scope1 scope2 scope3--": { + "uid.utid-login.microsoftonline.com-accesstoken-mock_client_id-utid-scope1 scope2 scope3--": { "environment": "login.microsoftonline.com", "credential_type": "AccessToken", "secret": "an access token", - "realm": "microsoft", + "realm": "utid", "target": "scope1 scope2 scope3", "client_id": "mock_client_id", "cached_at": "1000", @@ -48,11 +49,11 @@ "extended_expires_on": "4600", "expires_on": "4600" }, - "uid.utid-login.microsoftonline.com-accesstoken-mock_client_id-microsoft-scope4 scope5--": { + "uid.utid-login.microsoftonline.com-accesstoken-mock_client_id-utid-scope4 scope5--": { "environment": "login.microsoftonline.com", "credential_type": "AccessToken", "secret": "an access token", - "realm": "microsoft", + "realm": "utid", "target": "scope4 scope5", "client_id": "mock_client_id", "cached_at": "1000", @@ -62,8 +63,8 @@ } }, "IdToken": { - "uid.utid-login.microsoftonline.com-idtoken-mock_client_id-microsoft---": { - "realm": "microsoft", + "uid.utid-login.microsoftonline.com-idtoken-mock_client_id-utid---": { + "realm": "utid", "environment": "login.microsoftonline.com", "credential_type": "IdToken", "secret": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IjFMVE16YWtpaGlSbGFfOHoyQkVKVlhlV01xbyJ9.eyJ2ZXIiOiIyLjAiLCJpc3MiOiJodHRwczovL2xvZ2luLm1pY3Jvc29mdG9ubGluZS5jb20vOTE4ODA0MGQtNmM2Ny00YzViLWIxMTItMzZhMzA0YjY2ZGFkL3YyLjAiLCJzdWIiOiJBQUFBQUFBQUFBQUFBQUFBQUFBQUFJa3pxRlZyU2FTYUZIeTc4MmJidGFRIiwiYXVkIjoiNmNiMDQwMTgtYTNmNS00NmE3LWI5OTUtOTQwYzc4ZjVhZWYzIiwiZXhwIjoxNTM2MzYxNDExLCJpYXQiOjE1MzYyNzQ3MTEsIm5iZiI6MTUzNjI3NDcxMSwibmFtZSI6IkFiZSBMaW5jb2xuIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiQWJlTGlAbWljcm9zb2Z0LmNvbSIsIm9pZCI6IjAwMDAwMDAwLTAwMDAtMDAwMC02NmYzLTMzMzJlY2E3ZWE4MSIsInRpZCI6IjMzMzgwNDBkLTZjNjctNGM1Yi1iMTEyLTM2YTMwNGI2NmRhZCIsIm5vbmNlIjoiMTIzNTIzIiwiYWlvIjoiRGYyVVZYTDFpeCFsTUNXTVNPSkJjRmF0emNHZnZGR2hqS3Y4cTVnMHg3MzJkUjVNQjVCaXN2R1FPN1lXQnlqZDhpUURMcSFlR2JJRGFreXA1bW5PcmNkcUhlWVNubHRlcFFtUnA2QUlaOGpZIn0=.1AFWW-Ck5nROwSlltm7GzZvDwUkqvhSQpm55TQsmVo9Y59cLhRXpvB8n-55HCr9Z6G_31_UbeUkoz612I2j_Sm9FFShSDDjoaLQr54CreGIJvjtmS3EkK9a7SJBbcpL1MpUtlfygow39tFjY7EVNW9plWUvRrTgVk7lYLprvfzw-CIqw3gHC-T7IK_m_xkr08INERBtaecwhTeN4chPC4W3jdmw_lIxzC48YoQ0dB1L9-ImX98Egypfrlbm0IBL5spFzL6JDZIRRJOu8vecJvj1mq-IUhGt0MacxX8jdxYLP-KUu2d9MbNKpCKJuZ7p8gwTL5B7NlUdh_dmSviPWrw", diff --git a/lib/msal-node/test/cache/cacheConstants.ts b/lib/msal-node/test/cache/cacheConstants.ts index a8c76fc1ac..fa416cacbc 100644 --- a/lib/msal-node/test/cache/cacheConstants.ts +++ b/lib/msal-node/test/cache/cacheConstants.ts @@ -16,7 +16,7 @@ export const mockAccessTokenEntity_1 = { credentialType: "AccessToken", clientId: "mock_client_id", secret: "an access token", - realm: "microsoft", + realm: "utid", target: "scope1 scope2 scope3", cachedAt: "1000", expiresOn: "4600", @@ -30,7 +30,7 @@ export const mockAccessTokenEntity_2 = { credentialType: "AccessToken", clientId: "mock_client_id", secret: "an access token", - realm: "microsoft", + realm: "utid", target: "scope4 scope5", cachedAt: "1000", expiresOn: "4600", @@ -43,7 +43,7 @@ export const mockIdTokenEntity = { credentialType: "IdToken", clientId: "mock_client_id", secret: "header.eyJvaWQiOiAib2JqZWN0MTIzNCIsICJwcmVmZXJyZWRfdXNlcm5hbWUiOiAiSm9obiBEb2UiLCAic3ViIjogInN1YiJ9.signature", - realm: "microsoft", + realm: "utid", }; export const mockRefreshTokenEntity = { @@ -66,11 +66,12 @@ export const mockRefreshTokenEntityWithFamilyId = { export const mockAccountEntity = { homeAccountId: "uid.utid", environment: "login.microsoftonline.com", - realm: "microsoft", + realm: "utid", localAccountId: "object1234", username: "John Doe", authorityType: "MSSTS", clientInfo: "eyJ1aWQiOiJ1aWQiLCAidXRpZCI6InV0aWQifQ==", + tenants: ["utid"], }; export const mockAppMetaDataEntity = { diff --git a/lib/msal-node/test/cache/serializer/Deserializer.spec.ts b/lib/msal-node/test/cache/serializer/Deserializer.spec.ts index 66feb0c366..16439ed450 100644 --- a/lib/msal-node/test/cache/serializer/Deserializer.spec.ts +++ b/lib/msal-node/test/cache/serializer/Deserializer.spec.ts @@ -13,14 +13,15 @@ describe("Deserializer test cases", () => { test("deserializeJSONBlob", () => { const mockAccount = { - "uid.utid-login.microsoftonline.com-microsoft": { + "uid.utid-login.microsoftonline.com-utid": { username: "John Doe", local_account_id: "object1234", - realm: "microsoft", + realm: "utid", environment: "login.microsoftonline.com", home_account_id: "uid.utid", authority_type: "MSSTS", client_info: "eyJ1aWQiOiJ1aWQiLCAidXRpZCI6InV0aWQifQ==", + tenants: ["utid"], }, }; const acc = Deserializer.deserializeJSONBlob(cache); diff --git a/lib/msal-node/test/cache/serializer/cache.json b/lib/msal-node/test/cache/serializer/cache.json index b9f1e2475f..17ae3cbe49 100644 --- a/lib/msal-node/test/cache/serializer/cache.json +++ b/lib/msal-node/test/cache/serializer/cache.json @@ -1,13 +1,14 @@ { "Account": { - "uid.utid-login.microsoftonline.com-microsoft": { + "uid.utid-login.microsoftonline.com-utid": { "username": "John Doe", "local_account_id": "object1234", - "realm": "microsoft", + "realm": "utid", "environment": "login.microsoftonline.com", "home_account_id": "uid.utid", "authority_type": "MSSTS", - "client_info": "eyJ1aWQiOiJ1aWQiLCAidXRpZCI6InV0aWQifQ==" + "client_info": "eyJ1aWQiOiJ1aWQiLCAidXRpZCI6InV0aWQifQ==", + "tenants": ["utid"] } }, "RefreshToken": { @@ -28,11 +29,11 @@ } }, "AccessToken": { - "uid.utid-login.microsoftonline.com-accesstoken-mock_client_id-microsoft-scope1 scope2 scope3--": { + "uid.utid-login.microsoftonline.com-accesstoken-mock_client_id-utid-scope1 scope2 scope3--": { "environment": "login.microsoftonline.com", "credential_type": "AccessToken", "secret": "an access token", - "realm": "microsoft", + "realm": "utid", "target": "scope1 scope2 scope3", "client_id": "mock_client_id", "cached_at": "1000", @@ -41,11 +42,11 @@ "expires_on": "4600", "userAssertionHash": "mock_hash_string" }, - "uid.utid-login.microsoftonline.com-accesstoken-mock_client_id-microsoft-scope4 scope5--": { + "uid.utid-login.microsoftonline.com-accesstoken-mock_client_id-utid-scope4 scope5--": { "environment": "login.microsoftonline.com", "credential_type": "AccessToken", "secret": "an access token", - "realm": "microsoft", + "realm": "utid", "target": "scope4 scope5", "client_id": "mock_client_id", "cached_at": "1000", @@ -53,11 +54,11 @@ "extended_expires_on": "4600", "expires_on": "4600" }, - "uid.utid-login.microsoftonline.com-accesstoken_with_authscheme-mock_client_id-microsoft-scope4 scope5--pop": { + "uid.utid-login.microsoftonline.com-accesstoken_with_authscheme-mock_client_id-utid-scope4 scope5--pop": { "environment": "login.microsoftonline.com", "credential_type": "AccessToken_With_AuthScheme", "secret": "a POP-protected access token", - "realm": "microsoft", + "realm": "utid", "target": "scope4 scope5", "client_id": "mock_client_id", "cached_at": "1000", @@ -69,8 +70,8 @@ } }, "IdToken": { - "uid.utid-login.microsoftonline.com-idtoken-mock_client_id-microsoft---": { - "realm": "microsoft", + "uid.utid-login.microsoftonline.com-idtoken-mock_client_id-utid---": { + "realm": "utid", "environment": "login.microsoftonline.com", "credential_type": "IdToken", "secret": "header.eyJvaWQiOiAib2JqZWN0MTIzNCIsICJwcmVmZXJyZWRfdXNlcm5hbWUiOiAiSm9obiBEb2UiLCAic3ViIjogInN1YiJ9.signature", From ad67c662f7dcd48f00606f1febc8cec72a1b92c6 Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Thu, 12 Oct 2023 12:38:25 -0700 Subject: [PATCH 29/91] Factor out getTenantFromAuthorityString into an exported function --- lib/msal-common/src/authority/Authority.ts | 26 +++++++++++----------- lib/msal-common/src/cache/CacheManager.ts | 6 ++--- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/lib/msal-common/src/authority/Authority.ts b/lib/msal-common/src/authority/Authority.ts index 0bab950462..d20e1bbf59 100644 --- a/lib/msal-common/src/authority/Authority.ts +++ b/lib/msal-common/src/authority/Authority.ts @@ -1252,22 +1252,22 @@ export class Authority { return ciamAuthority; } +} +/** + * Extract tenantId from authority + */ +export function getTenantFromAuthorityString( + authority: string +): string | undefined { + const authorityUrl = new UrlString(authority); + const authorityUrlComponents = authorityUrl.getUrlComponents(); /** - * Extract tenantId from authority + * Tenant ID is the path after the domain: + * AAD Authority - domain/tenantId + * B2C Authority - domain/tenantId?/tfp?/policy */ - static getTenantFromAuthorityString(authority: string): string | undefined { - const authorityUrl = new UrlString(authority); - const authorityUrlComponents = authorityUrl.getUrlComponents(); - /** - * Tenant ID is the path after the domain: - * AAD Authority - domain/tenantId - * B2C Authority - domain/tenantId?/tfp?/policy - */ - return authorityUrlComponents.PathSegments.join( - Constants.FORWARD_SLASH - ); - } + return authorityUrlComponents.PathSegments.join(Constants.FORWARD_SLASH); } export function formatAuthorityUri(authorityUri: string): string { diff --git a/lib/msal-common/src/cache/CacheManager.ts b/lib/msal-common/src/cache/CacheManager.ts index 93e30c2f3c..d8739af266 100644 --- a/lib/msal-common/src/cache/CacheManager.ts +++ b/lib/msal-common/src/cache/CacheManager.ts @@ -43,7 +43,7 @@ import { BaseAuthRequest } from "../request/BaseAuthRequest"; import { Logger } from "../logger/Logger"; import { name, version } from "../packageMetadata"; import { StoreInCache } from "../request/StoreInCache"; -import { Authority } from "../authority/Authority"; +import { getTenantFromAuthorityString } from "../authority/Authority"; import { getAliasesFromConfigMetadata, getHardcodedAliasesForCanonicalAuthority, @@ -923,9 +923,7 @@ export abstract class CacheManager implements ICacheManager { request: BaseAuthRequest, environment: string ): CacheRecord { - const requestTenantId = Authority.getTenantFromAuthorityString( - request.authority - ); + const requestTenantId = getTenantFromAuthorityString(request.authority); const tokenKeys = this.getTokenKeys(); const cachedAccount = this.readAccountFromCache(account); const cachedIdToken = this.getIdToken( From 6f504053d7506db25fc30f5211d9ccdb21b9aeae Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Thu, 12 Oct 2023 12:43:38 -0700 Subject: [PATCH 30/91] Fix mock client info parsing in node ClientTestUtils --- lib/msal-node/test/client/ClientTestUtils.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/msal-node/test/client/ClientTestUtils.ts b/lib/msal-node/test/client/ClientTestUtils.ts index ef0784d8da..cef23b03bd 100644 --- a/lib/msal-node/test/client/ClientTestUtils.ts +++ b/lib/msal-node/test/client/ClientTestUtils.ts @@ -35,6 +35,7 @@ import { RANDOM_TEST_GUID, TEST_CONFIG, TEST_CRYPTO_VALUES, + TEST_DATA_CLIENT_INFO, TEST_POP_VALUES, TEST_TOKENS, } from "../test_kit/StringConstants"; @@ -216,6 +217,8 @@ export const mockCrypto = { return TEST_POP_VALUES.DECODED_REQ_CNF; case TEST_TOKENS.POP_TOKEN_PAYLOAD: return TEST_TOKENS.DECODED_POP_TOKEN_PAYLOAD; + case TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO: + return TEST_DATA_CLIENT_INFO.TEST_DECODED_CLIENT_INFO; default: return input; } From 8273740737959e04e694f518b376710bcbc538ea Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Thu, 12 Oct 2023 12:53:32 -0700 Subject: [PATCH 31/91] Update node storage tests with new test cache keys --- lib/msal-node/test/cache/Storage.spec.ts | 31 ++++++++++-------------- 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/lib/msal-node/test/cache/Storage.spec.ts b/lib/msal-node/test/cache/Storage.spec.ts index db870363ed..a017076d90 100644 --- a/lib/msal-node/test/cache/Storage.spec.ts +++ b/lib/msal-node/test/cache/Storage.spec.ts @@ -34,6 +34,7 @@ describe("Storage tests for msal-node: ", () => { }; let logger: Logger; + const ACCOUNT_KEY = "uid.utid-login.microsoftonline.com-utid"; beforeEach(() => { const cache = JSON.stringify(cacheJson); @@ -92,16 +93,15 @@ describe("Storage tests for msal-node: ", () => { nodeStorage.setInMemoryCache(inMemoryCache); const cache = nodeStorage.getCache(); - const accountKey = "uid.utid-login.microsoftonline.com-microsoft"; - const account: AccountEntity = cache[accountKey] as AccountEntity; + const account: AccountEntity = cache[ACCOUNT_KEY] as AccountEntity; expect(account).toBeInstanceOf(AccountEntity); expect(account.clientInfo).toBe( "eyJ1aWQiOiJ1aWQiLCAidXRpZCI6InV0aWQifQ==" ); const newInMemoryCache = nodeStorage.getInMemoryCache(); - expect(newInMemoryCache.accounts[accountKey]).toEqual( - cache[accountKey] + expect(newInMemoryCache.accounts[ACCOUNT_KEY]).toEqual( + cache[ACCOUNT_KEY] ); }); @@ -142,14 +142,13 @@ describe("Storage tests for msal-node: ", () => { DEFAULT_CRYPTO_IMPLEMENTATION ); nodeStorage.setInMemoryCache(inMemoryCache); - const accountKey = "uid.utid-login.microsoftonline.com-microsoft"; - const fetchedAccount = nodeStorage.getAccount(accountKey); + const fetchedAccount = nodeStorage.getAccount(ACCOUNT_KEY); const invalidAccountKey = "uid.utid-login.microsoftonline.com-invalid"; const invalidAccount = nodeStorage.getAccount(invalidAccountKey); expect(fetchedAccount).toBeInstanceOf(AccountEntity); - expect(fetchedAccount).toEqual(inMemoryCache.accounts[accountKey]); + expect(fetchedAccount).toEqual(inMemoryCache.accounts[ACCOUNT_KEY]); expect(invalidAccount).toBeNull(); const mockAccountData = { @@ -330,8 +329,7 @@ describe("Storage tests for msal-node: ", () => { ); nodeStorage.setInMemoryCache(inMemoryCache); - const accountKey = "uid.utid-login.microsoftonline.com-microsoft"; - expect(nodeStorage.containsKey(accountKey)).toBeTruthy; + expect(nodeStorage.containsKey(ACCOUNT_KEY)).toBeTruthy(); }); it("getKeys() tests - tests for an accountKey", () => { @@ -342,8 +340,8 @@ describe("Storage tests for msal-node: ", () => { ); nodeStorage.setInMemoryCache(inMemoryCache); - const accountKey = "uid.utid-login.microsoftonline.com-microsoft"; - expect(nodeStorage.getKeys().includes(accountKey)).toBeTruthy; + console.log(nodeStorage.getKeys().includes(ACCOUNT_KEY)); + expect(nodeStorage.getKeys().includes(ACCOUNT_KEY)).toBeTruthy(); }); it("removeItem() tests - removes an account", () => { @@ -354,14 +352,13 @@ describe("Storage tests for msal-node: ", () => { ); nodeStorage.setInMemoryCache(inMemoryCache); - const accountKey = "uid.utid-login.microsoftonline.com-microsoft"; const newInMemoryCache = nodeStorage.getInMemoryCache(); - expect(newInMemoryCache.accounts[accountKey]).toBeInstanceOf( + expect(newInMemoryCache.accounts[ACCOUNT_KEY]).toBeInstanceOf( AccountEntity ); - nodeStorage.removeItem(accountKey); - expect(newInMemoryCache.accounts[accountKey]).toBeUndefined; + nodeStorage.removeItem(ACCOUNT_KEY); + expect(newInMemoryCache.accounts[ACCOUNT_KEY]).toBeUndefined; }); it("should remove all keys from the cache when clear() is called", () => { @@ -372,11 +369,9 @@ describe("Storage tests for msal-node: ", () => { ); nodeStorage.setInMemoryCache(inMemoryCache); - const accountKey = "uid.utid-login.microsoftonline.com-microsoft"; - nodeStorage.clear(); - expect(nodeStorage.getAccount(accountKey)).toBeNull(); + expect(nodeStorage.getAccount(ACCOUNT_KEY)).toBeNull(); const newInMemoryCache = nodeStorage.getInMemoryCache(); Object.values(newInMemoryCache).forEach((cacheSection) => { From 223cab9ed9c8411833235e8790c31b9a04b8c8f3 Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Thu, 12 Oct 2023 13:07:08 -0700 Subject: [PATCH 32/91] Update Node PCA tests --- lib/msal-common/src/cache/entities/AccountEntity.ts | 3 +++ lib/msal-common/src/response/ResponseHandler.ts | 2 ++ .../test/client/PublicClientApplication.spec.ts | 13 ++----------- lib/msal-node/test/utils/TestConstants.ts | 1 + 4 files changed, 8 insertions(+), 11 deletions(-) diff --git a/lib/msal-common/src/cache/entities/AccountEntity.ts b/lib/msal-common/src/cache/entities/AccountEntity.ts index 24ecfef5c9..fa610cd60b 100644 --- a/lib/msal-common/src/cache/entities/AccountEntity.ts +++ b/lib/msal-common/src/cache/entities/AccountEntity.ts @@ -133,6 +133,7 @@ export class AccountEntity { msGraphHost?: string; environment?: string; nativeAccountId?: string; + tenants?: string[]; }, authority: Authority, cryptoObj?: ICrypto @@ -181,6 +182,8 @@ export class AccountEntity { accountDetails.idTokenClaims.tid || Constants.EMPTY_STRING; + account.tenants = accountDetails.tenants || []; + // How do you account for MSA CID here? account.localAccountId = clientInfo?.uid || diff --git a/lib/msal-common/src/response/ResponseHandler.ts b/lib/msal-common/src/response/ResponseHandler.ts index fe7d235f38..da7ae1dd65 100644 --- a/lib/msal-common/src/response/ResponseHandler.ts +++ b/lib/msal-common/src/response/ResponseHandler.ts @@ -545,6 +545,7 @@ export class ResponseHandler { clientInfo: serverTokenResponse.client_info, cloudGraphHostName: authCodePayload?.cloud_graph_host_name, msGraphHost: authCodePayload?.msgraph_host, + tenants: [responseTenantId], }, authority, this.cryptoObj @@ -555,6 +556,7 @@ export class ResponseHandler { tenants.push(responseTenantId); } baseAccount.tenants = tenants; + return baseAccount; } diff --git a/lib/msal-node/test/client/PublicClientApplication.spec.ts b/lib/msal-node/test/client/PublicClientApplication.spec.ts index 7282bf43e5..6496267fa6 100644 --- a/lib/msal-node/test/client/PublicClientApplication.spec.ts +++ b/lib/msal-node/test/client/PublicClientApplication.spec.ts @@ -817,17 +817,8 @@ describe("PublicClientApplication", () => { ...appConfig, }); - const cryptoProvider = new CryptoProvider(); - const accountEntity: AccountEntity = AccountEntity.createAccount( - { - homeAccountId: mockAccountInfo.homeAccountId, - idTokenClaims: AuthToken.extractTokenClaims( - mockAuthenticationResult.idToken, - cryptoProvider.base64Decode - ), - }, - fakeAuthority - ); + const accountEntity: AccountEntity = + AccountEntity.createFromAccountInfo(mockAccountInfo); // @ts-ignore authApp.storage.setAccount(accountEntity); diff --git a/lib/msal-node/test/utils/TestConstants.ts b/lib/msal-node/test/utils/TestConstants.ts index ad618e340f..eed68e1f73 100644 --- a/lib/msal-node/test/utils/TestConstants.ts +++ b/lib/msal-node/test/utils/TestConstants.ts @@ -233,6 +233,7 @@ export const mockAccountInfo: AccountInfo = { idToken: TEST_CONSTANTS.ID_TOKEN, name: ID_TOKEN_CLAIMS.name, nativeAccountId: undefined, + tenants: [ID_TOKEN_CLAIMS.tid], }; export const mockNativeAccountInfo: AccountInfo = { From 835e32fd7f634bed72fe608d3923a104fff16175 Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Thu, 12 Oct 2023 15:21:51 -0700 Subject: [PATCH 33/91] Remove idTokenClaims from AccountEntity since ID token claims are not cached --- .../NativeInteractionClient.ts | 13 +++--- .../test/app/PublicClientApplication.spec.ts | 1 - .../test/event/EventHandler.spec.ts | 42 ++++++------------- .../interaction_client/PopupClient.spec.ts | 2 - .../interaction_client/RedirectClient.spec.ts | 2 - lib/msal-common/src/account/AccountInfo.ts | 32 ++++++++++++++ lib/msal-common/src/cache/CacheManager.ts | 13 ------ .../src/cache/entities/AccountEntity.ts | 12 ------ lib/msal-common/src/index.ts | 6 ++- .../src/response/ResponseHandler.ts | 14 +++---- 10 files changed, 60 insertions(+), 77 deletions(-) diff --git a/lib/msal-browser/src/interaction_client/NativeInteractionClient.ts b/lib/msal-browser/src/interaction_client/NativeInteractionClient.ts index bae9289f37..cbc58c612c 100644 --- a/lib/msal-browser/src/interaction_client/NativeInteractionClient.ts +++ b/lib/msal-browser/src/interaction_client/NativeInteractionClient.ts @@ -33,6 +33,7 @@ import { invokeAsync, createAuthError, AuthErrorCodes, + updateTenantProfile, } from "@azure/msal-common"; import { BaseInteractionClient } from "./BaseInteractionClient"; import { BrowserConfiguration } from "../config/Configuration"; @@ -582,14 +583,10 @@ export class NativeInteractionClient extends BaseInteractionClient { idTokenClaims.tid || Constants.EMPTY_STRING; - const fullAccountEntity: AccountEntity = idTokenClaims - ? Object.assign(new AccountEntity(), { - ...accountEntity, - idTokenClaims: idTokenClaims, - }) - : accountEntity; - - const accountInfo = fullAccountEntity.getAccountInfo(); + const accountInfo: AccountInfo | null = updateTenantProfile( + accountEntity.getAccountInfo(), + idTokenClaims + ); // generate PoP token as needed const responseAccessToken = await this.generatePopAccessToken( diff --git a/lib/msal-browser/test/app/PublicClientApplication.spec.ts b/lib/msal-browser/test/app/PublicClientApplication.spec.ts index 8d07805f4f..2f360ae2b4 100644 --- a/lib/msal-browser/test/app/PublicClientApplication.spec.ts +++ b/lib/msal-browser/test/app/PublicClientApplication.spec.ts @@ -5348,7 +5348,6 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { testAccount2.authorityType = "MSSTS"; testAccount2.clientInfo = TEST_DATA_CLIENT_INFO.TEST_CLIENT_INFO_B64ENCODED; - testAccount2.idTokenClaims = ID_TOKEN_CLAIMS; const idTokenData2 = { realm: testAccountInfo2.tenantId, diff --git a/lib/msal-browser/test/event/EventHandler.spec.ts b/lib/msal-browser/test/event/EventHandler.spec.ts index 5c9cd773fc..d16a6e5562 100644 --- a/lib/msal-browser/test/event/EventHandler.spec.ts +++ b/lib/msal-browser/test/event/EventHandler.spec.ts @@ -117,7 +117,7 @@ describe("Event API tests", () => { const subscriber = (message: EventMessage) => { expect(message.eventType).toEqual(EventType.ACCOUNT_ADDED); expect(message.interactionType).toBeNull(); - expect(message.payload).toEqual(account); + expect(message.payload).toEqual(accountEntity.getAccountInfo()); expect(message.error).toBeNull(); expect(message.timestamp).not.toBeNull(); done(); @@ -126,29 +126,21 @@ describe("Event API tests", () => { const eventHandler = new EventHandler(logger, browserCrypto); eventHandler.addEventCallback(subscriber); - const accountEntity = { + const account: AccountInfo = { + authorityType: "MSSTS", homeAccountId: "test-home-accountId-1", localAccountId: "test-local-accountId-1", username: "user-1@example.com", environment: "test-environment-1", - realm: "test-tenantId-1", + tenantId: "test-tenantId-1", name: "name-1", idTokenClaims: {}, - authorityType: "MSSTS", - }; - - const account: AccountInfo = { - authorityType: "MSSTS", - homeAccountId: accountEntity.homeAccountId, - localAccountId: accountEntity.localAccountId, - username: accountEntity.username, - environment: accountEntity.environment, - tenantId: accountEntity.realm, - name: accountEntity.name, - idTokenClaims: accountEntity.idTokenClaims, nativeAccountId: undefined, + tenants: ["test-tenantId-1"], }; + const accountEntity = AccountEntity.createFromAccountInfo(account); + const cacheKey1 = AccountEntity.generateAccountCacheKey(account); // @ts-ignore @@ -163,7 +155,7 @@ describe("Event API tests", () => { const subscriber = (message: EventMessage) => { expect(message.eventType).toEqual(EventType.ACCOUNT_REMOVED); expect(message.interactionType).toBeNull(); - expect(message.payload).toEqual(account); + expect(message.payload).toEqual(accountEntity.getAccountInfo()); expect(message.error).toBeNull(); expect(message.timestamp).not.toBeNull(); done(); @@ -172,29 +164,21 @@ describe("Event API tests", () => { const eventHandler = new EventHandler(logger, browserCrypto); eventHandler.addEventCallback(subscriber); - const accountEntity = { + const account: AccountInfo = { homeAccountId: "test-home-accountId-1", localAccountId: "test-local-accountId-1", username: "user-1@example.com", environment: "test-environment-1", - realm: "test-tenantId-1", + tenantId: "test-tenantId-1", name: "name-1", idTokenClaims: {}, authorityType: "MSSTS", - }; - - const account: AccountInfo = { - authorityType: "MSSTS", - homeAccountId: accountEntity.homeAccountId, - localAccountId: accountEntity.localAccountId, - username: accountEntity.username, - environment: accountEntity.environment, - tenantId: accountEntity.realm, - name: accountEntity.name, - idTokenClaims: accountEntity.idTokenClaims, nativeAccountId: undefined, + tenants: ["test-tenantId-1"], }; + const accountEntity = AccountEntity.createFromAccountInfo(account); + const cacheKey1 = AccountEntity.generateAccountCacheKey(account); // @ts-ignore diff --git a/lib/msal-browser/test/interaction_client/PopupClient.spec.ts b/lib/msal-browser/test/interaction_client/PopupClient.spec.ts index 2a16b18de8..27ff452537 100644 --- a/lib/msal-browser/test/interaction_client/PopupClient.spec.ts +++ b/lib/msal-browser/test/interaction_client/PopupClient.spec.ts @@ -1141,7 +1141,6 @@ describe("PopupClient", () => { testAccount.authorityType = "MSSTS"; testAccount.clientInfo = TEST_DATA_CLIENT_INFO.TEST_CLIENT_INFO_B64ENCODED; - testAccount.idTokenClaims = testIdTokenClaims; // @ts-ignore pca.browserStorage.setAccount(testAccount); @@ -1228,7 +1227,6 @@ describe("PopupClient", () => { testAccount.authorityType = "MSSTS"; testAccount.clientInfo = TEST_DATA_CLIENT_INFO.TEST_CLIENT_INFO_B64ENCODED; - testAccount.idTokenClaims = testIdTokenClaims; // @ts-ignore pca.browserStorage.setAccount(testAccount); diff --git a/lib/msal-browser/test/interaction_client/RedirectClient.spec.ts b/lib/msal-browser/test/interaction_client/RedirectClient.spec.ts index 5bd0adc335..540e6148cb 100644 --- a/lib/msal-browser/test/interaction_client/RedirectClient.spec.ts +++ b/lib/msal-browser/test/interaction_client/RedirectClient.spec.ts @@ -3302,7 +3302,6 @@ describe("RedirectClient", () => { testAccount.authorityType = "MSSTS"; testAccount.clientInfo = TEST_DATA_CLIENT_INFO.TEST_CLIENT_INFO_B64ENCODED; - testAccount.idTokenClaims = testIdTokenClaims; browserStorage.setAccount(testAccount); @@ -3357,7 +3356,6 @@ describe("RedirectClient", () => { testAccount.authorityType = "MSSTS"; testAccount.clientInfo = TEST_DATA_CLIENT_INFO.TEST_CLIENT_INFO_B64ENCODED; - testAccount.idTokenClaims = testIdTokenClaims; browserStorage.setAccount(testAccount); diff --git a/lib/msal-common/src/account/AccountInfo.ts b/lib/msal-common/src/account/AccountInfo.ts index b35ff5177f..6cde693a76 100644 --- a/lib/msal-common/src/account/AccountInfo.ts +++ b/lib/msal-common/src/account/AccountInfo.ts @@ -44,3 +44,35 @@ export type ActiveAccountFilters = { localAccountId: string; tenantId: string; }; + +/** + * Replaces account info that varies by tenant profile sourced from the ID token claims passed in with the tenant-specific account info + * @param accountInfo + * @param idTokenClaims + * @returns + */ +export function updateTenantProfile( + accountInfo: AccountInfo, + idTokenClaims?: TokenClaims +): AccountInfo { + if (idTokenClaims) { + const updatedAccountInfo = accountInfo; + const { oid, tid, name } = idTokenClaims; + + if (oid && oid !== accountInfo.localAccountId) { + updatedAccountInfo.localAccountId = oid; + } + if (tid && tid !== accountInfo.tenantId) { + updatedAccountInfo.tenantId = tid; + } + if (name && name !== accountInfo.name) { + updatedAccountInfo.name = name; + } + + updatedAccountInfo.idTokenClaims = idTokenClaims; + + return updatedAccountInfo; + } + + return accountInfo; +} diff --git a/lib/msal-common/src/cache/CacheManager.ts b/lib/msal-common/src/cache/CacheManager.ts index d8739af266..67f456ca60 100644 --- a/lib/msal-common/src/cache/CacheManager.ts +++ b/lib/msal-common/src/cache/CacheManager.ts @@ -944,19 +944,6 @@ export abstract class CacheManager implements ICacheManager { ); const cachedAppMetadata = this.readAppMetadataFromCache(environment); - if (cachedAccount && cachedIdToken) { - const idTokenClaims = extractTokenClaims( - cachedIdToken.secret, - this.cryptoImpl.base64Decode - ); - - cachedAccount.idTokenClaims = idTokenClaims; - cachedAccount.localAccountId = - idTokenClaims.oid || cachedAccount.localAccountId; - cachedAccount.name = idTokenClaims.name || cachedAccount.name; - cachedAccount.realm = idTokenClaims.tid || cachedAccount.realm; - } - return { account: cachedAccount, idToken: cachedIdToken, diff --git a/lib/msal-common/src/cache/entities/AccountEntity.ts b/lib/msal-common/src/cache/entities/AccountEntity.ts index fa610cd60b..d369d5d252 100644 --- a/lib/msal-common/src/cache/entities/AccountEntity.ts +++ b/lib/msal-common/src/cache/entities/AccountEntity.ts @@ -35,7 +35,6 @@ import { ProtocolMode } from "../../authority/ProtocolMode"; * name: Full name for the account, including given name and family name, * lastModificationTime: last time this entity was modified in the cache * lastModificationApp: - * idTokenClaims: Object containing claims parsed from ID token * nativeAccountId: Account identifier on the native device * } * @internal @@ -53,7 +52,6 @@ export class AccountEntity { lastModificationApp?: string; cloudGraphHostName?: string; msGraphHost?: string; - idTokenClaims?: TokenClaims; nativeAccountId?: string; tenants?: string[]; @@ -89,22 +87,12 @@ export class AccountEntity { username: this.username, localAccountId: this.localAccountId, name: this.name, - idTokenClaims: this.idTokenClaims, nativeAccountId: this.nativeAccountId, authorityType: this.authorityType, tenants: this.tenants, }; } - /** - * Update attributes that are sourced from ID token claims and vary by tenant profile - */ - updateTenantProfile(idTokenClaims: TokenClaims): void { - this.idTokenClaims = idTokenClaims; - this.realm = idTokenClaims.tid || Constants.EMPTY_STRING; - this.name = idTokenClaims.name || Constants.EMPTY_STRING; - } - /** * Generates account key from interface * @param accountInterface diff --git a/lib/msal-common/src/index.ts b/lib/msal-common/src/index.ts index fb53df6c69..c4b13d2ebb 100644 --- a/lib/msal-common/src/index.ts +++ b/lib/msal-common/src/index.ts @@ -28,7 +28,11 @@ export { } from "./config/AppTokenProvider"; export { ClientConfiguration } from "./config/ClientConfiguration"; // Account -export { AccountInfo, ActiveAccountFilters } from "./account/AccountInfo"; +export { + AccountInfo, + ActiveAccountFilters, + updateTenantProfile, +} from "./account/AccountInfo"; export * as AuthToken from "./account/AuthToken"; export { TokenClaims } from "./account/TokenClaims"; export { TokenClaims as IdTokenClaims } from "./account/TokenClaims"; diff --git a/lib/msal-common/src/response/ResponseHandler.ts b/lib/msal-common/src/response/ResponseHandler.ts index da7ae1dd65..4a26570cd8 100644 --- a/lib/msal-common/src/response/ResponseHandler.ts +++ b/lib/msal-common/src/response/ResponseHandler.ts @@ -44,7 +44,7 @@ import { IPerformanceClient } from "../telemetry/performance/IPerformanceClient" import { PerformanceEvents } from "../telemetry/performance/PerformanceEvent"; import { checkMaxAge, extractTokenClaims } from "../account/AuthToken"; import { TokenClaims } from "../account/TokenClaims"; -import { AccountInfo } from "../account/AccountInfo"; +import { AccountInfo, updateTenantProfile } from "../account/AccountInfo"; /** * Class that handles response parsing. @@ -626,10 +626,6 @@ export class ResponseHandler { } } - if (cacheRecord.account && idTokenClaims) { - cacheRecord.account.updateTenantProfile(idTokenClaims); - } - if (cacheRecord.appMetadata) { familyId = cacheRecord.appMetadata.familyId === THE_FAMILY_ID @@ -646,10 +642,10 @@ export class ResponseHandler { } const accountInfo: AccountInfo | null = cacheRecord.account - ? { - ...cacheRecord.account.getAccountInfo(), - idTokenClaims, - } + ? updateTenantProfile( + cacheRecord.account.getAccountInfo(), + idTokenClaims + ) : null; return { From b81b8186a37f61e34e058982d5e60f55ebdec8ec Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Thu, 12 Oct 2023 15:44:11 -0700 Subject: [PATCH 34/91] Make tenantId optional in active account filters --- lib/msal-common/src/account/AccountInfo.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/msal-common/src/account/AccountInfo.ts b/lib/msal-common/src/account/AccountInfo.ts index 6cde693a76..069e17a90c 100644 --- a/lib/msal-common/src/account/AccountInfo.ts +++ b/lib/msal-common/src/account/AccountInfo.ts @@ -42,7 +42,7 @@ export type AccountInfo = { export type ActiveAccountFilters = { homeAccountId: string; localAccountId: string; - tenantId: string; + tenantId?: string; }; /** From b3ded6f055c6c332cb356377e4208ec140748da7 Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Thu, 12 Oct 2023 17:14:03 -0700 Subject: [PATCH 35/91] Add account cache update from single tenant to multi-tenant --- .../src/cache/BrowserCacheManager.ts | 28 ++++++++++++++++++- .../src/cache/entities/AccountEntity.ts | 7 +++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/lib/msal-browser/src/cache/BrowserCacheManager.ts b/lib/msal-browser/src/cache/BrowserCacheManager.ts index 6f09bcfb1c..2f6d13ea18 100644 --- a/lib/msal-browser/src/cache/BrowserCacheManager.ts +++ b/lib/msal-browser/src/cache/BrowserCacheManager.ts @@ -421,10 +421,36 @@ export class BrowserCacheManager extends CacheManager { return null; } - return CacheManager.toObject( + const accountEntity = CacheManager.toObject( new AccountEntity(), parsedAccount ); + + return accountEntity.isSingleTenant() + ? this.updateOutdatedCachedAccount(accountKey, accountEntity) + : accountEntity; + } + + /** + * Replaces the single-tenant account with a new account object that contains the array of tenants. + * Returns the updated account entity. + * @param accountKey + * @param accountEntity + * @returns + */ + private updateOutdatedCachedAccount( + accountKey: string, + accountEntity: AccountEntity + ): AccountEntity { + const updatedAccount = Object.assign(new AccountEntity(), { + ...accountEntity, + tenants: [accountEntity.realm], + }); + this.removeItem(accountKey); + this.removeAccountKeyFromMap(accountKey); + this.setAccount(updatedAccount); + this.logger.verbose("Updated an outdated account entity in the cache"); + return updatedAccount; } /** diff --git a/lib/msal-common/src/cache/entities/AccountEntity.ts b/lib/msal-common/src/cache/entities/AccountEntity.ts index d369d5d252..b1d13c7d17 100644 --- a/lib/msal-common/src/cache/entities/AccountEntity.ts +++ b/lib/msal-common/src/cache/entities/AccountEntity.ts @@ -93,6 +93,13 @@ export class AccountEntity { }; } + /** + * Returns true if the account entity is in single tenant format (outdated), false otherwise + */ + isSingleTenant(): boolean { + return !this.tenants; + } + /** * Generates account key from interface * @param accountInterface From 140295e0087dfc5e93544deebd57707a77b9a419 Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Thu, 12 Oct 2023 18:22:46 -0700 Subject: [PATCH 36/91] Add outdated (single-tenant) account update logic when cached account is read from the cache --- .../src/cache/BrowserCacheManager.ts | 35 +++++---------- .../SilentCacheClient.spec.ts | 35 +++++++-------- lib/msal-common/src/cache/CacheManager.ts | 34 ++++++++++++++ .../test/client/ClientTestUtils.ts | 4 ++ lib/msal-node/src/cache/NodeStorage.ts | 17 +++++-- lib/msal-node/test/cache/Storage.spec.ts | 45 ++++++++++++++++++- 6 files changed, 120 insertions(+), 50 deletions(-) diff --git a/lib/msal-browser/src/cache/BrowserCacheManager.ts b/lib/msal-browser/src/cache/BrowserCacheManager.ts index 2f6d13ea18..7119112c63 100644 --- a/lib/msal-browser/src/cache/BrowserCacheManager.ts +++ b/lib/msal-browser/src/cache/BrowserCacheManager.ts @@ -426,31 +426,7 @@ export class BrowserCacheManager extends CacheManager { parsedAccount ); - return accountEntity.isSingleTenant() - ? this.updateOutdatedCachedAccount(accountKey, accountEntity) - : accountEntity; - } - - /** - * Replaces the single-tenant account with a new account object that contains the array of tenants. - * Returns the updated account entity. - * @param accountKey - * @param accountEntity - * @returns - */ - private updateOutdatedCachedAccount( - accountKey: string, - accountEntity: AccountEntity - ): AccountEntity { - const updatedAccount = Object.assign(new AccountEntity(), { - ...accountEntity, - tenants: [accountEntity.realm], - }); - this.removeItem(accountKey); - this.removeAccountKeyFromMap(accountKey); - this.setAccount(updatedAccount); - this.logger.verbose("Updated an outdated account entity in the cache"); - return updatedAccount; + return this.updateOutdatedCachedAccount(accountKey, accountEntity); } /** @@ -544,6 +520,15 @@ export class BrowserCacheManager extends CacheManager { this.removeAccountKeyFromMap(key); } + /** + * Remove account entity from the platform cache if it's outdated + * @param accountKey + */ + removeOutdatedAccount(accountKey: string): void { + this.removeItem(accountKey); + this.removeAccountKeyFromMap(accountKey); + } + /** * Removes given idToken from the cache and from the key map * @param key diff --git a/lib/msal-browser/test/interaction_client/SilentCacheClient.spec.ts b/lib/msal-browser/test/interaction_client/SilentCacheClient.spec.ts index 491f1d98d9..0b239d4996 100644 --- a/lib/msal-browser/test/interaction_client/SilentCacheClient.spec.ts +++ b/lib/msal-browser/test/interaction_client/SilentCacheClient.spec.ts @@ -25,14 +25,21 @@ import { AccountInfo, } from "@azure/msal-common"; -const testAccountEntity: AccountEntity = new AccountEntity(); -testAccountEntity.homeAccountId = `${ID_TOKEN_CLAIMS.oid}.${ID_TOKEN_CLAIMS.tid}`; -testAccountEntity.localAccountId = ID_TOKEN_CLAIMS.oid; -testAccountEntity.environment = "login.microsoftonline.com"; -testAccountEntity.realm = ID_TOKEN_CLAIMS.tid; -testAccountEntity.username = ID_TOKEN_CLAIMS.preferred_username; -testAccountEntity.name = ID_TOKEN_CLAIMS.name; -testAccountEntity.authorityType = "MSSTS"; +const testAccount: AccountInfo = { + homeAccountId: `${ID_TOKEN_CLAIMS.oid}.${ID_TOKEN_CLAIMS.tid}`, + environment: "login.microsoftonline.com", + tenantId: ID_TOKEN_CLAIMS.tid, + username: ID_TOKEN_CLAIMS.preferred_username, + localAccountId: ID_TOKEN_CLAIMS.oid, + idTokenClaims: ID_TOKEN_CLAIMS, + name: ID_TOKEN_CLAIMS.name, + authorityType: "MSSTS", + nativeAccountId: undefined, + tenants: [ID_TOKEN_CLAIMS.tid], +}; + +const testAccountEntity: AccountEntity = + AccountEntity.createFromAccountInfo(testAccount); const testIdToken: IdTokenEntity = new IdTokenEntity(); testIdToken.homeAccountId = `${ID_TOKEN_CLAIMS.oid}.${ID_TOKEN_CLAIMS.tid}`; @@ -62,18 +69,6 @@ testRefreshTokenEntity.realm = ID_TOKEN_CLAIMS.tid; testRefreshTokenEntity.secret = TEST_TOKENS.REFRESH_TOKEN; testRefreshTokenEntity.credentialType = CredentialType.REFRESH_TOKEN; -const testAccount: AccountInfo = { - homeAccountId: `${ID_TOKEN_CLAIMS.oid}.${ID_TOKEN_CLAIMS.tid}`, - environment: testAccountEntity.environment, - tenantId: ID_TOKEN_CLAIMS.tid, - username: ID_TOKEN_CLAIMS.preferred_username, - localAccountId: ID_TOKEN_CLAIMS.oid, - idTokenClaims: ID_TOKEN_CLAIMS, - name: ID_TOKEN_CLAIMS.name, - authorityType: "MSSTS", - nativeAccountId: undefined, -}; - describe("SilentCacheClient", () => { let silentCacheClient: SilentCacheClient; let pca: PublicClientApplication; diff --git a/lib/msal-common/src/cache/CacheManager.ts b/lib/msal-common/src/cache/CacheManager.ts index 67f456ca60..1329adbf36 100644 --- a/lib/msal-common/src/cache/CacheManager.ts +++ b/lib/msal-common/src/cache/CacheManager.ts @@ -86,6 +86,11 @@ export abstract class CacheManager implements ICacheManager { */ abstract setAccount(account: AccountEntity): void; + /** + * remove account entity from the platform cache if it's outdated + */ + abstract removeOutdatedAccount(accountKey: string): void; + /** * fetch the idToken entity from the platform cache * @param idTokenKey @@ -861,6 +866,32 @@ export abstract class CacheManager implements ICacheManager { await Promise.all(removedCredentials); } + /** + * Replaces the single-tenant account with a new account object that contains the array of tenants. + * Returns the updated account entity. + * @param accountKey + * @param accountEntity + * @returns + */ + protected updateOutdatedCachedAccount( + accountKey: string, + accountEntity: AccountEntity + ): AccountEntity { + if (accountEntity.isSingleTenant()) { + const updatedAccount = Object.assign(new AccountEntity(), { + ...accountEntity, + tenants: [accountEntity.realm], + }); + this.removeOutdatedAccount(accountKey); + this.setAccount(updatedAccount); + this.commonLogger.verbose( + "Updated an outdated account entity in the cache" + ); + return updatedAccount; + } + return accountEntity; + } + /** * returns a boolean if the given credential is removed * @param credential @@ -1756,4 +1787,7 @@ export class DefaultStorageClass extends CacheManager { updateCredentialCacheKey(): string { throw createClientAuthError(ClientAuthErrorCodes.methodNotImplemented); } + removeOutdatedAccount(): void { + throw createClientAuthError(ClientAuthErrorCodes.methodNotImplemented); + } } diff --git a/lib/msal-common/test/client/ClientTestUtils.ts b/lib/msal-common/test/client/ClientTestUtils.ts index 948d2c9eaf..a5ed1af842 100644 --- a/lib/msal-common/test/client/ClientTestUtils.ts +++ b/lib/msal-common/test/client/ClientTestUtils.ts @@ -70,6 +70,10 @@ export class MockStorageClass extends CacheManager { } } + removeOutdatedAccount(accountKey: string): void { + delete this.store[accountKey]; + } + getAccountKeys(): string[] { return this.store[ACCOUNT_KEYS] || []; } diff --git a/lib/msal-node/src/cache/NodeStorage.ts b/lib/msal-node/src/cache/NodeStorage.ts index 074918f35e..35ab4e8477 100644 --- a/lib/msal-node/src/cache/NodeStorage.ts +++ b/lib/msal-node/src/cache/NodeStorage.ts @@ -219,9 +219,12 @@ export class NodeStorage extends CacheManager { * @param accountKey - lookup key to fetch cache type AccountEntity */ getAccount(accountKey: string): AccountEntity | null { - const account = this.getItem(accountKey) as AccountEntity; - if (AccountEntity.isAccountEntity(account)) { - return account; + const account = this.getItem(accountKey); + + const accountEntity = Object.assign(new AccountEntity(), account); + + if (AccountEntity.isAccountEntity(accountEntity)) { + return this.updateOutdatedCachedAccount(accountKey, accountEntity); } return null; } @@ -456,6 +459,14 @@ export class NodeStorage extends CacheManager { return result; } + /** + * Remove account entity from the platform cache if it's outdated + * @param accountKey + */ + removeOutdatedAccount(accountKey: string): void { + this.removeItem(accountKey); + } + /** * Checks whether key is in cache. * @param key - look up key for a cache entity diff --git a/lib/msal-node/test/cache/Storage.spec.ts b/lib/msal-node/test/cache/Storage.spec.ts index a017076d90..f9a4307f4b 100644 --- a/lib/msal-node/test/cache/Storage.spec.ts +++ b/lib/msal-node/test/cache/Storage.spec.ts @@ -159,6 +159,7 @@ describe("Storage tests for msal-node: ", () => { homeAccountId: "uid1.utid1", authorityType: "MSSTS", clientInfo: "eyJ1aWQiOiJ1aWQxIiwgInV0aWQiOiJ1dGlkMSJ9", + tenants: ["samplerealm"], }; let mockAccountEntity = CacheManager.toObject( @@ -172,6 +173,48 @@ describe("Storage tests for msal-node: ", () => { ).toEqual(mockAccountEntity); }); + it("getAccount() updates an outdated (single-tenant) account cache entry", () => { + const nodeStorage = new NodeStorage( + logger, + clientId, + DEFAULT_CRYPTO_IMPLEMENTATION + ); + nodeStorage.setInMemoryCache(inMemoryCache); + const outdatedAccountKey = "uid.utid3-login.microsoftonline.com-utid3"; + + const outdatedAccountData = { + username: "Jane Doe", + localAccountId: "object5678", + realm: "utid3", + environment: "login.microsoftonline.com", + homeAccountId: "uid.utid3", + authorityType: "MSSTS", + clientInfo: "eyJ1aWQiOiJ1aWQxIiwgInV0aWQiOiJ1dGlkMSJ9", + }; + + let outdatedMockAccountEntity = CacheManager.toObject( + new AccountEntity(), + outdatedAccountData + ); + + let updatedMockAccountEntity = CacheManager.toObject( + new AccountEntity(), + { ...outdatedAccountData, tenants: ["utid3"] } + ); + const updatedAccountKey = updatedMockAccountEntity.generateAccountKey(); + expect(outdatedMockAccountEntity).toBeInstanceOf(AccountEntity); + // Set an outdated account + nodeStorage.setAccount(outdatedMockAccountEntity); + expect(nodeStorage.getItem(outdatedAccountKey)).toEqual( + outdatedAccountData + ); + + // Get account should update and return updated account + expect(nodeStorage.getAccount(updatedAccountKey)).toEqual( + updatedMockAccountEntity + ); + }); + it("setCache() and getCache() tests - tests for an accessToken", () => { const nodeStorage = new NodeStorage( logger, @@ -339,8 +382,6 @@ describe("Storage tests for msal-node: ", () => { DEFAULT_CRYPTO_IMPLEMENTATION ); nodeStorage.setInMemoryCache(inMemoryCache); - - console.log(nodeStorage.getKeys().includes(ACCOUNT_KEY)); expect(nodeStorage.getKeys().includes(ACCOUNT_KEY)).toBeTruthy(); }); From d508aa8bf60e9c5e790d3d663aa5911a078fbf81 Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Fri, 13 Oct 2023 12:18:31 -0700 Subject: [PATCH 37/91] Address PR feedback --- .../src/cache/entities/AccountEntity.ts | 25 ++++++------------- .../src/response/ResponseHandler.ts | 2 +- 2 files changed, 9 insertions(+), 18 deletions(-) diff --git a/lib/msal-common/src/cache/entities/AccountEntity.ts b/lib/msal-common/src/cache/entities/AccountEntity.ts index b1d13c7d17..efb3b4bdcb 100644 --- a/lib/msal-common/src/cache/entities/AccountEntity.ts +++ b/lib/msal-common/src/cache/entities/AccountEntity.ts @@ -108,8 +108,8 @@ export class AccountEntity { const homeTenantId = accountInterface.homeAccountId.split(".")[1]; const accountKey = [ accountInterface.homeAccountId, - accountInterface.environment || Constants.EMPTY_STRING, - homeTenantId || accountInterface.tenantId || Constants.EMPTY_STRING, + accountInterface.environment || "", + homeTenantId || accountInterface.tenantId || "", ]; return accountKey.join(Separators.CACHE_KEY_SEPARATOR).toLowerCase(); @@ -145,15 +145,8 @@ export class AccountEntity { let clientInfo: ClientInfo | undefined; - try { - if (accountDetails.clientInfo && cryptoObj) { - clientInfo = buildClientInfo( - accountDetails.clientInfo, - cryptoObj - ); - } - } catch (e) { - throw e; + if (accountDetails.clientInfo && cryptoObj) { + clientInfo = buildClientInfo(accountDetails.clientInfo, cryptoObj); } account.clientInfo = accountDetails.clientInfo; @@ -173,9 +166,7 @@ export class AccountEntity { account.environment = env; // non AAD scenarios can have empty realm account.realm = - clientInfo?.utid || - accountDetails.idTokenClaims.tid || - Constants.EMPTY_STRING; + clientInfo?.utid || accountDetails.idTokenClaims.tid || ""; account.tenants = accountDetails.tenants || []; @@ -184,7 +175,7 @@ export class AccountEntity { clientInfo?.uid || accountDetails.idTokenClaims.oid || accountDetails.idTokenClaims.sub || - Constants.EMPTY_STRING; + ""; /* * In B2C scenarios the emails claim is used instead of preferred_username and it is an array. @@ -198,7 +189,7 @@ export class AccountEntity { ? accountDetails.idTokenClaims.emails[0] : null; - account.username = preferredUsername || email || Constants.EMPTY_STRING; + account.username = preferredUsername || email || ""; account.name = accountDetails.idTokenClaims.name; account.cloudGraphHostName = accountDetails.cloudGraphHostName; @@ -275,7 +266,7 @@ export class AccountEntity { } // default to "sub" claim - return idTokenClaims?.sub ? idTokenClaims.sub : Constants.EMPTY_STRING; + return idTokenClaims?.sub || ""; } /** diff --git a/lib/msal-common/src/response/ResponseHandler.ts b/lib/msal-common/src/response/ResponseHandler.ts index 4a26570cd8..a899217b34 100644 --- a/lib/msal-common/src/response/ResponseHandler.ts +++ b/lib/msal-common/src/response/ResponseHandler.ts @@ -523,7 +523,7 @@ export class ResponseHandler { authCodePayload?: AuthorizationCodePayload ): AccountEntity | undefined { this.logger.verbose("setCachedAccount called"); - const responseTenantId = idTokenClaims.tid || Constants.EMPTY_STRING; + const responseTenantId = idTokenClaims.tid || ""; // Check if base account is already cached const accountKeys = this.cacheStorage.getAccountKeys(); From fc7b14d7c2e2421ed4492d59195e7f7f07a2f9bd Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Fri, 13 Oct 2023 13:00:54 -0700 Subject: [PATCH 38/91] Address PR feedback --- lib/msal-common/src/account/AccountInfo.ts | 1 - lib/msal-common/src/cache/CacheManager.ts | 29 ++++++++++++---------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/lib/msal-common/src/account/AccountInfo.ts b/lib/msal-common/src/account/AccountInfo.ts index 069e17a90c..ea62c62d60 100644 --- a/lib/msal-common/src/account/AccountInfo.ts +++ b/lib/msal-common/src/account/AccountInfo.ts @@ -70,7 +70,6 @@ export function updateTenantProfile( } updatedAccountInfo.idTokenClaims = idTokenClaims; - return updatedAccountInfo; } diff --git a/lib/msal-common/src/cache/CacheManager.ts b/lib/msal-common/src/cache/CacheManager.ts index 1329adbf36..5b32906ab5 100644 --- a/lib/msal-common/src/cache/CacheManager.ts +++ b/lib/msal-common/src/cache/CacheManager.ts @@ -32,7 +32,7 @@ import { createClientAuthError, ClientAuthErrorCodes, } from "../error/ClientAuthError"; -import { AccountInfo } from "../account/AccountInfo"; +import { AccountInfo, updateTenantProfile } from "../account/AccountInfo"; import { AppMetadataEntity } from "./entities/AppMetadataEntity"; import { ServerTelemetryEntity } from "./entities/ServerTelemetryEntity"; import { ThrottlingEntity } from "./entities/ThrottlingEntity"; @@ -325,10 +325,15 @@ export abstract class CacheManager implements ICacheManager { targetTenantId?: string, tenantProfileFilter?: TenantProfileFilter ): AccountInfo | null { + // At this point, accoutnInfo only contains base/home account data, so it's not a "real" account const accountInfo = accountEntity.getAccountInfo(); + /** + * An ID token matching the account entity and tenant ID must exist in the cache + * for the account to be a valid authenticated tenant profile. No matching ID token + * means the account being requested doesn't technically exist in the cache. + */ const idToken = this.getIdToken(accountInfo, undefined, targetTenantId); - if (idToken) { const idTokenClaims = extractTokenClaims( idToken.secret, @@ -341,17 +346,11 @@ export abstract class CacheManager implements ICacheManager { tenantProfileFilter ) ) { - return { - ...accountInfo, - idToken: idToken.secret, - idTokenClaims: idTokenClaims, - localAccountId: - idTokenClaims.oid || accountInfo.localAccountId, - name: idTokenClaims.name || accountInfo.name, - tenantId: idTokenClaims.tid || accountInfo.tenantId, - }; + return updateTenantProfile(accountInfo, idTokenClaims); } } + + // Account matching all filters doesn't techincally exist in the cache, return null return null; } @@ -482,7 +481,7 @@ export abstract class CacheManager implements ICacheManager { } /** - * Retrieve accounts matching all provided filters; if no filter is set, get all accounts + * Retrieve account entities matching all provided tenant-agnostic filters; if no filter is set, get all account entities in the cache * Not checking for casing as keys are all generated in lower case, remember to convert to lower case if object properties are compared * @param accountFilter - An object containing Account properties to filter by */ @@ -882,7 +881,11 @@ export abstract class CacheManager implements ICacheManager { ...accountEntity, tenants: [accountEntity.realm], }); - this.removeOutdatedAccount(accountKey); + + // If the cache keys don't match, the key is also outdated and should be removed, if they match it's fine to just override the value + if (updatedAccount.generateAccountKey() !== accountKey) { + this.removeOutdatedAccount(accountKey); + } this.setAccount(updatedAccount); this.commonLogger.verbose( "Updated an outdated account entity in the cache" From 191ef0bbc8b0217535938741612a615d6ff21086 Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Fri, 13 Oct 2023 16:11:45 -0700 Subject: [PATCH 39/91] Fix B2C ID token matching by removing account tenantId from ID token filter and adding logic to prioritize home account ID token when multiple ID tokens match --- lib/msal-common/src/cache/CacheManager.ts | 21 +++++++++++++++++-- .../src/cache/entities/AccountEntity.ts | 2 +- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/lib/msal-common/src/cache/CacheManager.ts b/lib/msal-common/src/cache/CacheManager.ts index 5b32906ab5..674a072e17 100644 --- a/lib/msal-common/src/cache/CacheManager.ts +++ b/lib/msal-common/src/cache/CacheManager.ts @@ -1014,7 +1014,7 @@ export abstract class CacheManager implements ICacheManager { environment: account.environment, credentialType: CredentialType.ID_TOKEN, clientId: this.clientId, - realm: targetRealm || account.tenantId, + realm: targetRealm, }; const idTokens: IdTokenEntity[] = this.getIdTokensByFilter( @@ -1022,11 +1022,28 @@ export abstract class CacheManager implements ICacheManager { tokenKeys ); const numIdTokens = idTokens.length; - if (numIdTokens < 1) { this.commonLogger.info("CacheManager:getIdToken - No token found"); return null; } else if (numIdTokens > 1) { + // Multiple tenant profiles and no tenant specified, pick home account + if (!targetRealm) { + const homeIdToken = idTokens.filter((idToken) => { + return idToken.realm === account.tenantId; + })[0]; + if (homeIdToken) { + this.commonLogger.info( + "CacheManager:getIdToken - Multiple id tokens found, defaulting to home tenant profile" + ); + return homeIdToken; + } else { + this.commonLogger.info( + "CacheManager:getIdToken - Multiple id tokens found for account but none match account entity tenant id, returning first result" + ); + return idTokens[0]; + } + } + // Multiple tokens for a single tenant profile, remove all and return null this.commonLogger.info( "CacheManager:getIdToken - Multiple id tokens found, clearing them" ); diff --git a/lib/msal-common/src/cache/entities/AccountEntity.ts b/lib/msal-common/src/cache/entities/AccountEntity.ts index efb3b4bdcb..d862525e02 100644 --- a/lib/msal-common/src/cache/entities/AccountEntity.ts +++ b/lib/msal-common/src/cache/entities/AccountEntity.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. */ -import { Separators, CacheAccountType, Constants } from "../../utils/Constants"; +import { Separators, CacheAccountType } from "../../utils/Constants"; import { Authority } from "../../authority/Authority"; import { ICrypto } from "../../crypto/ICrypto"; import { ClientInfo, buildClientInfo } from "../../account/ClientInfo"; From 6f6fda9536555aaa73d218a00b2e832ad62a1c70 Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Mon, 16 Oct 2023 14:47:26 -0700 Subject: [PATCH 40/91] Change files --- ...-msal-browser-79eee614-ede9-45f3-b008-5fa2b8a7ecf3.json | 7 +++++++ ...e-msal-common-8efe538a-488d-46f1-b1ea-f1bdea1ad4ae.json | 7 +++++++ ...ure-msal-node-fd8922d0-15cd-4b36-b209-0810fd9a67d6.json | 7 +++++++ 3 files changed, 21 insertions(+) create mode 100644 change/@azure-msal-browser-79eee614-ede9-45f3-b008-5fa2b8a7ecf3.json create mode 100644 change/@azure-msal-common-8efe538a-488d-46f1-b1ea-f1bdea1ad4ae.json create mode 100644 change/@azure-msal-node-fd8922d0-15cd-4b36-b209-0810fd9a67d6.json diff --git a/change/@azure-msal-browser-79eee614-ede9-45f3-b008-5fa2b8a7ecf3.json b/change/@azure-msal-browser-79eee614-ede9-45f3-b008-5fa2b8a7ecf3.json new file mode 100644 index 0000000000..4eca7291f5 --- /dev/null +++ b/change/@azure-msal-browser-79eee614-ede9-45f3-b008-5fa2b8a7ecf3.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "Add support for Multi-tenant accounts and cross-tenant token caching #6466", + "packageName": "@azure/msal-browser", + "email": "hemoral@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@azure-msal-common-8efe538a-488d-46f1-b1ea-f1bdea1ad4ae.json b/change/@azure-msal-common-8efe538a-488d-46f1-b1ea-f1bdea1ad4ae.json new file mode 100644 index 0000000000..4eb60b8cfd --- /dev/null +++ b/change/@azure-msal-common-8efe538a-488d-46f1-b1ea-f1bdea1ad4ae.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "Add support for Multi-tenant accounts and cross-tenant token caching #6466", + "packageName": "@azure/msal-common", + "email": "hemoral@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@azure-msal-node-fd8922d0-15cd-4b36-b209-0810fd9a67d6.json b/change/@azure-msal-node-fd8922d0-15cd-4b36-b209-0810fd9a67d6.json new file mode 100644 index 0000000000..0074112d7d --- /dev/null +++ b/change/@azure-msal-node-fd8922d0-15cd-4b36-b209-0810fd9a67d6.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "Add support for Multi-tenant accounts and cross-tenant token caching #6466", + "packageName": "@azure/msal-node", + "email": "hemoral@microsoft.com", + "dependentChangeType": "patch" +} From e58470aef876fa4c5b77c70cc4d70e5fa70827e6 Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Mon, 23 Oct 2023 17:22:10 -0700 Subject: [PATCH 41/91] Fix B2C and named tenant cross-tenant caching and lookup --- lib/msal-common/src/account/AccountInfo.ts | 22 ++++++++++++--- lib/msal-common/src/account/TokenClaims.ts | 28 +++++++++++++++++++ lib/msal-common/src/authority/Authority.ts | 19 ++++++++++--- lib/msal-common/src/cache/CacheManager.ts | 5 +++- .../src/cache/entities/AccountEntity.ts | 15 ++++++++-- lib/msal-common/src/index.ts | 5 +++- .../src/response/ResponseHandler.ts | 20 ++++++++----- 7 files changed, 94 insertions(+), 20 deletions(-) diff --git a/lib/msal-common/src/account/AccountInfo.ts b/lib/msal-common/src/account/AccountInfo.ts index ea62c62d60..9f419b1a0d 100644 --- a/lib/msal-common/src/account/AccountInfo.ts +++ b/lib/msal-common/src/account/AccountInfo.ts @@ -57,18 +57,32 @@ export function updateTenantProfile( ): AccountInfo { if (idTokenClaims) { const updatedAccountInfo = accountInfo; - const { oid, tid, name } = idTokenClaims; + const { oid, tid, name, tfp, acr } = idTokenClaims; if (oid && oid !== accountInfo.localAccountId) { updatedAccountInfo.localAccountId = oid; } - if (tid && tid !== accountInfo.tenantId) { - updatedAccountInfo.tenantId = tid; - } if (name && name !== accountInfo.name) { updatedAccountInfo.name = name; } + const baseTenantId = accountInfo.tenantId; + + let claimsTenantId = null; + // Determine tenantId from token claims tid > tfp > acr + if (tid && tid !== baseTenantId) { + claimsTenantId = tid; + } else if (tfp && tfp !== baseTenantId) { + claimsTenantId = tfp; + } else if (acr && acr !== baseTenantId) { + claimsTenantId = acr; + } + + if (claimsTenantId) { + // Downcase to match the realm case-insensitive comparison requirements + updatedAccountInfo.tenantId = claimsTenantId.toLowerCase(); + } + updatedAccountInfo.idTokenClaims = idTokenClaims; return updatedAccountInfo; } diff --git a/lib/msal-common/src/account/TokenClaims.ts b/lib/msal-common/src/account/TokenClaims.ts index c747c6663e..5d6a317fac 100644 --- a/lib/msal-common/src/account/TokenClaims.ts +++ b/lib/msal-common/src/account/TokenClaims.ts @@ -35,6 +35,14 @@ export type TokenClaims = { * Users' tenant or '9188040d-6c67-4c5b-b112-36a304b66dad' for personal accounts. */ tid?: string; + /** + * Trusted Framework Policy (B2C) The name of the policy that was used to acquire the ID token. + */ + tfp?: string; + /** + * Authentication Context Class Reference (B2C) Used only with older policies. + */ + acr?: string; ver?: string; upn?: string; preferred_username?: string; @@ -68,3 +76,23 @@ export type TokenClaims = { tenant_region_scope?: string; tenant_region_sub_scope?: string; }; + +/** + * Gets tenantId from available ID token claims to set as credential realm with the following precedence: + * 1. tid - if the token is acquired from an Azure AD tenant tid will be present + * 2. tfp - if the token is acquired from a modern B2C tenant tfp should be present + * 3. acr - if the token is acquired from a legacy B2C tenant acr should be present + * Downcased to match the realm case-insensitive comparison requirements + * @param idTokenClaims + * @returns + */ +export function getTenantIdFromIdTokenClaims( + idTokenClaims?: TokenClaims +): string | null { + if (idTokenClaims) { + const tenantId = + idTokenClaims.tid || idTokenClaims.tfp || idTokenClaims.acr; + return tenantId?.toLowerCase() || null; + } + return null; +} diff --git a/lib/msal-common/src/authority/Authority.ts b/lib/msal-common/src/authority/Authority.ts index d20e1bbf59..eb15db3b3f 100644 --- a/lib/msal-common/src/authority/Authority.ts +++ b/lib/msal-common/src/authority/Authority.ts @@ -1263,11 +1263,22 @@ export function getTenantFromAuthorityString( const authorityUrl = new UrlString(authority); const authorityUrlComponents = authorityUrl.getUrlComponents(); /** - * Tenant ID is the path after the domain: - * AAD Authority - domain/tenantId - * B2C Authority - domain/tenantId?/tfp?/policy + * For credential matching purposes, tenantId is the last path segment of the authority URL: + * AAD Authority - domain/tenantId -> Credentials are cached with realm = tenantId + * B2C Authority - domain/{tenantId}?/.../policy -> Credentials are cached with realm = policy + * tenantId is downcased because B2C policies can have mixed case but tfp claim is downcased */ - return authorityUrlComponents.PathSegments.join(Constants.FORWARD_SLASH); + const tenantId = + authorityUrlComponents.PathSegments.slice(-1)[0].toLowerCase(); + + switch (tenantId) { + case AADAuthorityConstants.COMMON: + case AADAuthorityConstants.ORGANIZATIONS: + case AADAuthorityConstants.CONSUMERS: + return undefined; + default: + return tenantId; + } } export function formatAuthorityUri(authorityUri: string): string { diff --git a/lib/msal-common/src/cache/CacheManager.ts b/lib/msal-common/src/cache/CacheManager.ts index 674a072e17..301bda7648 100644 --- a/lib/msal-common/src/cache/CacheManager.ts +++ b/lib/msal-common/src/cache/CacheManager.ts @@ -957,7 +957,10 @@ export abstract class CacheManager implements ICacheManager { request: BaseAuthRequest, environment: string ): CacheRecord { - const requestTenantId = getTenantFromAuthorityString(request.authority); + // Use authority tenantId for cache lookup filter if it's defined, otherwise use tenantId from account passed in + const requestTenantId = + getTenantFromAuthorityString(request.authority) || account.tenantId; + const tokenKeys = this.getTokenKeys(); const cachedAccount = this.readAccountFromCache(account); const cachedIdToken = this.getIdToken( diff --git a/lib/msal-common/src/cache/entities/AccountEntity.ts b/lib/msal-common/src/cache/entities/AccountEntity.ts index d862525e02..ca013f0790 100644 --- a/lib/msal-common/src/cache/entities/AccountEntity.ts +++ b/lib/msal-common/src/cache/entities/AccountEntity.ts @@ -14,7 +14,10 @@ import { } from "../../error/ClientAuthError"; import { AuthorityType } from "../../authority/AuthorityType"; import { Logger } from "../../logger/Logger"; -import { TokenClaims } from "../../account/TokenClaims"; +import { + TokenClaims, + getTenantIdFromIdTokenClaims, +} from "../../account/TokenClaims"; import { ProtocolMode } from "../../authority/ProtocolMode"; /** @@ -166,9 +169,15 @@ export class AccountEntity { account.environment = env; // non AAD scenarios can have empty realm account.realm = - clientInfo?.utid || accountDetails.idTokenClaims.tid || ""; + clientInfo?.utid || + getTenantIdFromIdTokenClaims(accountDetails.idTokenClaims) || + ""; - account.tenants = accountDetails.tenants || []; + if (accountDetails.tenants) { + account.tenants = accountDetails.tenants; + } else { + account.tenants = account.realm ? [account.realm] : []; + } // How do you account for MSA CID here? account.localAccountId = diff --git a/lib/msal-common/src/index.ts b/lib/msal-common/src/index.ts index c4b13d2ebb..dc9f183b38 100644 --- a/lib/msal-common/src/index.ts +++ b/lib/msal-common/src/index.ts @@ -34,7 +34,10 @@ export { updateTenantProfile, } from "./account/AccountInfo"; export * as AuthToken from "./account/AuthToken"; -export { TokenClaims } from "./account/TokenClaims"; +export { + TokenClaims, + getTenantIdFromIdTokenClaims, +} from "./account/TokenClaims"; export { TokenClaims as IdTokenClaims } from "./account/TokenClaims"; export { CcsCredential, CcsCredentialType } from "./account/CcsCredential"; export { diff --git a/lib/msal-common/src/response/ResponseHandler.ts b/lib/msal-common/src/response/ResponseHandler.ts index a899217b34..38ce8a939c 100644 --- a/lib/msal-common/src/response/ResponseHandler.ts +++ b/lib/msal-common/src/response/ResponseHandler.ts @@ -43,7 +43,10 @@ import { BaseAuthRequest } from "../request/BaseAuthRequest"; import { IPerformanceClient } from "../telemetry/performance/IPerformanceClient"; import { PerformanceEvents } from "../telemetry/performance/PerformanceEvent"; import { checkMaxAge, extractTokenClaims } from "../account/AuthToken"; -import { TokenClaims } from "../account/TokenClaims"; +import { + TokenClaims, + getTenantIdFromIdTokenClaims, +} from "../account/TokenClaims"; import { AccountInfo, updateTenantProfile } from "../account/AccountInfo"; /** @@ -412,6 +415,8 @@ export class ResponseHandler { ); } + const claimsTenantId = getTenantIdFromIdTokenClaims(idTokenClaims); + // IdToken: non AAD scenarios can have empty realm let cachedIdToken: IdTokenEntity | undefined; let cachedAccount: AccountEntity | undefined; @@ -421,13 +426,14 @@ export class ResponseHandler { env, serverTokenResponse.id_token, this.clientId, - idTokenClaims.tid || "" + claimsTenantId || "" ); cachedAccount = this.setCachedAccount( authority, serverTokenResponse, idTokenClaims, + claimsTenantId, authCodePayload ); } @@ -470,7 +476,7 @@ export class ResponseHandler { env, serverTokenResponse.access_token || Constants.EMPTY_STRING, this.clientId, - idTokenClaims?.tid || authority.tenant, + claimsTenantId || authority.tenant, responseScopes.printScopes(), tokenExpirationSeconds, extendedTokenExpirationSeconds, @@ -520,10 +526,10 @@ export class ResponseHandler { authority: Authority, serverTokenResponse: ServerAuthorizationTokenResponse, idTokenClaims: TokenClaims, + claimsTenantId: string | null, authCodePayload?: AuthorizationCodePayload ): AccountEntity | undefined { this.logger.verbose("setCachedAccount called"); - const responseTenantId = idTokenClaims.tid || ""; // Check if base account is already cached const accountKeys = this.cacheStorage.getAccountKeys(); @@ -545,15 +551,15 @@ export class ResponseHandler { clientInfo: serverTokenResponse.client_info, cloudGraphHostName: authCodePayload?.cloud_graph_host_name, msGraphHost: authCodePayload?.msgraph_host, - tenants: [responseTenantId], + tenants: claimsTenantId ? [claimsTenantId] : [], }, authority, this.cryptoObj ); const tenants = baseAccount.tenants || []; - if (!tenants.includes(responseTenantId)) { - tenants.push(responseTenantId); + if (claimsTenantId && !tenants.includes(claimsTenantId)) { + tenants.push(claimsTenantId); } baseAccount.tenants = tenants; From 0cfe01ee3b55660eef352441cfdfdb87b423e0d5 Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Mon, 23 Oct 2023 17:28:43 -0700 Subject: [PATCH 42/91] Update common unit tests --- lib/msal-common/test/cache/CacheManager.spec.ts | 7 +++++-- .../test/client/AuthorizationCodeClient.spec.ts | 4 ++++ lib/msal-common/test/client/SilentFlowClient.spec.ts | 6 ------ 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/lib/msal-common/test/cache/CacheManager.spec.ts b/lib/msal-common/test/cache/CacheManager.spec.ts index f1ba4ac41c..d55b820420 100644 --- a/lib/msal-common/test/cache/CacheManager.spec.ts +++ b/lib/msal-common/test/cache/CacheManager.spec.ts @@ -239,7 +239,11 @@ describe("CacheManager.ts test cases", () => { mockCache.cacheManager.getTokenKeys().idToken.length ).toEqual(2); expect( - mockCache.cacheManager.getIdToken(TEST_ACCOUNT_INFO) + mockCache.cacheManager.getIdToken( + TEST_ACCOUNT_INFO, + undefined, + TEST_ACCOUNT_INFO.tenantId + ) ).toBeNull(); expect( mockCache.cacheManager.getTokenKeys().idToken.length @@ -275,7 +279,6 @@ describe("CacheManager.ts test cases", () => { const accounts = mockCache.cacheManager.getAllAccounts(); expect(accounts).not.toBeNull(); - expect(accounts[0].idToken).toEqual(TEST_TOKENS.IDTOKEN_V2); expect(accounts[0].idTokenClaims).toEqual(ID_TOKEN_CLAIMS); }); diff --git a/lib/msal-common/test/client/AuthorizationCodeClient.spec.ts b/lib/msal-common/test/client/AuthorizationCodeClient.spec.ts index d6074516cc..4605912ab4 100644 --- a/lib/msal-common/test/client/AuthorizationCodeClient.spec.ts +++ b/lib/msal-common/test/client/AuthorizationCodeClient.spec.ts @@ -731,6 +731,8 @@ describe("AuthorizationCodeClient unit tests", () => { | "amr" | "idp" | "auth_time" + | "tfp" + | "acr" > > = { ver: "2.0", @@ -811,6 +813,8 @@ describe("AuthorizationCodeClient unit tests", () => { | "amr" | "idp" | "auth_time" + | "tfp" + | "acr" > > = { ver: "2.0", diff --git a/lib/msal-common/test/client/SilentFlowClient.spec.ts b/lib/msal-common/test/client/SilentFlowClient.spec.ts index a2706d81a7..5da8d1b106 100644 --- a/lib/msal-common/test/client/SilentFlowClient.spec.ts +++ b/lib/msal-common/test/client/SilentFlowClient.spec.ts @@ -488,12 +488,6 @@ describe("SilentFlowClient unit tests", () => { }); it("Throws error if scopes are not included in request object", async () => { - sinon - .stub( - Authority.prototype, - "getEndpointMetadataFromNetwork" - ) - .resolves(DEFAULT_OPENID_CONFIG_RESPONSE.body); const config = await ClientTestUtils.createTestClientConfiguration(); const client = new SilentFlowClient(config, stubPerformanceClient); From dda75884929036268748470986566649f98e0fe2 Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Mon, 23 Oct 2023 17:48:14 -0700 Subject: [PATCH 43/91] Update common, browser and node unit tests --- .../test/app/PublicClientApplication.spec.ts | 50 +++++++++---------- .../test/cache/TestStorageManager.ts | 4 ++ .../interaction_client/RedirectClient.spec.ts | 1 - .../SilentCacheClient.spec.ts | 2 +- lib/msal-node/test/client/ClientTestUtils.ts | 4 ++ lib/msal-node/test/utils/TestConstants.ts | 1 - 6 files changed, 33 insertions(+), 29 deletions(-) diff --git a/lib/msal-browser/test/app/PublicClientApplication.spec.ts b/lib/msal-browser/test/app/PublicClientApplication.spec.ts index d3291e549f..fa38416121 100644 --- a/lib/msal-browser/test/app/PublicClientApplication.spec.ts +++ b/lib/msal-browser/test/app/PublicClientApplication.spec.ts @@ -5046,7 +5046,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { expect(pca.getActiveAccount()).toEqual(null); pca.setActiveAccount(testAccountInfo1); const activeAccount = pca.getActiveAccount(); - expect(activeAccount?.idToken).not.toBeUndefined(); + expect(activeAccount?.idTokenClaims).not.toBeUndefined(); expect(activeAccount).toEqual(testAccountInfo1); pca.clearCache(); expect(pca.getActiveAccount()).toEqual(null); @@ -5056,7 +5056,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { expect(pca.getActiveAccount()).toEqual(null); pca.setActiveAccount(testAccountInfo1); const activeAccount = pca.getActiveAccount(); - expect(activeAccount?.idToken).not.toBeUndefined(); + expect(activeAccount?.idTokenClaims).not.toBeUndefined(); expect(activeAccount).toEqual(testAccountInfo1); pca.clearCache({ account: testAccountInfo1, @@ -5076,7 +5076,6 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { username: ID_TOKEN_CLAIMS.preferred_username, name: "Abe Lincoln", localAccountId: ID_TOKEN_CLAIMS.oid, - idToken: TEST_TOKENS.IDTOKEN_V2, idTokenClaims: ID_TOKEN_CLAIMS, nativeAccountId: undefined, tenants: [ID_TOKEN_CLAIMS.tid], @@ -5105,7 +5104,6 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { username: "anotherExample@microsoft.com", name: "Abe Lincoln", localAccountId: ID_TOKEN_ALT_CLAIMS.oid, - idToken: TEST_TOKENS.IDTOKEN_V2_ALT, idTokenClaims: ID_TOKEN_ALT_CLAIMS, nativeAccountId: undefined, tenants: [ID_TOKEN_ALT_CLAIMS.tid], @@ -5247,10 +5245,10 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { it("getAllAccounts with no account filter returns all signed in accounts", () => { const accounts = pca.getAllAccounts(); expect(accounts).toHaveLength(4); - expect(accounts[0].idToken).not.toBeUndefined(); - expect(accounts[1].idToken).not.toBeUndefined(); - expect(accounts[2].idToken).not.toBeUndefined(); - expect(accounts[3].idToken).not.toBeUndefined(); + expect(accounts[0].idTokenClaims).not.toBeUndefined(); + expect(accounts[1].idTokenClaims).not.toBeUndefined(); + expect(accounts[2].idTokenClaims).not.toBeUndefined(); + expect(accounts[3].idTokenClaims).not.toBeUndefined(); }); it("getAllAccounts returns all accounts matching the filter passed in", () => { @@ -5268,19 +5266,19 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { it("getAccountByUsername returns account specified", () => { const account = pca.getAccountByUsername("AbeLi@microsoft.com"); - expect(account?.idToken).not.toBeUndefined(); + expect(account?.idTokenClaims).not.toBeUndefined(); expect(account).toEqual(testAccountInfo1); }); it("getAccountByUsername returns account specified with case mismatch", () => { const account = pca.getAccountByUsername("abeli@Microsoft.com"); - expect(account?.idToken).not.toBeUndefined(); + expect(account?.idTokenClaims).not.toBeUndefined(); expect(account).toEqual(testAccountInfo1); const account2 = pca.getAccountByUsername( "anotherexample@microsoft.com" ); - expect(account2?.idToken).not.toBeUndefined(); + expect(account2?.idTokenClaims).not.toBeUndefined(); expect(account2).toEqual(testAccountInfo2); }); @@ -5301,7 +5299,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { const account = pca.getAccountByHomeId( TEST_DATA_CLIENT_INFO.TEST_HOME_ACCOUNT_ID ); - expect(account?.idToken).not.toBeUndefined(); + expect(account?.idTokenClaims).not.toBeUndefined(); expect(account).toEqual(testAccountInfo1); }); @@ -5318,7 +5316,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { it("getAccountByLocalId returns account specified", () => { const account = pca.getAccountByLocalId(ID_TOKEN_CLAIMS.oid); - expect(account?.idToken).not.toBeUndefined(); + expect(account?.idTokenClaims).not.toBeUndefined(); expect(account).toEqual(testAccountInfo1); }); @@ -5344,7 +5342,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { const account = pca.getAccount({ loginHint: "testLoginHint", }); - expect(account?.idToken).not.toBeUndefined(); + expect(account?.idTokenClaims).not.toBeUndefined(); expect(account?.homeAccountId).toEqual( testAccountInfo3.homeAccountId ); @@ -5353,7 +5351,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { const account = pca.getAccount({ loginHint: ID_TOKEN_ALT_CLAIMS.preferred_username, }); - expect(account?.idToken).not.toBeUndefined(); + expect(account?.idTokenClaims).not.toBeUndefined(); expect(account?.homeAccountId).toEqual( testAccountInfo2.homeAccountId ); @@ -5362,7 +5360,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { const account = pca.getAccount({ loginHint: "testUpn", }); - expect(account?.idToken).not.toBeUndefined(); + expect(account?.idTokenClaims).not.toBeUndefined(); expect(account?.homeAccountId).toEqual( testAccountInfo4.homeAccountId ); @@ -5371,7 +5369,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { const account = pca.getAccount({ sid: "testSid", }); - expect(account?.idToken).not.toBeUndefined(); + expect(account?.idTokenClaims).not.toBeUndefined(); expect(account?.homeAccountId).toEqual( testAccountInfo3.homeAccountId ); @@ -5382,7 +5380,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { const account = pca.getAccount({ homeAccountId: TEST_DATA_CLIENT_INFO.TEST_HOME_ACCOUNT_ID, }); - expect(account?.idToken).not.toBeUndefined(); + expect(account?.idTokenClaims).not.toBeUndefined(); expect(account).toEqual(testAccountInfo1); }); @@ -5390,7 +5388,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { const account = pca.getAccount({ localAccountId: ID_TOKEN_CLAIMS.oid, }); - expect(account?.idToken).not.toBeUndefined(); + expect(account?.idTokenClaims).not.toBeUndefined(); expect(account).toEqual(testAccountInfo1); }); @@ -5398,7 +5396,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { const account = pca.getAccount({ username: ID_TOKEN_CLAIMS.preferred_username, }); - expect(account?.idToken).not.toBeUndefined(); + expect(account?.idTokenClaims).not.toBeUndefined(); expect(account).toEqual(testAccountInfo1); }); @@ -5407,7 +5405,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { homeAccountId: testAccountInfo1.homeAccountId, localAccountId: testAccountInfo1.localAccountId, }); - expect(account?.idToken).not.toBeUndefined(); + expect(account?.idTokenClaims).not.toBeUndefined(); expect(account).toEqual(testAccountInfo1); }); }); @@ -5536,7 +5534,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { expect(pca.getActiveAccount()).toBe(null); pca.setActiveAccount(testAccountInfo1); const activeAccount = pca.getActiveAccount(); - expect(activeAccount?.idToken).not.toBeUndefined(); + expect(activeAccount?.idTokenClaims).not.toBeUndefined(); expect(activeAccount).toEqual(testAccountInfo1); }); @@ -5611,7 +5609,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { pca.setActiveAccount(testAccountInfo1); const activeAccount = pca.getActiveAccount(); expect(activeAccount).not.toBeNull(); - expect(activeAccount?.idToken).not.toBeUndefined(); + expect(activeAccount?.idTokenClaims).not.toBeUndefined(); expect(activeAccount?.homeAccountId).toEqual( testAccountInfo1.homeAccountId ); @@ -5625,13 +5623,13 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { pca.setActiveAccount(testAccountInfo1); let activeAccount = pca.getActiveAccount(); - expect(activeAccount?.idToken).not.toBeUndefined(); + expect(activeAccount?.idTokenClaims).not.toBeUndefined(); expect(activeAccount).toEqual(testAccountInfo1); expect(activeAccount).not.toEqual(testAccountInfo2); pca.setActiveAccount(testAccountInfo2); activeAccount = pca.getActiveAccount(); - expect(activeAccount?.idToken).not.toBeUndefined(); + expect(activeAccount?.idTokenClaims).not.toBeUndefined(); expect(pca.getActiveAccount()).not.toEqual( testAccountInfo1 ); @@ -5643,7 +5641,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { pca.setActiveAccount(testAccountInfo2); const activeAccount = pca.getActiveAccount(); - expect(activeAccount?.idToken).not.toBeUndefined(); + expect(activeAccount?.idTokenClaims).not.toBeUndefined(); expect(activeAccount).not.toEqual(testAccountInfo1); expect(activeAccount).toEqual(testAccountInfo2); diff --git a/lib/msal-browser/test/cache/TestStorageManager.ts b/lib/msal-browser/test/cache/TestStorageManager.ts index dcbc28c001..4b6e782e9f 100644 --- a/lib/msal-browser/test/cache/TestStorageManager.ts +++ b/lib/msal-browser/test/cache/TestStorageManager.ts @@ -55,6 +55,10 @@ export class TestStorageManager extends CacheManager { } } + removeOutdatedAccount(accountKey: string): void { + this.removeAccount(accountKey); + } + getAccountKeys(): string[] { return this.store[ACCOUNT_KEYS] || []; } diff --git a/lib/msal-browser/test/interaction_client/RedirectClient.spec.ts b/lib/msal-browser/test/interaction_client/RedirectClient.spec.ts index 09b9f8788c..90a1bb5ca4 100644 --- a/lib/msal-browser/test/interaction_client/RedirectClient.spec.ts +++ b/lib/msal-browser/test/interaction_client/RedirectClient.spec.ts @@ -3654,7 +3654,6 @@ describe("RedirectClient", () => { environment: "login.windows.net", tenantId: ID_TOKEN_CLAIMS.tid, username: ID_TOKEN_CLAIMS.preferred_username, - idToken: TEST_TOKENS.IDTOKEN_V2, idTokenClaims: ID_TOKEN_CLAIMS, name: ID_TOKEN_CLAIMS.name, nativeAccountId: undefined, diff --git a/lib/msal-browser/test/interaction_client/SilentCacheClient.spec.ts b/lib/msal-browser/test/interaction_client/SilentCacheClient.spec.ts index a11d7fc133..a5114f1361 100644 --- a/lib/msal-browser/test/interaction_client/SilentCacheClient.spec.ts +++ b/lib/msal-browser/test/interaction_client/SilentCacheClient.spec.ts @@ -160,7 +160,7 @@ describe("SilentCacheClient", () => { pca.setActiveAccount(testAccount); expect(pca.getActiveAccount()).toEqual({ ...testAccount, - idToken: TEST_TOKENS.IDTOKEN_V2, + idTokenClaims: ID_TOKEN_CLAIMS, }); silentCacheClient.logout({ account: testAccount }); //@ts-ignore diff --git a/lib/msal-node/test/client/ClientTestUtils.ts b/lib/msal-node/test/client/ClientTestUtils.ts index cef23b03bd..b07672b9bd 100644 --- a/lib/msal-node/test/client/ClientTestUtils.ts +++ b/lib/msal-node/test/client/ClientTestUtils.ts @@ -76,6 +76,10 @@ export class MockStorageClass extends CacheManager { } } + removeOutdatedAccount(accountKey: string): void { + this.removeAccount(accountKey); + } + getAccountKeys(): string[] { return this.store[ACCOUNT_KEYS] || []; } diff --git a/lib/msal-node/test/utils/TestConstants.ts b/lib/msal-node/test/utils/TestConstants.ts index eed68e1f73..c5a18d6573 100644 --- a/lib/msal-node/test/utils/TestConstants.ts +++ b/lib/msal-node/test/utils/TestConstants.ts @@ -230,7 +230,6 @@ export const mockAccountInfo: AccountInfo = { tenantId: ID_TOKEN_CLAIMS.tid, username: ID_TOKEN_CLAIMS.preferred_username, idTokenClaims: ID_TOKEN_CLAIMS, - idToken: TEST_CONSTANTS.ID_TOKEN, name: ID_TOKEN_CLAIMS.name, nativeAccountId: undefined, tenants: [ID_TOKEN_CLAIMS.tid], From af6130ba9dc3ce6a8cc2b8106df2aae1e357f0b8 Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Mon, 23 Oct 2023 18:12:04 -0700 Subject: [PATCH 44/91] Update browser cache test utils to use utid from idToken homeAccountId instead of idToken realm to look up the account in the cache --- samples/e2eTestUtils/src/BrowserCacheTestUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/e2eTestUtils/src/BrowserCacheTestUtils.ts b/samples/e2eTestUtils/src/BrowserCacheTestUtils.ts index 679474fe5b..7d879ebad5 100644 --- a/samples/e2eTestUtils/src/BrowserCacheTestUtils.ts +++ b/samples/e2eTestUtils/src/BrowserCacheTestUtils.ts @@ -174,7 +174,7 @@ export class BrowserCacheUtils { "-" + tokenVal.environment + "-" + - tokenVal.realm; + tokenVal.homeAccountId.split(".")[1]; if (Object.keys(storage).includes(accountKey)) { return JSON.parse(storage[accountKey]); From 6554303888c0154a3285f74ca7fd60962f99a13c Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Wed, 25 Oct 2023 10:43:02 -0700 Subject: [PATCH 45/91] Add ID token duplicate removal logic --- lib/msal-common/src/cache/CacheManager.ts | 54 +++++++++++++------- lib/msal-node/src/client/OnBehalfOfClient.ts | 6 +-- 2 files changed, 38 insertions(+), 22 deletions(-) diff --git a/lib/msal-common/src/cache/CacheManager.ts b/lib/msal-common/src/cache/CacheManager.ts index 301bda7648..a2812857cb 100644 --- a/lib/msal-common/src/cache/CacheManager.ts +++ b/lib/msal-common/src/cache/CacheManager.ts @@ -1020,44 +1020,57 @@ export abstract class CacheManager implements ICacheManager { realm: targetRealm, }; - const idTokens: IdTokenEntity[] = this.getIdTokensByFilter( + const idTokenMap: Map = this.getIdTokensByFilter( idTokenFilter, tokenKeys ); - const numIdTokens = idTokens.length; + + const numIdTokens = idTokenMap.size; + if (numIdTokens < 1) { this.commonLogger.info("CacheManager:getIdToken - No token found"); return null; } else if (numIdTokens > 1) { + let tokensToBeRemoved: Map = idTokenMap; // Multiple tenant profiles and no tenant specified, pick home account if (!targetRealm) { - const homeIdToken = idTokens.filter((idToken) => { - return idToken.realm === account.tenantId; - })[0]; - if (homeIdToken) { + const homeIdTokenMap: Map = new Map< + string, + IdTokenEntity + >(); + idTokenMap.forEach((idToken, key) => { + if (idToken.realm === account.tenantId) { + homeIdTokenMap.set(key, idToken); + } + }); + const numHomeIdTokens = homeIdTokenMap.size; + if (numHomeIdTokens < 1) { this.commonLogger.info( - "CacheManager:getIdToken - Multiple id tokens found, defaulting to home tenant profile" + "CacheManager:getIdToken - Multiple ID tokens found for account but none match account entity tenant id, returning first result" ); - return homeIdToken; - } else { + return idTokenMap.values().next().value; + } else if (numHomeIdTokens === 1) { this.commonLogger.info( - "CacheManager:getIdToken - Multiple id tokens found for account but none match account entity tenant id, returning first result" + "CacheManager:getIdToken - Multiple ID tokens found for account, defaulting to home tenant profile" ); - return idTokens[0]; + return homeIdTokenMap.values().next().value; + } else { + // Multiple ID tokens for home tenant profile, remove all and return null + tokensToBeRemoved = homeIdTokenMap; } } // Multiple tokens for a single tenant profile, remove all and return null this.commonLogger.info( - "CacheManager:getIdToken - Multiple id tokens found, clearing them" + "CacheManager:getIdToken - Multiple matching ID tokens found, clearing them" ); - idTokens.forEach((idToken) => { - this.removeIdToken(idToken.generateCredentialKey()); + tokensToBeRemoved.forEach((idToken, key) => { + this.removeIdToken(key); }); return null; } - this.commonLogger.info("CacheManager:getIdToken - Returning id token"); - return idTokens[0]; + this.commonLogger.info("CacheManager:getIdToken - Returning ID token"); + return idTokenMap.values().next().value; } /** @@ -1068,11 +1081,14 @@ export abstract class CacheManager implements ICacheManager { getIdTokensByFilter( filter: CredentialFilter, tokenKeys?: TokenKeys - ): IdTokenEntity[] { + ): Map { const idTokenKeys = (tokenKeys && tokenKeys.idToken) || this.getTokenKeys().idToken; - const idTokens: IdTokenEntity[] = []; + const idTokens: Map = new Map< + string, + IdTokenEntity + >(); idTokenKeys.forEach((key) => { if ( !this.idTokenKeyMatchesFilter(key, { @@ -1084,7 +1100,7 @@ export abstract class CacheManager implements ICacheManager { } const idToken = this.getIdTokenCredential(key); if (idToken && this.credentialMatchesFilter(idToken, filter)) { - idTokens.push(idToken); + idTokens.set(key, idToken); } }); diff --git a/lib/msal-node/src/client/OnBehalfOfClient.ts b/lib/msal-node/src/client/OnBehalfOfClient.ts index 4a1bc845e4..9d58cf2d3d 100644 --- a/lib/msal-node/src/client/OnBehalfOfClient.ts +++ b/lib/msal-node/src/client/OnBehalfOfClient.ts @@ -184,14 +184,14 @@ export class OnBehalfOfClient extends BaseClient { realm: this.authority.tenant, }; - const idTokens: IdTokenEntity[] = + const idTokenMap: Map = this.cacheManager.getIdTokensByFilter(idTokenFilter); // When acquiring a token on behalf of an application, there might not be an id token in the cache - if (idTokens.length < 1) { + if (Object.values(idTokenMap).length < 1) { return null; } - return idTokens[0] as IdTokenEntity; + return Object.values(idTokenMap)[0] as IdTokenEntity; } /** From 276d2bdf0899ae6fa90e26d0c3021160c54ba56c Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Tue, 31 Oct 2023 12:41:53 -0700 Subject: [PATCH 46/91] Set sample to localstorage --- .../VanillaJSTestApp2.0/app/multi-tenant/authConfig.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/authConfig.js b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/authConfig.js index a66b2d2cc7..77d9a22636 100644 --- a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/authConfig.js +++ b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/authConfig.js @@ -9,7 +9,7 @@ const msalConfig = { authority: `${baseAuthority}/${homeTenant}` }, cache: { - cacheLocation: "sessionStorage", // This configures where your cache will be stored + cacheLocation: "localStorage", // This configures where your cache will be stored storeAuthStateInCookie: false, // Set this to "true" if you are having issues on IE11 or Edge }, system: { From f9d1797b59c42a0d77d27b57074e44167571c2c7 Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Tue, 7 Nov 2023 16:34:48 -0800 Subject: [PATCH 47/91] Add tenant profile maps --- lib/msal-browser/src/cache/TokenCache.ts | 2 - .../NativeInteractionClient.ts | 5 +- lib/msal-common/src/account/AccountInfo.ts | 87 +++++++++++++------ lib/msal-common/src/cache/CacheManager.ts | 33 ++++++- .../src/cache/entities/AccountEntity.ts | 45 +++++++--- lib/msal-common/src/index.ts | 3 +- .../src/response/ResponseHandler.ts | 28 ++++-- .../app/multi-tenant/auth.js | 2 +- .../app/multi-tenant/ui.js | 12 +-- 9 files changed, 156 insertions(+), 61 deletions(-) diff --git a/lib/msal-browser/src/cache/TokenCache.ts b/lib/msal-browser/src/cache/TokenCache.ts index 3529ed75a2..cad72715a9 100644 --- a/lib/msal-browser/src/cache/TokenCache.ts +++ b/lib/msal-browser/src/cache/TokenCache.ts @@ -259,14 +259,12 @@ export class TokenCache implements ITokenCache { ); } - const tenantId = idTokenClaims.tid; const accountEntity = AccountEntity.createAccount( { homeAccountId, idTokenClaims: idTokenClaims, clientInfo, environment: authority.hostnameAndPort, - tenants: tenantId ? [tenantId] : [], }, authority ); diff --git a/lib/msal-browser/src/interaction_client/NativeInteractionClient.ts b/lib/msal-browser/src/interaction_client/NativeInteractionClient.ts index d0b9b1b617..36ea9354a5 100644 --- a/lib/msal-browser/src/interaction_client/NativeInteractionClient.ts +++ b/lib/msal-browser/src/interaction_client/NativeInteractionClient.ts @@ -33,7 +33,7 @@ import { invokeAsync, createAuthError, AuthErrorCodes, - updateTenantProfile, + updateAccountTenantProfileData, CacheHelpers, } from "@azure/msal-common"; import { BaseInteractionClient } from "./BaseInteractionClient"; @@ -584,8 +584,9 @@ export class NativeInteractionClient extends BaseInteractionClient { idTokenClaims.tid || Constants.EMPTY_STRING; - const accountInfo: AccountInfo | null = updateTenantProfile( + const accountInfo: AccountInfo | null = updateAccountTenantProfileData( accountEntity.getAccountInfo(), + undefined, idTokenClaims ); diff --git a/lib/msal-common/src/account/AccountInfo.ts b/lib/msal-common/src/account/AccountInfo.ts index 9f419b1a0d..df781ce89f 100644 --- a/lib/msal-common/src/account/AccountInfo.ts +++ b/lib/msal-common/src/account/AccountInfo.ts @@ -15,7 +15,7 @@ import { TokenClaims } from "./TokenClaims"; * - idToken - raw ID token * - idTokenClaims - Object contains claims from ID token * - nativeAccountId - The user's native account ID - * - tenants - Array of strings that represent a unique identifier for the tenants that the account has been authenticated with. + * - tenantProfiles - Map of tenant profile objects for each tenant that the account has authenticated with in the browser */ export type AccountInfo = { homeAccountId: string; @@ -36,7 +36,22 @@ export type AccountInfo = { }; nativeAccountId?: string; authorityType?: string; - tenants?: string[]; + tenantProfiles?: Map; +}; + +/** + * Account details that vary across tenants for the same user + * + * - tenantId - Full tenant or organizational id that this account belongs to + * - localAccountId - Local, tenant-specific account identifer for this account object, usually used in legacy cases + * - name - Full name for the account, including given name and family name + * - isHomeTenant - True if this is the home tenant profile of the account, false if it's a guest tenant profile + */ +export type TenantProfile = Pick< + AccountInfo, + "tenantId" | "localAccountId" | "name" +> & { + isHomeTenant?: boolean; }; export type ActiveAccountFilters = { @@ -51,37 +66,30 @@ export type ActiveAccountFilters = { * @param idTokenClaims * @returns */ -export function updateTenantProfile( +export function updateAccountTenantProfileData( accountInfo: AccountInfo, + tenantProfile?: TenantProfile, idTokenClaims?: TokenClaims ): AccountInfo { - if (idTokenClaims) { - const updatedAccountInfo = accountInfo; - const { oid, tid, name, tfp, acr } = idTokenClaims; - - if (oid && oid !== accountInfo.localAccountId) { - updatedAccountInfo.localAccountId = oid; - } - if (name && name !== accountInfo.name) { - updatedAccountInfo.name = name; - } - - const baseTenantId = accountInfo.tenantId; + let updatedAccountInfo = accountInfo; + // Tenant Profile overrides passed in account info + if (tenantProfile) { + updatedAccountInfo = { ...accountInfo, ...tenantProfile }; + } - let claimsTenantId = null; - // Determine tenantId from token claims tid > tfp > acr - if (tid && tid !== baseTenantId) { - claimsTenantId = tid; - } else if (tfp && tfp !== baseTenantId) { - claimsTenantId = tfp; - } else if (acr && acr !== baseTenantId) { - claimsTenantId = acr; - } + // ID token claims override passed in account info and tenant profile + if (idTokenClaims) { + // @ts-ignore + const { isHomeTenant, ...tenantProfile } = + buildTenantProfileFromIdTokenClaims( + accountInfo.homeAccountId, + idTokenClaims + ); - if (claimsTenantId) { - // Downcase to match the realm case-insensitive comparison requirements - updatedAccountInfo.tenantId = claimsTenantId.toLowerCase(); - } + updatedAccountInfo = { + ...updatedAccountInfo, + ...tenantProfile, + }; updatedAccountInfo.idTokenClaims = idTokenClaims; return updatedAccountInfo; @@ -89,3 +97,26 @@ export function updateTenantProfile( return accountInfo; } + +export function buildTenantProfileFromIdTokenClaims( + homeAccountId: string, + idTokenClaims: TokenClaims +): TenantProfile { + const { oid, sub, tid, name, tfp, acr } = idTokenClaims; + + const claimsTenantId = tid || tfp || acr; + let tenantId: string | undefined; + + if (claimsTenantId) { + // Downcase to match the realm case-insensitive comparison requirements + tenantId = claimsTenantId.toLowerCase(); + } + const homeTenantId = homeAccountId.split(".")[1]; + + return { + tenantId: tenantId || "", + localAccountId: oid || sub || "", + name: name, + isHomeTenant: tenantId === homeTenantId, + }; +} diff --git a/lib/msal-common/src/cache/CacheManager.ts b/lib/msal-common/src/cache/CacheManager.ts index ff173fd248..658881c36e 100644 --- a/lib/msal-common/src/cache/CacheManager.ts +++ b/lib/msal-common/src/cache/CacheManager.ts @@ -33,7 +33,10 @@ import { createClientAuthError, ClientAuthErrorCodes, } from "../error/ClientAuthError"; -import { AccountInfo, updateTenantProfile } from "../account/AccountInfo"; +import { + AccountInfo, + updateAccountTenantProfileData, +} from "../account/AccountInfo"; import { AppMetadataEntity } from "./entities/AppMetadataEntity"; import { ServerTelemetryEntity } from "./entities/ServerTelemetryEntity"; import { ThrottlingEntity } from "./entities/ThrottlingEntity"; @@ -351,9 +354,35 @@ export abstract class CacheManager implements ICacheManager { tenantProfileFilter ) ) { - return updateTenantProfile(accountInfo, idTokenClaims); + return updateAccountTenantProfileData( + accountInfo, + undefined, + idTokenClaims + ); + } + } else { + if (targetTenantId) { + this.commonLogger.verbose( + "getAccountInfoFromEntity(): No matching ID token found in cache for given tenant ID, falling back to cached tenant profiles" + ); + const tenantProfile = + accountInfo.tenantProfiles?.get(targetTenantId); + if (tenantProfile) { + this.commonLogger.verbose( + "getAccountInfoFromEntity(): Found cached tenant profile matching given tenant ID, updating account info with tenant profile data" + ); + return updateAccountTenantProfileData( + accountInfo, + tenantProfile + ); + } else { + this.commonLogger.verbose( + "getAccountInfoFromEntity(): No matching ID token or tenant profile found in cache for given tenant ID. Returning base account." + ); + } } } + return accountInfo; } diff --git a/lib/msal-common/src/cache/entities/AccountEntity.ts b/lib/msal-common/src/cache/entities/AccountEntity.ts index ca013f0790..4e95c74e2a 100644 --- a/lib/msal-common/src/cache/entities/AccountEntity.ts +++ b/lib/msal-common/src/cache/entities/AccountEntity.ts @@ -7,7 +7,11 @@ import { Separators, CacheAccountType } from "../../utils/Constants"; import { Authority } from "../../authority/Authority"; import { ICrypto } from "../../crypto/ICrypto"; import { ClientInfo, buildClientInfo } from "../../account/ClientInfo"; -import { AccountInfo } from "../../account/AccountInfo"; +import { + AccountInfo, + TenantProfile, + buildTenantProfileFromIdTokenClaims, +} from "../../account/AccountInfo"; import { createClientAuthError, ClientAuthErrorCodes, @@ -39,6 +43,7 @@ import { ProtocolMode } from "../../authority/ProtocolMode"; * lastModificationTime: last time this entity was modified in the cache * lastModificationApp: * nativeAccountId: Account identifier on the native device + * tenantProfiles: Array of tenant profile objects for each tenant that the account has authenticated with in the browser * } * @internal */ @@ -56,7 +61,7 @@ export class AccountEntity { cloudGraphHostName?: string; msGraphHost?: string; nativeAccountId?: string; - tenants?: string[]; + tenantProfiles?: Array; /** * Generate Account Id key component as per the schema: - @@ -92,7 +97,12 @@ export class AccountEntity { name: this.name, nativeAccountId: this.nativeAccountId, authorityType: this.authorityType, - tenants: this.tenants, + // Deserialize tenant profiles array into a Map + tenantProfiles: new Map( + (this.tenantProfiles || []).map((tenantProfile) => { + return [tenantProfile.tenantId, tenantProfile]; + }) + ), }; } @@ -100,7 +110,7 @@ export class AccountEntity { * Returns true if the account entity is in single tenant format (outdated), false otherwise */ isSingleTenant(): boolean { - return !this.tenants; + return !this.tenantProfiles; } /** @@ -131,7 +141,7 @@ export class AccountEntity { msGraphHost?: string; environment?: string; nativeAccountId?: string; - tenants?: string[]; + tenantProfiles?: Array; }, authority: Authority, cryptoObj?: ICrypto @@ -173,12 +183,6 @@ export class AccountEntity { getTenantIdFromIdTokenClaims(accountDetails.idTokenClaims) || ""; - if (accountDetails.tenants) { - account.tenants = accountDetails.tenants; - } else { - account.tenants = account.realm ? [account.realm] : []; - } - // How do you account for MSA CID here? account.localAccountId = clientInfo?.uid || @@ -204,6 +208,20 @@ export class AccountEntity { account.cloudGraphHostName = accountDetails.cloudGraphHostName; account.msGraphHost = accountDetails.msGraphHost; + if (accountDetails.tenantProfiles) { + account.tenantProfiles = accountDetails.tenantProfiles; + } else { + const tenantProfiles = []; + if (accountDetails.idTokenClaims) { + const tenantProfile = buildTenantProfileFromIdTokenClaims( + accountDetails.homeAccountId, + accountDetails.idTokenClaims + ); + tenantProfiles.push(tenantProfile); + } + account.tenantProfiles = tenantProfiles; + } + return account; } @@ -235,7 +253,10 @@ export class AccountEntity { account.cloudGraphHostName = cloudGraphHostName; account.msGraphHost = msGraphHost; - account.tenants = accountInfo.tenants; + // Serialize tenant profiles map into an array + account.tenantProfiles = Array.from( + accountInfo.tenantProfiles?.values() || [] + ); return account; } diff --git a/lib/msal-common/src/index.ts b/lib/msal-common/src/index.ts index bda921db94..d8b46047ec 100644 --- a/lib/msal-common/src/index.ts +++ b/lib/msal-common/src/index.ts @@ -31,7 +31,8 @@ export { ClientConfiguration } from "./config/ClientConfiguration"; export { AccountInfo, ActiveAccountFilters, - updateTenantProfile, + TenantProfile, + updateAccountTenantProfileData, } from "./account/AccountInfo"; export * as AuthToken from "./account/AuthToken"; export { diff --git a/lib/msal-common/src/response/ResponseHandler.ts b/lib/msal-common/src/response/ResponseHandler.ts index 936c8b5390..78469b51f9 100644 --- a/lib/msal-common/src/response/ResponseHandler.ts +++ b/lib/msal-common/src/response/ResponseHandler.ts @@ -46,7 +46,11 @@ import { TokenClaims, getTenantIdFromIdTokenClaims, } from "../account/TokenClaims"; -import { AccountInfo, updateTenantProfile } from "../account/AccountInfo"; +import { + AccountInfo, + buildTenantProfileFromIdTokenClaims, + updateAccountTenantProfileData, +} from "../account/AccountInfo"; import * as CacheHelpers from "../cache/utils/CacheHelpers"; /** @@ -546,17 +550,26 @@ export class ResponseHandler { clientInfo: serverTokenResponse.client_info, cloudGraphHostName: authCodePayload?.cloud_graph_host_name, msGraphHost: authCodePayload?.msgraph_host, - tenants: claimsTenantId ? [claimsTenantId] : [], }, authority, this.cryptoObj ); - const tenants = baseAccount.tenants || []; - if (claimsTenantId && !tenants.includes(claimsTenantId)) { - tenants.push(claimsTenantId); + const tenantProfiles = baseAccount.tenantProfiles || []; + + if ( + claimsTenantId && + !tenantProfiles.find((tenantProfile) => { + return tenantProfile.tenantId === claimsTenantId; + }) + ) { + const newTenantProfile = buildTenantProfileFromIdTokenClaims( + this.homeAccountIdentifier, + idTokenClaims + ); + tenantProfiles.push(newTenantProfile); } - baseAccount.tenants = tenants; + baseAccount.tenantProfiles = tenantProfiles; return baseAccount; } @@ -643,8 +656,9 @@ export class ResponseHandler { } const accountInfo: AccountInfo | null = cacheRecord.account - ? updateTenantProfile( + ? updateAccountTenantProfileData( cacheRecord.account.getAccountInfo(), + undefined, idTokenClaims ) : null; diff --git a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/auth.js b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/auth.js index 1919b0e709..6cdcf16816 100644 --- a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/auth.js +++ b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/auth.js @@ -36,7 +36,7 @@ async function pickActiveAccountAndTenantProfile(homeAccount) { } accountId = activeAccount.homeAccountId; showWelcomeMessage(activeAccount); - showTenantProfilePicker(homeAccount.tenants || [], activeAccount); + showTenantProfilePicker(homeAccount.tenantProfiles || new Map(), activeAccount); } async function setActiveAccount(tenantId) { diff --git a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/ui.js b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/ui.js index a42286c33d..f71d1e62db 100644 --- a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/ui.js +++ b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/ui.js @@ -27,13 +27,13 @@ function showWelcomeMessage(activeAccount) { redirectButton.innerHTML = "Sign Out with Redirect"; } -function createTenantProfileButton(tenantProfile, activeTenantId) { +function createTenantProfileButton(tenantId, tenantProfile, activeTenantId) { const tenantIdButton = document.createElement('button'); - const styleClass = (tenantProfile === activeTenantId) ? "btn btn-success" : "btn btn-primary"; + const styleClass = (tenantId === activeTenantId) ? "btn btn-success" : "btn btn-primary"; tenantIdButton.setAttribute('class', styleClass); - tenantIdButton.setAttribute('id', tenantProfile); + tenantIdButton.setAttribute('id', tenantId); tenantIdButton.setAttribute('onClick', "setActiveAccount(this.id)"); - const label = `${tenantProfile}${ tenantProfile.isHomeTenant ? " (Home Tenant)" : "" }`; + const label = `${tenantId}${ tenantProfile.isHomeTenant ? " (Home Tenant)" : "" }`; tenantIdButton.innerHTML = label; return tenantIdButton; } @@ -43,8 +43,8 @@ function showTenantProfilePicker(tenantProfiles, activeAccount) { cardDivTenantProfiles.style.display = 'initial'; pickProfileMessage.innerHTML = "Select a tenant profile to set as the active account"; tenantProfileDiv.innerHTML = ""; - tenantProfiles.forEach(function (profile) { - tenantProfileDiv.appendChild(createTenantProfileButton(profile, activeAccount.tenantId)); + tenantProfiles.forEach(function (tenantProfile, tenantId) { + tenantProfileDiv.appendChild(createTenantProfileButton(tenantId, tenantProfile, activeAccount.tenantId)); tenantProfileDiv.appendChild(document.createElement('br')); tenantProfileDiv.appendChild(document.createElement('br')); }); From 889404181bf06ef57f16a4d2845bb35b5f49e021 Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Wed, 8 Nov 2023 15:46:50 -0800 Subject: [PATCH 48/91] Add migration logic from single-tenant to multi-tenant account entities --- .../src/cache/BrowserCacheManager.ts | 28 +++-- lib/msal-common/src/account/AccountInfo.ts | 16 ++- lib/msal-common/src/authority/Authority.ts | 4 +- lib/msal-common/src/cache/CacheManager.ts | 103 +++++++++++++++--- lib/msal-common/src/index.ts | 1 + .../src/response/ResponseHandler.ts | 7 +- lib/msal-node/src/cache/NodeStorage.ts | 16 ++- lib/msal-node/src/cache/TokenCache.ts | 2 +- .../src/cache/serializer/Deserializer.ts | 6 +- .../src/cache/serializer/Serializer.ts | 6 +- .../src/cache/serializer/SerializerTypes.ts | 2 +- 11 files changed, 150 insertions(+), 41 deletions(-) diff --git a/lib/msal-browser/src/cache/BrowserCacheManager.ts b/lib/msal-browser/src/cache/BrowserCacheManager.ts index 404213e63f..a6c6c75440 100644 --- a/lib/msal-browser/src/cache/BrowserCacheManager.ts +++ b/lib/msal-browser/src/cache/BrowserCacheManager.ts @@ -394,26 +394,40 @@ export class BrowserCacheManager extends CacheManager { * fetch the account entity from the platform cache * @param accountKey */ - getAccount(accountKey: string): AccountEntity | null { + getAccount(accountKey: string, logger?: Logger): AccountEntity | null { this.logger.trace("BrowserCacheManager.getAccount called"); - const account = this.getItem(accountKey); - if (!account) { + const accountEntity = this.getCachedAccountEntity(accountKey); + + return this.updateOutdatedCachedAccount( + accountKey, + accountEntity, + logger + ); + } + + /** + * 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 + */ + getCachedAccountEntity(accountKey: string): AccountEntity | null { + const serializedAccount = this.getItem(accountKey); + if (!serializedAccount) { this.removeAccountKeyFromMap(accountKey); return null; } - const parsedAccount = this.validateAndParseJson(account); + const parsedAccount = this.validateAndParseJson(serializedAccount); if (!parsedAccount || !AccountEntity.isAccountEntity(parsedAccount)) { this.removeAccountKeyFromMap(accountKey); return null; } - const accountEntity = CacheManager.toObject( + return CacheManager.toObject( new AccountEntity(), parsedAccount ); - - return this.updateOutdatedCachedAccount(accountKey, accountEntity); } /** diff --git a/lib/msal-common/src/account/AccountInfo.ts b/lib/msal-common/src/account/AccountInfo.ts index df781ce89f..db9075b4fd 100644 --- a/lib/msal-common/src/account/AccountInfo.ts +++ b/lib/msal-common/src/account/AccountInfo.ts @@ -79,7 +79,7 @@ export function updateAccountTenantProfileData( // ID token claims override passed in account info and tenant profile if (idTokenClaims) { - // @ts-ignore + // eslint-disable-next-line @typescript-eslint/no-unused-vars const { isHomeTenant, ...tenantProfile } = buildTenantProfileFromIdTokenClaims( accountInfo.homeAccountId, @@ -111,12 +111,22 @@ export function buildTenantProfileFromIdTokenClaims( // Downcase to match the realm case-insensitive comparison requirements tenantId = claimsTenantId.toLowerCase(); } - const homeTenantId = homeAccountId.split(".")[1]; return { tenantId: tenantId || "", localAccountId: oid || sub || "", name: name, - isHomeTenant: tenantId === homeTenantId, + isHomeTenant: tenantIdMatchesHomeTenant(tenantId, homeAccountId), }; } + +export function tenantIdMatchesHomeTenant( + tenantId?: string, + homeAccountId?: string +): boolean { + return ( + !!tenantId && + !!homeAccountId && + tenantId === homeAccountId.split(".")[1] + ); +} diff --git a/lib/msal-common/src/authority/Authority.ts b/lib/msal-common/src/authority/Authority.ts index 8951a9e859..4d246cfbf7 100644 --- a/lib/msal-common/src/authority/Authority.ts +++ b/lib/msal-common/src/authority/Authority.ts @@ -1066,12 +1066,12 @@ export class Authority { const matches = this.authorityOptions.knownAuthorities.filter( (authority) => { return ( + authority && UrlString.getDomainFromUrl(authority).toLowerCase() === - this.hostnameAndPort + this.hostnameAndPort ); } ); - return matches.length > 0; } diff --git a/lib/msal-common/src/cache/CacheManager.ts b/lib/msal-common/src/cache/CacheManager.ts index 658881c36e..9da5d60178 100644 --- a/lib/msal-common/src/cache/CacheManager.ts +++ b/lib/msal-common/src/cache/CacheManager.ts @@ -35,6 +35,7 @@ import { } from "../error/ClientAuthError"; import { AccountInfo, + tenantIdMatchesHomeTenant, updateAccountTenantProfileData, } from "../account/AccountInfo"; import { AppMetadataEntity } from "./entities/AppMetadataEntity"; @@ -80,7 +81,15 @@ export abstract class CacheManager implements ICacheManager { * fetch the account entity from the platform cache * @param accountKey */ - abstract getAccount(accountKey: string): AccountEntity | null; + abstract getAccount( + accountKey: string, + logger?: Logger + ): AccountEntity | null; + + /** + * Returns deserialized account if found in the cache, otherwiser returns null + */ + abstract getCachedAccountEntity(accountKey: string): AccountEntity | null; /** * set account entity in the platform cache @@ -526,7 +535,10 @@ export abstract class CacheManager implements ICacheManager { return; } - const entity: AccountEntity | null = this.getAccount(cacheKey); + const entity: AccountEntity | null = this.getAccount( + cacheKey, + this.commonLogger + ); if (!entity) { return; @@ -859,7 +871,7 @@ export abstract class CacheManager implements ICacheManager { * @param account */ async removeAccount(accountKey: string): Promise { - const account = this.getAccount(accountKey); + const account = this.getAccount(accountKey, this.commonLogger); if (!account) { return; } @@ -898,32 +910,84 @@ export abstract class CacheManager implements ICacheManager { } /** - * Replaces the single-tenant account with a new account object that contains the array of tenants. - * Returns the updated account entity. + * Migrates a single-tenant account and all it's associated alternate cross-tenant account objects in the + * cache into a condensed multi-tenant account object with tenant profiles. * @param accountKey * @param accountEntity + * @param logger * @returns */ protected updateOutdatedCachedAccount( accountKey: string, - accountEntity: AccountEntity - ): AccountEntity { - if (accountEntity.isSingleTenant()) { + accountEntity: AccountEntity | null, + logger?: Logger + ): AccountEntity | null { + // Only update if account entity is defined and has no tenantProfiles object (is outdated) + if (accountEntity && accountEntity.isSingleTenant()) { + this.commonLogger?.verbose( + "updateOutdatedCachedAccount: Found a single-tenant (outdated) account entity in the cache, migrating to multi-tenant account entity" + ); + + // Get keys of all accounts belonging to user + const matchingAccountKeys = this.getAccountKeys().filter( + (key: string) => { + return key.startsWith(accountEntity.homeAccountId); + } + ); + + // Get all account entities belonging to user + const accountsToMerge: AccountEntity[] = []; + matchingAccountKeys.forEach((key: string) => { + const account = this.getCachedAccountEntity(key); + if (account) { + accountsToMerge.push(account); + } + }); + + // Set base account to home account if available, any account if not + const baseAccount = + accountsToMerge.find((account) => { + return tenantIdMatchesHomeTenant( + account.realm, + account.homeAccountId + ); + }) || accountsToMerge[0]; + + // Populate tenant profiles built from each account entity belonging to the user + baseAccount.tenantProfiles = accountsToMerge.map( + (account: AccountEntity) => { + return { + tenantId: account.realm, + localAccountId: account.localAccountId, + name: account.name, + isHomeTenant: tenantIdMatchesHomeTenant( + account.realm, + account.homeAccountId + ), + }; + } + ); + const updatedAccount = Object.assign(new AccountEntity(), { - ...accountEntity, - tenants: [accountEntity.realm], + ...baseAccount, }); - // If the cache keys don't match, the key is also outdated and should be removed, if they match it's fine to just override the value - if (updatedAccount.generateAccountKey() !== accountKey) { - this.removeOutdatedAccount(accountKey); - } + const newAccountKey = updatedAccount.generateAccountKey(); + + // Clear cache of legacy account objects that have been collpsed into tenant profiles + matchingAccountKeys.forEach((key: string) => { + if (key !== newAccountKey) { + this.removeOutdatedAccount(accountKey); + } + }); + + // Cache updated account object this.setAccount(updatedAccount); - this.commonLogger.verbose( - "Updated an outdated account entity in the cache" - ); + logger?.verbose("Updated an outdated account entity in the cache"); return updatedAccount; } + + // No update is necessary return accountEntity; } @@ -1037,7 +1101,7 @@ export abstract class CacheManager implements ICacheManager { readAccountFromCache(account: AccountInfo): AccountEntity | null { const accountKey: string = AccountEntity.generateAccountCacheKey(account); - return this.getAccount(accountKey); + return this.getAccount(accountKey, this.commonLogger); } /** @@ -1822,6 +1886,9 @@ export class DefaultStorageClass extends CacheManager { getAccount(): AccountEntity { throw createClientAuthError(ClientAuthErrorCodes.methodNotImplemented); } + getCachedAccountEntity(): AccountEntity | null { + throw createClientAuthError(ClientAuthErrorCodes.methodNotImplemented); + } setIdTokenCredential(): void { throw createClientAuthError(ClientAuthErrorCodes.methodNotImplemented); } diff --git a/lib/msal-common/src/index.ts b/lib/msal-common/src/index.ts index d8b46047ec..b0ed697f84 100644 --- a/lib/msal-common/src/index.ts +++ b/lib/msal-common/src/index.ts @@ -33,6 +33,7 @@ export { ActiveAccountFilters, TenantProfile, updateAccountTenantProfileData, + tenantIdMatchesHomeTenant, } from "./account/AccountInfo"; export * as AuthToken from "./account/AuthToken"; export { diff --git a/lib/msal-common/src/response/ResponseHandler.ts b/lib/msal-common/src/response/ResponseHandler.ts index 78469b51f9..d1702d842a 100644 --- a/lib/msal-common/src/response/ResponseHandler.ts +++ b/lib/msal-common/src/response/ResponseHandler.ts @@ -344,7 +344,7 @@ export class ResponseHandler { cacheRecord.account ) { const key = cacheRecord.account.generateAccountKey(); - const account = this.cacheStorage.getAccount(key); + const account = this.cacheStorage.getAccount(key, this.logger); if (!account) { this.logger.warning( "Account used to refresh tokens not in persistence, refreshed tokens will not be stored in the cache" @@ -538,7 +538,10 @@ export class ResponseHandler { let cachedAccount: AccountEntity | null = null; if (baseAccountKey) { - cachedAccount = this.cacheStorage.getAccount(baseAccountKey); + cachedAccount = this.cacheStorage.getAccount( + baseAccountKey, + this.logger + ); } const baseAccount = diff --git a/lib/msal-node/src/cache/NodeStorage.ts b/lib/msal-node/src/cache/NodeStorage.ts index 3cac844d74..f902690945 100644 --- a/lib/msal-node/src/cache/NodeStorage.ts +++ b/lib/msal-node/src/cache/NodeStorage.ts @@ -218,16 +218,22 @@ export class NodeStorage extends CacheManager { * @param accountKey - lookup key to fetch cache type AccountEntity */ getAccount(accountKey: string): AccountEntity | null { - const account = this.getItem(accountKey); - - const accountEntity = Object.assign(new AccountEntity(), account); - - if (AccountEntity.isAccountEntity(accountEntity)) { + const accountEntity = this.getCachedAccountEntity(accountKey); + if (accountEntity && AccountEntity.isAccountEntity(accountEntity)) { return this.updateOutdatedCachedAccount(accountKey, accountEntity); } return null; } + /** + * Reads account from cache, builds it into an account entity and returns it. + * @param accountKey + * @returns + */ + getCachedAccountEntity(accountKey: string): AccountEntity | null { + return Object.assign(new AccountEntity(), this.getItem(accountKey)); + } + /** * set account entity * @param account - cache value to be set of type AccountEntity diff --git a/lib/msal-node/src/cache/TokenCache.ts b/lib/msal-node/src/cache/TokenCache.ts index 2260e7c764..6d5f3655fa 100644 --- a/lib/msal-node/src/cache/TokenCache.ts +++ b/lib/msal-node/src/cache/TokenCache.ts @@ -124,7 +124,7 @@ export class TokenCache implements ISerializableTokenCache, ITokenCache { let cacheContext; try { if (this.persistence) { - cacheContext = new TokenCacheContext(this, false); + cacheContext = new TokenCacheContext(this, true); await this.persistence.beforeCacheAccess(cacheContext); } return this.storage.getAllAccounts(); diff --git a/lib/msal-node/src/cache/serializer/Deserializer.ts b/lib/msal-node/src/cache/serializer/Deserializer.ts index f5ef6c0a91..070ee50fdc 100644 --- a/lib/msal-node/src/cache/serializer/Deserializer.ts +++ b/lib/msal-node/src/cache/serializer/Deserializer.ts @@ -63,7 +63,11 @@ export class Deserializer { clientInfo: serializedAcc.client_info, lastModificationTime: serializedAcc.last_modification_time, lastModificationApp: serializedAcc.last_modification_app, - tenants: serializedAcc.tenants, + tenantProfiles: serializedAcc.tenantProfiles?.map( + (tenantProfile) => { + return JSON.parse(tenantProfile); + } + ), }; const account: AccountEntity = new AccountEntity(); CacheManager.toObject(account, mappedAcc); diff --git a/lib/msal-node/src/cache/serializer/Serializer.ts b/lib/msal-node/src/cache/serializer/Serializer.ts index 89380113c5..908cb50ad4 100644 --- a/lib/msal-node/src/cache/serializer/Serializer.ts +++ b/lib/msal-node/src/cache/serializer/Serializer.ts @@ -50,7 +50,11 @@ export class Serializer { client_info: accountEntity.clientInfo, last_modification_time: accountEntity.lastModificationTime, last_modification_app: accountEntity.lastModificationApp, - tenants: accountEntity.tenants, + tenantProfiles: accountEntity.tenantProfiles?.map( + (tenantProfile) => { + return JSON.stringify(tenantProfile); + } + ), }; }); diff --git a/lib/msal-node/src/cache/serializer/SerializerTypes.ts b/lib/msal-node/src/cache/serializer/SerializerTypes.ts index 517a8f3529..1ae0afdabe 100644 --- a/lib/msal-node/src/cache/serializer/SerializerTypes.ts +++ b/lib/msal-node/src/cache/serializer/SerializerTypes.ts @@ -57,7 +57,7 @@ export type SerializedAccountEntity = { client_info?: string; last_modification_time?: string; last_modification_app?: string; - tenants?: string[]; + tenantProfiles?: string[]; }; /** From f9dae3c1c65537d7d8ef0305e46b58fffbb40a17 Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Sat, 11 Nov 2023 20:24:51 -0800 Subject: [PATCH 49/91] Update account APIs to return all tenant profiles as overriden account info objects --- lib/msal-common/src/account/AccountInfo.ts | 87 ++-- lib/msal-common/src/cache/CacheManager.ts | 269 +++++++--- lib/msal-common/src/cache/utils/CacheTypes.ts | 8 +- .../test/account/AccountInfo.spec.ts | 318 ++++++++++++ .../test/account/AuthToken.spec.ts | 24 +- .../test/authority/AuthorityMetadata.spec.ts | 1 - .../test/cache/CacheManager.spec.ts | 476 +++++++++++------- lib/msal-common/test/cache/MockCache.ts | 128 +++-- .../test/client/ClientTestUtils.ts | 7 +- .../test/client/SilentFlowClient.spec.ts | 51 +- .../test/test_kit/StringConstants.ts | 31 +- .../app/multi-tenant/auth.js | 18 +- .../app/multi-tenant/authConfig.js | 2 +- 13 files changed, 996 insertions(+), 424 deletions(-) create mode 100644 lib/msal-common/test/account/AccountInfo.spec.ts diff --git a/lib/msal-common/src/account/AccountInfo.ts b/lib/msal-common/src/account/AccountInfo.ts index db9075b4fd..f8aa19dee9 100644 --- a/lib/msal-common/src/account/AccountInfo.ts +++ b/lib/msal-common/src/account/AccountInfo.ts @@ -60,73 +60,76 @@ export type ActiveAccountFilters = { tenantId?: string; }; +/** + * Returns true if tenantId matches the utid portion of homeAccountId + * @param tenantId + * @param homeAccountId + * @returns + */ +export function tenantIdMatchesHomeTenant( + tenantId?: string, + homeAccountId?: string +): boolean { + return ( + !!tenantId && + !!homeAccountId && + tenantId === homeAccountId.split(".")[1] + ); +} + +export function buildTenantProfileFromIdTokenClaims( + homeAccountId: string, + idTokenClaims: TokenClaims +): TenantProfile { + const { oid, sub, tid, name, tfp, acr } = idTokenClaims; + + const tenantId = tid || tfp || acr || ""; + + return { + tenantId: tenantId.toLowerCase(), + localAccountId: oid || sub || "", + name: name, + isHomeTenant: tenantIdMatchesHomeTenant(tenantId, homeAccountId), + }; +} + /** * Replaces account info that varies by tenant profile sourced from the ID token claims passed in with the tenant-specific account info - * @param accountInfo + * @param baseAccountInfo * @param idTokenClaims * @returns */ export function updateAccountTenantProfileData( - accountInfo: AccountInfo, + baseAccountInfo: AccountInfo, tenantProfile?: TenantProfile, idTokenClaims?: TokenClaims ): AccountInfo { - let updatedAccountInfo = accountInfo; + let updatedAccountInfo = baseAccountInfo; // Tenant Profile overrides passed in account info if (tenantProfile) { - updatedAccountInfo = { ...accountInfo, ...tenantProfile }; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { isHomeTenant, ...tenantProfileOverride } = tenantProfile; + updatedAccountInfo = { ...baseAccountInfo, ...tenantProfileOverride }; } // ID token claims override passed in account info and tenant profile if (idTokenClaims) { + // Ignore isHomeTenant, loginHint, and sid which are part of tenant profile but not base account info // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { isHomeTenant, ...tenantProfile } = + const { isHomeTenant, ...claimsSourcedTenantProfile } = buildTenantProfileFromIdTokenClaims( - accountInfo.homeAccountId, + baseAccountInfo.homeAccountId, idTokenClaims ); updatedAccountInfo = { ...updatedAccountInfo, - ...tenantProfile, + ...claimsSourcedTenantProfile, + idTokenClaims: idTokenClaims, }; - updatedAccountInfo.idTokenClaims = idTokenClaims; return updatedAccountInfo; } - return accountInfo; -} - -export function buildTenantProfileFromIdTokenClaims( - homeAccountId: string, - idTokenClaims: TokenClaims -): TenantProfile { - const { oid, sub, tid, name, tfp, acr } = idTokenClaims; - - const claimsTenantId = tid || tfp || acr; - let tenantId: string | undefined; - - if (claimsTenantId) { - // Downcase to match the realm case-insensitive comparison requirements - tenantId = claimsTenantId.toLowerCase(); - } - - return { - tenantId: tenantId || "", - localAccountId: oid || sub || "", - name: name, - isHomeTenant: tenantIdMatchesHomeTenant(tenantId, homeAccountId), - }; -} - -export function tenantIdMatchesHomeTenant( - tenantId?: string, - homeAccountId?: string -): boolean { - return ( - !!tenantId && - !!homeAccountId && - tenantId === homeAccountId.split(".")[1] - ); + return updatedAccountInfo; } diff --git a/lib/msal-common/src/cache/CacheManager.ts b/lib/msal-common/src/cache/CacheManager.ts index 9da5d60178..496f26eaca 100644 --- a/lib/msal-common/src/cache/CacheManager.ts +++ b/lib/msal-common/src/cache/CacheManager.ts @@ -35,6 +35,7 @@ import { } from "../error/ClientAuthError"; import { AccountInfo, + TenantProfile, tenantIdMatchesHomeTenant, updateAccountTenantProfileData, } from "../account/AccountInfo"; @@ -264,15 +265,16 @@ export abstract class CacheManager implements ICacheManager { } /** - * Gets accountInfo object based on provided filters + * Gets first tenanted AccountInfo object found based on provided filters */ getAccountInfoFilteredBy(accountFilter: AccountFilter): AccountInfo | null { const allAccounts = this.getAllAccounts(accountFilter); if (allAccounts.length > 1) { - // If one or more accounts are found, further filter to the first account that has an ID token - return allAccounts.filter((account) => { - return !!account.idTokenClaims; - })[0]; + // If one or more accounts are found, prioritize accounts that have an ID token + const sortedAccounts = allAccounts.sort((account) => { + return account.idTokenClaims ? -1 : 1; + }); + return sortedAccounts[0]; } else if (allAccounts.length === 1) { // If only one account is found, return it regardless of whether a matching ID token was found return allAccounts[0]; @@ -306,93 +308,159 @@ export abstract class CacheManager implements ICacheManager { cachedAccounts: AccountEntity[], accountFilter?: AccountFilter ): AccountInfo[] { - const validAccounts: AccountInfo[] = []; - cachedAccounts.forEach((accountEntity) => { - const tenantProfile: AccountInfo | null = - this.getAccountInfoForTenantProfile( - accountEntity, - accountFilter - ); - if (tenantProfile) { - validAccounts.push(tenantProfile); - } + return cachedAccounts.flatMap((accountEntity) => { + return this.getAccountInfoForTenantProfiles( + accountEntity, + accountFilter + ); }); - return validAccounts; } - private getAccountInfoForTenantProfile( + private getAccountInfoForTenantProfiles( accountEntity: AccountEntity, accountFilter?: AccountFilter - ): AccountInfo | null { - const tenantProfileFilter: TenantProfileFilter = { - localAccountId: accountFilter?.localAccountId, - loginHint: accountFilter?.loginHint, - name: accountFilter?.name, - sid: accountFilter?.sid, - }; - return this.getAccountInfoFromEntity( + ): AccountInfo[] { + const tenantProfileFilter: TenantProfileFilter | undefined = + accountFilter + ? { + localAccountId: accountFilter.localAccountId, + loginHint: accountFilter.loginHint, + name: accountFilter.name, + username: accountFilter.username, + sid: accountFilter.sid, + isHomeTenant: accountFilter.isHomeTenant, + } + : undefined; + + return this.getTenantProfilesFromAccountEntity( accountEntity, accountFilter?.tenantId, tenantProfileFilter ); } - private getAccountInfoFromEntity( - accountEntity: AccountEntity, - targetTenantId?: string, + private getTenantedAccountInfoByFilter( + accountInfo: AccountInfo, + tokenKeys: TokenKeys, + tenantProfile: TenantProfile, tenantProfileFilter?: TenantProfileFilter ): AccountInfo | null { - // At this point, accoutnInfo only contains base/home account data, so it's not a "real" account - const accountInfo = accountEntity.getAccountInfo(); + let tenantedAccountInfo: AccountInfo | null = null; + let idTokenClaims: TokenClaims | undefined; + + if (tenantProfileFilter) { + if ( + !this.tenantProfileMatchesFilter( + tenantProfile, + tenantProfileFilter + ) + ) { + return null; + } + } + + const idToken = this.getIdToken( + accountInfo, + tokenKeys, + tenantProfile.tenantId + ); - /** - * An ID token matching the account entity and tenant ID must exist in the cache - * for the account to be a valid authenticated tenant profile. No matching ID token - * means the account being requested doesn't technically exist in the cache. - */ - const idToken = this.getIdToken(accountInfo, undefined, targetTenantId); if (idToken) { - const idTokenClaims = extractTokenClaims( + idTokenClaims = extractTokenClaims( idToken.secret, this.cryptoImpl.base64Decode ); if ( - this.idTokenClaimsMatchTenantProfileFilter( + !this.idTokenClaimsMatchTenantProfileFilter( idTokenClaims, tenantProfileFilter ) ) { - return updateAccountTenantProfileData( - accountInfo, - undefined, - idTokenClaims - ); + // ID token sourced claims don't match so this tenant profile is not a match + return null; } - } else { - if (targetTenantId) { - this.commonLogger.verbose( - "getAccountInfoFromEntity(): No matching ID token found in cache for given tenant ID, falling back to cached tenant profiles" - ); - const tenantProfile = - accountInfo.tenantProfiles?.get(targetTenantId); - if (tenantProfile) { - this.commonLogger.verbose( - "getAccountInfoFromEntity(): Found cached tenant profile matching given tenant ID, updating account info with tenant profile data" - ); - return updateAccountTenantProfileData( - accountInfo, - tenantProfile - ); - } else { - this.commonLogger.verbose( - "getAccountInfoFromEntity(): No matching ID token or tenant profile found in cache for given tenant ID. Returning base account." - ); - } + } + + // Expand tenant profile into account info based on matching tenant profile and if available matching ID token claims + tenantedAccountInfo = updateAccountTenantProfileData( + accountInfo, + tenantProfile, + idTokenClaims + ); + + return tenantedAccountInfo; + } + + private getTenantProfilesFromAccountEntity( + accountEntity: AccountEntity, + targetTenantId?: string, + tenantProfileFilter?: TenantProfileFilter + ): AccountInfo[] { + const accountInfo = accountEntity.getAccountInfo(); + let searchTenantProfiles: Map = + accountInfo.tenantProfiles || new Map(); + const tokenKeys = this.getTokenKeys(); + + // If a tenant ID was provided, only return the tenant profile for that tenant ID if it exists + if (targetTenantId) { + const tenantProfile = searchTenantProfiles.get(targetTenantId); + if (tenantProfile) { + // Reduce search field to just this tenant profile + searchTenantProfiles = new Map([ + [targetTenantId, tenantProfile], + ]); + } else { + // No tenant profile for search tenant ID, return empty array + return []; } } - return accountInfo; + const matchingTenantProfiles: AccountInfo[] = []; + searchTenantProfiles.forEach((tenantProfile: TenantProfile) => { + const tenantedAccountInfo = this.getTenantedAccountInfoByFilter( + accountInfo, + tokenKeys, + tenantProfile, + tenantProfileFilter + ); + if (tenantedAccountInfo) { + matchingTenantProfiles.push(tenantedAccountInfo); + } + }); + + return matchingTenantProfiles; + } + + private tenantProfileMatchesFilter( + tenantProfile: TenantProfile, + tenantProfileFilter: TenantProfileFilter + ): boolean { + if ( + !!tenantProfileFilter.localAccountId && + !this.matchLocalAccountIdFromTenantProfile( + tenantProfile, + tenantProfileFilter.localAccountId + ) + ) { + return false; + } + + if ( + !!tenantProfileFilter.name && + !(tenantProfile.name === tenantProfileFilter.name) + ) { + return false; + } + + if ( + tenantProfileFilter.isHomeTenant !== undefined && + !(tenantProfile.isHomeTenant === tenantProfileFilter.isHomeTenant) + ) { + return false; + } + + return true; } private idTokenClaimsMatchTenantProfileFilter( @@ -403,7 +471,7 @@ export abstract class CacheManager implements ICacheManager { if (tenantProfileFilter) { if ( !!tenantProfileFilter.localAccountId && - !this.matchLocalAccountId( + !this.matchLocalAccountIdFromTokenClaims( idTokenClaims, tenantProfileFilter.localAccountId ) @@ -413,7 +481,7 @@ export abstract class CacheManager implements ICacheManager { if ( !!tenantProfileFilter.loginHint && - !this.matchLoginHint( + !this.matchLoginHintFromTokenClaims( idTokenClaims, tenantProfileFilter.loginHint ) @@ -421,6 +489,16 @@ export abstract class CacheManager implements ICacheManager { return false; } + if ( + !!tenantProfileFilter.username && + !this.matchUsername( + idTokenClaims.preferred_username, + tenantProfileFilter.username + ) + ) { + return false; + } + if ( !!tenantProfileFilter.name && !this.matchName(idTokenClaims, tenantProfileFilter.name) @@ -540,6 +618,8 @@ export abstract class CacheManager implements ICacheManager { this.commonLogger ); + // Match base account fields + if (!entity) { return; } @@ -553,7 +633,7 @@ export abstract class CacheManager implements ICacheManager { if ( !!accountFilter.username && - !this.matchUsername(entity, accountFilter.username) + !this.matchUsername(entity.username, accountFilter.username) ) { return; } @@ -589,6 +669,26 @@ export abstract class CacheManager implements ICacheManager { return; } + // If at least one tenant profile matches the tenant profile filter, add the account to the list of matching accounts + const tenantProfileFilter: TenantProfileFilter = { + localAccountId: accountFilter?.localAccountId, + name: accountFilter?.name, + }; + + const matchingTenantProfiles = entity.tenantProfiles?.filter( + (tenantProfile: TenantProfile) => { + return this.tenantProfileMatchesFilter( + tenantProfile, + tenantProfileFilter + ); + } + ); + + if (matchingTenantProfiles && matchingTenantProfiles.length === 0) { + // No tenant profile for this account matches filter, don't add to list of matching accounts + return; + } + matchingAccounts.push(entity); }); @@ -1607,14 +1707,21 @@ export abstract class CacheManager implements ICacheManager { * @param localAccountId * @returns */ - private matchLocalAccountId( - claims: TokenClaims, + private matchLocalAccountIdFromTokenClaims( + tokenClaims: TokenClaims, localAccountId: string ): boolean { - const idTokenLocalAccountId = claims.oid || claims.sub; + const idTokenLocalAccountId = tokenClaims.oid || tokenClaims.sub; return localAccountId === idTokenLocalAccountId; } + private matchLocalAccountIdFromTenantProfile( + tenantProfile: TenantProfile, + localAccountId: string + ): boolean { + return tenantProfile.localAccountId === localAccountId; + } + /** * helper to match names * @param entity @@ -1631,10 +1738,14 @@ export abstract class CacheManager implements ICacheManager { * @param username * @returns */ - private matchUsername(entity: AccountEntity, username: string): boolean { + private matchUsername( + cachedUsername?: string, + filterUsername?: string + ): boolean { return !!( - typeof entity.username === "string" && - username.toLowerCase() === entity.username.toLowerCase() + cachedUsername && + typeof cachedUsername === "string" && + filterUsername?.toLowerCase() === cachedUsername.toLowerCase() ); } @@ -1762,19 +1873,19 @@ export abstract class CacheManager implements ICacheManager { * @param loginHint * @returns */ - private matchLoginHint( - idTokenClaims: TokenClaims, + private matchLoginHintFromTokenClaims( + tokenClaims: TokenClaims, loginHint: string ): boolean { - if (idTokenClaims.login_hint === loginHint) { + if (tokenClaims.login_hint === loginHint) { return true; } - if (idTokenClaims.preferred_username === loginHint) { + if (tokenClaims.preferred_username === loginHint) { return true; } - if (idTokenClaims.upn === loginHint) { + if (tokenClaims.upn === loginHint) { return true; } @@ -1783,7 +1894,7 @@ export abstract class CacheManager implements ICacheManager { /** * Helper to match sid - * @param idTokenClaims + * @param entity * @param sid * @returns true if the sid claim is present and matches the filter */ diff --git a/lib/msal-common/src/cache/utils/CacheTypes.ts b/lib/msal-common/src/cache/utils/CacheTypes.ts index e083ba76d8..f181ef4310 100644 --- a/lib/msal-common/src/cache/utils/CacheTypes.ts +++ b/lib/msal-common/src/cache/utils/CacheTypes.ts @@ -60,11 +60,17 @@ export type AccountFilter = Omit< realm?: string; loginHint?: string; sid?: string; + isHomeTenant?: boolean; }; export type TenantProfileFilter = Pick< AccountFilter, - "localAccountId" | "loginHint" | "name" | "sid" + | "localAccountId" + | "loginHint" + | "name" + | "sid" + | "isHomeTenant" + | "username" >; /** diff --git a/lib/msal-common/test/account/AccountInfo.spec.ts b/lib/msal-common/test/account/AccountInfo.spec.ts new file mode 100644 index 0000000000..35975185e7 --- /dev/null +++ b/lib/msal-common/test/account/AccountInfo.spec.ts @@ -0,0 +1,318 @@ +import { AccountEntity } from "../../src"; +import * as AccountInfo from "../../src/account/AccountInfo"; +import { buildAccountFromIdTokenClaims } from "../cache/MockCache"; +import { + ID_TOKEN_ALT_CLAIMS, + ID_TOKEN_CLAIMS, + ID_TOKEN_EXTRA_CLAIMS, + TEST_ACCOUNT_INFO, + TEST_DATA_CLIENT_INFO, +} from "../test_kit/StringConstants"; + +describe("AccountInfo Unit Tests", () => { + describe("tenantIdMatchesHomeTenant()", () => { + const { TEST_UID, TEST_UTID } = TEST_DATA_CLIENT_INFO; + const HOME_ACCOUNT_ID = `${TEST_UID}.${TEST_UTID}`; + it("returns true if tenantId passed in matches utid portion of homeAccountId", () => { + expect( + AccountInfo.tenantIdMatchesHomeTenant( + TEST_UTID, + HOME_ACCOUNT_ID + ) + ).toBe(true); + }); + + it("returns false if tenantId passed in does not match the utid portion of homeAccountId", () => { + const differentTenantId = "different-tenant-id"; + expect( + AccountInfo.tenantIdMatchesHomeTenant( + differentTenantId, + HOME_ACCOUNT_ID + ) + ).toBe(false); + }); + + it("returns false if tenantId passed in undefined", () => { + expect( + AccountInfo.tenantIdMatchesHomeTenant(undefined, "uid.utid") + ).toBe(false); + }); + + it("returns false if homeAccountId passed in undefined", () => { + expect( + AccountInfo.tenantIdMatchesHomeTenant("utid", undefined) + ).toBe(false); + }); + }); + + describe("buildTenantProfileFromIdTokenClaims()", () => { + describe("correctly sets tenantId", () => { + it("from the tid claim when present", () => { + const idTokenClaims = { + tid: ID_TOKEN_CLAIMS.tid, + tfp: ID_TOKEN_EXTRA_CLAIMS.tfp, + acr: ID_TOKEN_EXTRA_CLAIMS.acr, + }; + const tenantProfile = + AccountInfo.buildTenantProfileFromIdTokenClaims( + TEST_ACCOUNT_INFO.homeAccountId, + idTokenClaims + ); + expect(tenantProfile.tenantId).toEqual(idTokenClaims.tid); + expect(tenantProfile.tenantId).not.toEqual(idTokenClaims.tfp); + expect(tenantProfile.tenantId).not.toEqual(idTokenClaims.acr); + expect(tenantProfile.tenantId).not.toEqual(""); + }); + it("from the tfp claim (downcased) when present and tid not present", () => { + const idTokenClaims = { + tfp: ID_TOKEN_EXTRA_CLAIMS.tfp, + acr: ID_TOKEN_EXTRA_CLAIMS.acr, + }; + const tenantProfile = + AccountInfo.buildTenantProfileFromIdTokenClaims( + TEST_ACCOUNT_INFO.homeAccountId, + idTokenClaims + ); + expect(tenantProfile.tenantId).toEqual( + idTokenClaims.tfp.toLowerCase() + ); + expect(tenantProfile.tenantId).not.toEqual(idTokenClaims.acr); + expect(tenantProfile.tenantId).not.toEqual(""); + }); + + it("from the acr claim (downcased) when present but tid and tfp not present", () => { + const idTokenClaims = { + acr: ID_TOKEN_EXTRA_CLAIMS.acr, + }; + const tenantProfile = + AccountInfo.buildTenantProfileFromIdTokenClaims( + TEST_ACCOUNT_INFO.homeAccountId, + idTokenClaims + ); + expect(tenantProfile.tenantId).toEqual( + idTokenClaims.acr.toLowerCase() + ); + expect(tenantProfile.tenantId).not.toEqual(""); + }); + + it("falls back to empty string when tid, tfp and acr claims not present", () => { + const idTokenClaims = { iss: ID_TOKEN_CLAIMS.iss }; + const tenantProfile = + AccountInfo.buildTenantProfileFromIdTokenClaims( + TEST_ACCOUNT_INFO.homeAccountId, + idTokenClaims + ); + expect(tenantProfile.tenantId).toEqual(""); + }); + }); + + describe("correctly sets localAccountId", () => { + it("from the oid claim when present", () => { + const idTokenClaims = { + oid: ID_TOKEN_CLAIMS.oid, + sub: ID_TOKEN_CLAIMS.sub, + }; + const tenantProfile = + AccountInfo.buildTenantProfileFromIdTokenClaims( + TEST_ACCOUNT_INFO.homeAccountId, + idTokenClaims + ); + expect(tenantProfile.localAccountId).toEqual(idTokenClaims.oid); + expect(tenantProfile.localAccountId).not.toEqual( + idTokenClaims.sub + ); + expect(tenantProfile.localAccountId).not.toEqual(""); + }); + + it("from sub claim when present and oid not present", () => { + const idTokenClaims = { + sub: ID_TOKEN_CLAIMS.sub, + }; + const tenantProfile = + AccountInfo.buildTenantProfileFromIdTokenClaims( + TEST_ACCOUNT_INFO.homeAccountId, + idTokenClaims + ); + expect(tenantProfile.localAccountId).toEqual(idTokenClaims.sub); + expect(tenantProfile.localAccountId).not.toEqual(""); + }); + + it("falls back to empty string when oid and sub claims are not present", () => { + const idTokenClaims = { + iss: ID_TOKEN_CLAIMS.iss, + }; + const tenantProfile = + AccountInfo.buildTenantProfileFromIdTokenClaims( + TEST_ACCOUNT_INFO.homeAccountId, + idTokenClaims + ); + expect(tenantProfile.localAccountId).toEqual(""); + }); + }); + + describe("correctly sets name", () => { + it("from the name claim when present", () => { + const idTokenClaims = { + name: ID_TOKEN_CLAIMS.name, + }; + const tenantProfile = + AccountInfo.buildTenantProfileFromIdTokenClaims( + TEST_ACCOUNT_INFO.homeAccountId, + idTokenClaims + ); + expect(tenantProfile.name).toEqual(idTokenClaims.name); + expect(tenantProfile.name).not.toEqual(""); + }); + + it("set to undefined when name claim not present", () => { + const idTokenClaims = { + iss: ID_TOKEN_CLAIMS.iss, + }; + const tenantProfile = + AccountInfo.buildTenantProfileFromIdTokenClaims( + TEST_ACCOUNT_INFO.homeAccountId, + idTokenClaims + ); + expect(tenantProfile.name).toBeUndefined(); + }); + }); + + describe("correctly sets isHomeTenant", () => { + const { TEST_UID, TEST_UTID } = TEST_DATA_CLIENT_INFO; + const HOME_ACCOUNT_ID = `${TEST_UID}.${TEST_UTID}`; + + it("to true when tenantId matches utid portion of homeAccountId", () => { + const idTokenClaims = { tid: TEST_UTID }; + const tenantProfile = + AccountInfo.buildTenantProfileFromIdTokenClaims( + HOME_ACCOUNT_ID, + idTokenClaims + ); + expect(tenantProfile.isHomeTenant).toBe(true); + }); + + it("to false when tenantId does not match utid portion of homeAccountId", () => { + const idTokenClaims = { tid: "different-tenant-id" }; + const tenantProfile = + AccountInfo.buildTenantProfileFromIdTokenClaims( + HOME_ACCOUNT_ID, + idTokenClaims + ); + expect(tenantProfile.isHomeTenant).toBe(false); + }); + + it("to false when tenantId is undefined utid portion of homeAccountId", () => { + const idTokenClaims = { iss: ID_TOKEN_CLAIMS.iss }; + const tenantProfile = + AccountInfo.buildTenantProfileFromIdTokenClaims( + HOME_ACCOUNT_ID, + idTokenClaims + ); + expect(tenantProfile.isHomeTenant).toBe(false); + }); + }); + }); + + describe("updateAccountTenantProfileData()", () => { + const baseAccount: AccountEntity = + buildAccountFromIdTokenClaims(ID_TOKEN_CLAIMS); + const baseAccountInfo = baseAccount.getAccountInfo(); + // Get non-overridable properties to make sure they're unchanged + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { + tenantId, + localAccountId, + name, + ...CONSTANT_ACCOUNT_PROPERTIES + } = baseAccountInfo; + + it("returns unmodified baseAccountInfo when tenantProfile and idTokenClaims are undefined", () => { + const updatedAccountInfo = + AccountInfo.updateAccountTenantProfileData( + baseAccountInfo, + undefined, + undefined + ); + expect(updatedAccountInfo).toEqual(baseAccountInfo); + }); + + it("returns baseAccountInfo augmented with tenantProfile data when tenantProfile passed in and idTokenClaims are undefined", () => { + const guestTenantProfile: AccountInfo.TenantProfile = { + tenantId: "guest-tenant-id", + localAccountId: "guest-local-account-id", + name: "guest-name", + isHomeTenant: false, + }; + + const updatedAccountInfo = + AccountInfo.updateAccountTenantProfileData( + baseAccountInfo, + guestTenantProfile, + undefined + ); + expect(updatedAccountInfo.tenantId).toEqual( + guestTenantProfile.tenantId + ); + expect(updatedAccountInfo.localAccountId).toEqual( + guestTenantProfile.localAccountId + ); + expect(updatedAccountInfo.name).toEqual(guestTenantProfile.name); + expect(updatedAccountInfo.idTokenClaims).toBeUndefined(); + expect(updatedAccountInfo).toMatchObject( + CONSTANT_ACCOUNT_PROPERTIES + ); + }); + + it("returns baseAccountInfo augmented with idTokenClaims data when idTokenClaims passed in and tenantProfile is undefined", () => { + const updatedAccountInfo = + AccountInfo.updateAccountTenantProfileData( + baseAccountInfo, + undefined, + ID_TOKEN_ALT_CLAIMS + ); + expect(updatedAccountInfo.tenantId).toEqual( + ID_TOKEN_ALT_CLAIMS.tid + ); + expect(updatedAccountInfo.localAccountId).toEqual( + ID_TOKEN_ALT_CLAIMS.oid + ); + expect(updatedAccountInfo.name).toEqual(ID_TOKEN_ALT_CLAIMS.name); + expect(updatedAccountInfo.idTokenClaims).toEqual( + ID_TOKEN_ALT_CLAIMS + ); + expect(updatedAccountInfo).toMatchObject( + CONSTANT_ACCOUNT_PROPERTIES + ); + }); + + it("gives precedence to idTokenClaims over tenantProfile when both are passed in", () => { + const guestTenantProfile: AccountInfo.TenantProfile = { + tenantId: "guest-tenant-id", + localAccountId: "guest-local-account-id", + name: "guest-name", + isHomeTenant: false, + }; + + const updatedAccountInfo = + AccountInfo.updateAccountTenantProfileData( + baseAccountInfo, + guestTenantProfile, + ID_TOKEN_ALT_CLAIMS + ); + + expect(updatedAccountInfo.tenantId).toEqual( + ID_TOKEN_ALT_CLAIMS.tid + ); + expect(updatedAccountInfo.localAccountId).toEqual( + ID_TOKEN_ALT_CLAIMS.oid + ); + expect(updatedAccountInfo.name).toEqual(ID_TOKEN_ALT_CLAIMS.name); + expect(updatedAccountInfo.idTokenClaims).toEqual( + ID_TOKEN_ALT_CLAIMS + ); + expect(updatedAccountInfo).toMatchObject( + CONSTANT_ACCOUNT_PROPERTIES + ); + }); + }); +}); diff --git a/lib/msal-common/test/account/AuthToken.spec.ts b/lib/msal-common/test/account/AuthToken.spec.ts index 93458535b0..5e1b06ba67 100644 --- a/lib/msal-common/test/account/AuthToken.spec.ts +++ b/lib/msal-common/test/account/AuthToken.spec.ts @@ -2,10 +2,10 @@ import * as AuthToken from "../../src/account/AuthToken"; import { TEST_DATA_CLIENT_INFO, RANDOM_TEST_GUID, - TEST_URIS, TEST_POP_VALUES, TEST_CRYPTO_VALUES, TEST_TOKENS, + ID_TOKEN_CLAIMS, } from "../test_kit/StringConstants"; import { ICrypto } from "../../src/crypto/ICrypto"; import { @@ -14,21 +14,7 @@ import { } from "../../src/error/ClientAuthError"; import { AuthError } from "../../src/error/AuthError"; -// Set up stubs -const idTokenClaims = { - ver: "2.0", - iss: `${TEST_URIS.DEFAULT_INSTANCE}9188040d-6c67-4c5b-b112-36a304b66dad/v2.0`, - sub: "AAAAAAAAAAAAAAAAAAAAAIkzqFVrSaSaFHy782bbtaQ", - exp: 1536361411, - name: "Abe Lincoln", - preferred_username: "AbeLi@microsoft.com", - oid: "00000000-0000-0000-66f3-3332eca7ea81", - tid: "3338040d-6c67-4c5b-b112-36a304b66dad", - nonce: "123523", -}; - -const testTokenPayload = - "eyJ2ZXIiOiIyLjAiLCJpc3MiOiJodHRwczovL2xvZ2luLm1pY3Jvc29mdG9ubGluZS5jb20vOTE4ODA0MGQtNmM2Ny00YzViLWIxMTItMzZhMzA0YjY2ZGFkL3YyLjAiLCJzdWIiOiJBQUFBQUFBQUFBQUFBQUFBQUFBQUFJa3pxRlZyU2FTYUZIeTc4MmJidGFRIiwiYXVkIjoiNmNiMDQwMTgtYTNmNS00NmE3LWI5OTUtOTQwYzc4ZjVhZWYzIiwiZXhwIjoxNTM2MzYxNDExLCJpYXQiOjE1MzYyNzQ3MTEsIm5iZiI6MTUzNjI3NDcxMSwibmFtZSI6IkFiZSBMaW5jb2xuIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiQWJlTGlAbWljcm9zb2Z0LmNvbSIsIm9pZCI6IjAwMDAwMDAwLTAwMDAtMDAwMC02NmYzLTMzMzJlY2E3ZWE4MSIsInRpZCI6IjMzMzgwNDBkLTZjNjctNGM1Yi1iMTEyLTM2YTMwNGI2NmRhZCIsIm5vbmNlIjoiMTIzNTIzIiwiYWlvIjoiRGYyVVZYTDFpeCFsTUNXTVNPSkJjRmF0emNHZnZGR2hqS3Y4cTVnMHg3MzJkUjVNQjVCaXN2R1FPN1lXQnlqZDhpUURMcSFlR2JJRGFreXA1bW5PcmNkcUhlWVNubHRlcFFtUnA2QUlaOGpZIn0="; +const TEST_ID_TOKEN_PAYLOAD = TEST_TOKENS.IDTOKEN_V2.split(".")[1]; describe("AuthToken.ts Class Unit Tests", () => { let cryptoInterface: ICrypto; @@ -43,8 +29,8 @@ describe("AuthToken.ts Class Unit Tests", () => { return TEST_POP_VALUES.DECODED_REQ_CNF; case TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO: return TEST_DATA_CLIENT_INFO.TEST_DECODED_CLIENT_INFO; - case testTokenPayload: - return JSON.stringify(idTokenClaims); + case TEST_ID_TOKEN_PAYLOAD: + return JSON.stringify(ID_TOKEN_CLAIMS); default: return input; } @@ -220,7 +206,7 @@ describe("AuthToken.ts Class Unit Tests", () => { TEST_TOKENS.IDTOKEN_V2, cryptoInterface.base64Decode ) - ).toEqual(idTokenClaims); + ).toEqual(ID_TOKEN_CLAIMS); }); }); }); diff --git a/lib/msal-common/test/authority/AuthorityMetadata.spec.ts b/lib/msal-common/test/authority/AuthorityMetadata.spec.ts index e993e6e644..bb311b8b4f 100644 --- a/lib/msal-common/test/authority/AuthorityMetadata.spec.ts +++ b/lib/msal-common/test/authority/AuthorityMetadata.spec.ts @@ -51,7 +51,6 @@ describe("AuthorityMetadata.ts Unit Tests", () => { tenant ), }; - console.log(staticAuthorityOptions); expect( getAliasesFromStaticSources(staticAuthorityOptions) ).toEqual(METADATA_ALIASES[cloudKey]); diff --git a/lib/msal-common/test/cache/CacheManager.spec.ts b/lib/msal-common/test/cache/CacheManager.spec.ts index c293b7d171..c3f7b05828 100644 --- a/lib/msal-common/test/cache/CacheManager.spec.ts +++ b/lib/msal-common/test/cache/CacheManager.spec.ts @@ -23,13 +23,18 @@ import { TEST_ACCOUNT_INFO, TEST_TOKEN_LIFETIMES, ID_TOKEN_ALT_CLAIMS, + GUEST_ID_TOKEN_CLAIMS, } from "../test_kit/StringConstants"; import { ClientAuthErrorCodes, createClientAuthError, } from "../../src/error/ClientAuthError"; import { AccountInfo } from "../../src/account/AccountInfo"; -import { MockCache } from "./MockCache"; +import { + MockCache, + buildAccountFromIdTokenClaims, + buildIdToken, +} from "./MockCache"; import { mockCrypto } from "../client/ClientTestUtils"; import { TestError } from "../test_kit/TestErrors"; import { CacheManager } from "../../src/cache/CacheManager"; @@ -296,25 +301,38 @@ describe("CacheManager.ts test cases", () => { }); describe("getAllAccounts", () => { - const account1 = CACHE_MOCKS.MOCK_ACCOUNT_INFO; - const account2 = CACHE_MOCKS.MOCK_ACCOUNT_INFO_WITH_NATIVE_ACCOUNT_ID; + const account1 = + buildAccountFromIdTokenClaims(ID_TOKEN_CLAIMS).getAccountInfo(); + const account2 = + buildAccountFromIdTokenClaims(ID_TOKEN_ALT_CLAIMS).getAccountInfo(); it("getAllAccounts (gets all AccountInfo objects)", async () => { const accounts = mockCache.cacheManager.getAllAccounts(); expect(accounts).not.toBeNull(); + // 2 home accounts + 1 tenant profile + expect(accounts.length).toBe(3); expect(accounts[0].idTokenClaims).toEqual(ID_TOKEN_CLAIMS); + expect(accounts[1].idTokenClaims).toEqual(GUEST_ID_TOKEN_CLAIMS); + expect(accounts[2].idTokenClaims).toEqual(ID_TOKEN_ALT_CLAIMS); + }); + + it("getAllAccounts with isHomeTenant filter does not return guest tenant profiles as AccountInfo objects", () => { + const homeAccounts = mockCache.cacheManager.getAllAccounts({ + isHomeTenant: true, + }); + expect(homeAccounts).not.toBeNull(); + expect(homeAccounts.length).toBe(2); + expect(homeAccounts[0].idTokenClaims).toEqual(ID_TOKEN_CLAIMS); + expect(homeAccounts[1].idTokenClaims).toEqual(ID_TOKEN_ALT_CLAIMS); }); describe("getAllAccounts with loginHint filter", () => { it("loginHint filter matching login_hint ID token claim", () => { // filter by loginHint = login_hint - const loginHint = "testLoginHint"; const successFilter: AccountFilter = { - loginHint: loginHint, + loginHint: ID_TOKEN_CLAIMS.login_hint, }; - jest.spyOn(mockCrypto, "base64Decode").mockReturnValueOnce( - JSON.stringify({ login_hint: loginHint }) - ); + let accounts = mockCache.cacheManager.getAllAccounts(successFilter); expect(accounts.length).toEqual(1); @@ -328,13 +346,10 @@ describe("CacheManager.ts test cases", () => { it("loginHint filter matching username", () => { // filter by loginHint = preferred_username - const username = "janedoe@microsoft.com"; const successFilter: AccountFilter = { - loginHint: username, + loginHint: ID_TOKEN_CLAIMS.preferred_username, }; - jest.spyOn(mockCrypto, "base64Decode").mockReturnValueOnce( - JSON.stringify({ preferred_username: username }) - ); + let accounts = mockCache.cacheManager.getAllAccounts(successFilter); expect(accounts.length).toEqual(1); @@ -348,14 +363,10 @@ describe("CacheManager.ts test cases", () => { it("loginHint filter matching upn ID token claim", () => { // filter by loginHint = upn - const upn = "testUpn"; const successFilter: AccountFilter = { - loginHint: upn, + loginHint: ID_TOKEN_CLAIMS.upn, }; - jest.spyOn(mockCrypto, "base64Decode").mockReturnValueOnce( - JSON.stringify({ upn: upn }) - ); let accounts = mockCache.cacheManager.getAllAccounts(successFilter); expect(accounts.length).toEqual(1); @@ -368,150 +379,238 @@ describe("CacheManager.ts test cases", () => { }); }); - it("Matches accounts by username", () => { - expect(mockCache.cacheManager.getAllAccounts()).toHaveLength(2); - const account1Filter = { username: account1.username }; - const account2Filter = { username: account2.username }; - expect( - mockCache.cacheManager.getAllAccounts(account1Filter) - ).toHaveLength(1); - expect( - mockCache.cacheManager.getAllAccounts(account1Filter)[0] - .username - ).toBe(account1.username); - expect( - mockCache.cacheManager.getAllAccounts(account2Filter) - ).toHaveLength(1); - expect( - mockCache.cacheManager.getAllAccounts(account2Filter)[0] - .username - ).toBe(account2.username); - }); + describe("getAllAccounts with filter", () => { + it("Matches accounts by username", () => { + expect(mockCache.cacheManager.getAllAccounts()).toHaveLength(3); + const account1Filter = { username: account1.username }; + const account2Filter = { username: account2.username }; + const accounts = + mockCache.cacheManager.getAllAccounts(account1Filter); + expect(accounts).toHaveLength(1); + expect( + mockCache.cacheManager.getAllAccounts(account1Filter)[0] + .username + ).toBe(account1.username); + expect( + mockCache.cacheManager.getAllAccounts(account2Filter) + ).toHaveLength(1); + expect( + mockCache.cacheManager.getAllAccounts(account2Filter)[0] + .username + ).toBe(account2.username); + }); - it("Matches accounts by homeAccountId", () => { - expect(mockCache.cacheManager.getAllAccounts()).toHaveLength(2); - const account1Filter = { - homeAccountId: account1.homeAccountId, - }; - const account2Filter = { - homeAccountId: account2.homeAccountId, - }; - expect( - mockCache.cacheManager.getAllAccounts(account1Filter) - ).toHaveLength(1); - expect( - mockCache.cacheManager.getAllAccounts(account1Filter)[0] - .homeAccountId - ).toBe(account1.homeAccountId); - expect( - mockCache.cacheManager.getAllAccounts(account2Filter) - ).toHaveLength(1); - expect( - mockCache.cacheManager.getAllAccounts(account2Filter)[0] - .homeAccountId - ).toBe(account2.homeAccountId); + it("Matches accounts by homeAccountId", () => { + expect(mockCache.cacheManager.getAllAccounts()).toHaveLength(3); + const multiTenantAccountFilter = { + homeAccountId: account1.homeAccountId, + }; + + const multiTenantAccountHomeTenantOnlyFilter = { + ...multiTenantAccountFilter, + isHomeTenant: true, + }; + + const account2Filter = { + homeAccountId: account2.homeAccountId, + }; + // Multi-tenant account has two tenant profiles which will both match the same homeAccountId + const multiTenantAccountProfiles = + mockCache.cacheManager.getAllAccounts( + multiTenantAccountFilter + ); + expect(multiTenantAccountProfiles).toHaveLength(2); + expect(multiTenantAccountProfiles[0].homeAccountId).toBe( + account1.homeAccountId + ); + + // Set isHomeTenant = true to only get baseAccount + const multiTenantAccountHomeTenantOnlyProfiles = + mockCache.cacheManager.getAllAccounts( + multiTenantAccountHomeTenantOnlyFilter + ); + expect(multiTenantAccountHomeTenantOnlyProfiles).toHaveLength( + 1 + ); + expect( + multiTenantAccountHomeTenantOnlyProfiles[0].tenantId + ).toBe(account1.tenantId); + expect( + mockCache.cacheManager.getAllAccounts(account2Filter) + ).toHaveLength(1); + expect( + mockCache.cacheManager.getAllAccounts(account2Filter)[0] + .homeAccountId + ).toBe(account2.homeAccountId); + }); + + it("Matches accounts by localAccountId", () => { + expect(mockCache.cacheManager.getAllAccounts()).toHaveLength(3); + // Local account ID is sourced from ID token claims so for this test we compare against the decoded ID token claims instead of mock account object + const account1Filter = { + localAccountId: ID_TOKEN_CLAIMS.oid, + }; + const account2Filter = { + localAccountId: ID_TOKEN_ALT_CLAIMS.oid, + }; + expect( + mockCache.cacheManager.getAllAccounts(account1Filter) + ).toHaveLength(1); + expect( + mockCache.cacheManager.getAllAccounts(account1Filter)[0] + .localAccountId + ).toBe(account1Filter.localAccountId); + expect( + mockCache.cacheManager.getAllAccounts(account2Filter) + ).toHaveLength(1); + expect( + mockCache.cacheManager.getAllAccounts(account2Filter)[0] + .localAccountId + ).toBe(account2Filter.localAccountId); + }); + + it("Matches accounts by tenantId", () => { + expect(mockCache.cacheManager.getAllAccounts()).toHaveLength(3); + const firstTenantAccountFilter = { + tenantId: account1.tenantId, + }; + const secondTenantAccountFilter = { + tenantId: account2.tenantId, + }; + expect( + mockCache.cacheManager.getAllAccounts( + firstTenantAccountFilter + ) + ).toHaveLength(1); + expect( + mockCache.cacheManager.getAllAccounts( + firstTenantAccountFilter + )[0].tenantId + ).toBe(firstTenantAccountFilter.tenantId); + // Guest profile of first user account is from the same tenant as account 2 + expect( + mockCache.cacheManager.getAllAccounts( + secondTenantAccountFilter + ) + ).toHaveLength(2); + expect( + mockCache.cacheManager.getAllAccounts( + secondTenantAccountFilter + )[0].tenantId + ).toBe(secondTenantAccountFilter.tenantId); + }); + + it("Matches accounts by environment", () => { + expect(mockCache.cacheManager.getAllAccounts()).toHaveLength(3); + // Add local account ID to further filter because environments are aliases of eachother + const firstEnvironmentAccountsFilter = { + homeAccountId: account1.homeAccountId, + environment: account1.environment, + }; + const secondEnvironmentAccountsFilter = { + homeAccountId: account2.homeAccountId, + environment: account2.environment, + }; + expect( + mockCache.cacheManager.getAllAccounts( + firstEnvironmentAccountsFilter + ) + ).toHaveLength(2); + expect( + mockCache.cacheManager.getAllAccounts( + firstEnvironmentAccountsFilter + )[0].environment + ).toBe(account1.environment); + expect( + mockCache.cacheManager.getAllAccounts( + secondEnvironmentAccountsFilter + ) + ).toHaveLength(1); + expect( + mockCache.cacheManager.getAllAccounts( + secondEnvironmentAccountsFilter + )[0].environment + ).toBe(account2.environment); + }); + + it("Matches accounts by all filters", () => { + expect(mockCache.cacheManager.getAllAccounts()).toHaveLength(3); + const account1Filter = { + ...account1, + localAccountId: ID_TOKEN_CLAIMS.oid, + }; + const account2Filter = { + ...account2, + localAccountId: ID_TOKEN_ALT_CLAIMS.oid, + }; + expect( + mockCache.cacheManager.getAllAccounts(account1Filter) + ).toHaveLength(1); + expect( + mockCache.cacheManager.getAllAccounts(account1Filter)[0] + .localAccountId + ).toBe(account1Filter.localAccountId); + expect( + mockCache.cacheManager.getAllAccounts(account2Filter) + ).toHaveLength(1); + expect( + mockCache.cacheManager.getAllAccounts(account2Filter)[0] + .localAccountId + ).toBe(account2Filter.localAccountId); + }); }); + }); - it("Matches accounts by localAccountId", () => { - expect(mockCache.cacheManager.getAllAccounts()).toHaveLength(2); - // Local account ID is sourced from ID token claims so for this test we compare against the decoded ID token claims instead of mock account object - const account1Filter = { - localAccountId: ID_TOKEN_CLAIMS.oid, - }; - const account2Filter = { - localAccountId: ID_TOKEN_ALT_CLAIMS.oid, - }; - expect( - mockCache.cacheManager.getAllAccounts(account1Filter) - ).toHaveLength(1); + describe("getAccountInfoFilteredBy", () => { + const multiTenantAccount = buildAccountFromIdTokenClaims( + ID_TOKEN_CLAIMS, + [GUEST_ID_TOKEN_CLAIMS] + ).getAccountInfo(); + it("returns null if no accounts match filter", () => { expect( - mockCache.cacheManager.getAllAccounts(account1Filter)[0] - .localAccountId - ).toBe(account1Filter.localAccountId); - expect( - mockCache.cacheManager.getAllAccounts(account2Filter) - ).toHaveLength(1); - expect( - mockCache.cacheManager.getAllAccounts(account2Filter)[0] - .localAccountId - ).toBe(account2Filter.localAccountId); + mockCache.cacheManager.getAccountInfoFilteredBy({ + homeAccountId: "inexistent-account-id", + }) + ).toBeNull(); }); - it("Matches accounts by tenantId", () => { - expect(mockCache.cacheManager.getAllAccounts()).toHaveLength(2); - const account1Filter = { - tenantId: account1.tenantId, - }; - const account2Filter = { - tenantId: account2.tenantId, - }; - expect( - mockCache.cacheManager.getAllAccounts(account1Filter) - ).toHaveLength(1); - expect( - mockCache.cacheManager.getAllAccounts(account1Filter)[0] - .tenantId - ).toBe(account1Filter.tenantId); - expect( - mockCache.cacheManager.getAllAccounts(account2Filter) - ).toHaveLength(1); - expect( - mockCache.cacheManager.getAllAccounts(account2Filter)[0] - .tenantId - ).toBe(account2Filter.tenantId); + it("returns an account matching filter", () => { + const resultAccount = + mockCache.cacheManager.getAccountInfoFilteredBy({ + homeAccountId: multiTenantAccount.homeAccountId, + tenantId: multiTenantAccount.tenantId, + }); + expect(resultAccount).not.toBeNull(); + expect(resultAccount).toMatchObject(multiTenantAccount); }); - it("Matches accounts by environment", () => { - expect(mockCache.cacheManager.getAllAccounts()).toHaveLength(2); - // Add local account ID to further filter because environments are aliases of eachother - const account1Filter = { - homeAccountId: account1.homeAccountId, - environment: account1.environment, - }; - const account2Filter = { - homeAccountId: account2.homeAccountId, - environment: account2.environment, - }; - expect( - mockCache.cacheManager.getAllAccounts(account1Filter) - ).toHaveLength(1); - expect( - mockCache.cacheManager.getAllAccounts(account1Filter)[0] - .environment - ).toBe(account1.environment); - expect( - mockCache.cacheManager.getAllAccounts(account2Filter) - ).toHaveLength(1); - expect( - mockCache.cacheManager.getAllAccounts(account2Filter)[0] - .environment - ).toBe(account2.environment); + it("prioritizes the tenant profile with a matching ID token in the cache", () => { + const mainIdTokenEntity = buildIdToken( + ID_TOKEN_CLAIMS, + TEST_TOKENS.IDTOKEN_V2, + { homeAccountId: multiTenantAccount.homeAccountId } + ); + const mainIdTokenKey = + CacheHelpers.generateCredentialKey(mainIdTokenEntity); + // Remove main ID token + mockCache.cacheManager.removeIdToken(mainIdTokenKey); + const resultAccount = + mockCache.cacheManager.getAccountInfoFilteredBy({ + homeAccountId: multiTenantAccount.homeAccountId, + }); + expect(resultAccount).not.toBeNull(); + expect(resultAccount?.tenantId).toBe(GUEST_ID_TOKEN_CLAIMS.tid); }); - it("Matches accounts by all filters", () => { - expect(mockCache.cacheManager.getAllAccounts()).toHaveLength(2); - const account1Filter = { - ...account1, - localAccountId: ID_TOKEN_CLAIMS.oid, - }; - const account2Filter = { - ...account2, - localAccountId: ID_TOKEN_ALT_CLAIMS.oid, - }; - expect( - mockCache.cacheManager.getAllAccounts(account1Filter) - ).toHaveLength(1); - expect( - mockCache.cacheManager.getAllAccounts(account1Filter)[0] - .localAccountId - ).toBe(account1Filter.localAccountId); - expect( - mockCache.cacheManager.getAllAccounts(account2Filter) - ).toHaveLength(1); - expect( - mockCache.cacheManager.getAllAccounts(account2Filter)[0] - .localAccountId - ).toBe(account2Filter.localAccountId); + it("returns account matching filter with isHomeTenant = true", () => { + const resultAccount = + mockCache.cacheManager.getAccountInfoFilteredBy({ + homeAccountId: multiTenantAccount.homeAccountId, + tenantId: multiTenantAccount.tenantId, + isHomeTenant: true, + }); + expect(resultAccount).not.toBeNull(); + expect(resultAccount).toMatchObject(multiTenantAccount); }); }); @@ -593,11 +692,16 @@ describe("CacheManager.ts test cases", () => { }); describe("getAccountsFilteredBy", () => { + const matchAccountEntity = + buildAccountFromIdTokenClaims(ID_TOKEN_CLAIMS); it("homeAccountId filter", () => { // filter by homeAccountId - const successFilter: AccountFilter = { homeAccountId: "uid.utid" }; + const successFilter: AccountFilter = { + homeAccountId: matchAccountEntity.homeAccountId, + }; let accounts = mockCache.cacheManager.getAccountsFilteredBy(successFilter); + // getAccountsFilteredBy only gets cached accounts, so don't expect all tenant profiles to be returned as account objects expect(Object.keys(accounts).length).toEqual(1); const wrongFilter: AccountFilter = { homeAccountId: "Wrong Id" }; @@ -609,7 +713,7 @@ describe("CacheManager.ts test cases", () => { it("environment filter", () => { // filter by environment const successFilter: AccountFilter = { - environment: "login.microsoftonline.com", + environment: matchAccountEntity.environment, }; let accounts = mockCache.cacheManager.getAccountsFilteredBy(successFilter); @@ -625,7 +729,9 @@ describe("CacheManager.ts test cases", () => { it("realm filter", () => { // filter by realm - const successFilter: AccountFilter = { realm: ID_TOKEN_CLAIMS.tid }; + const successFilter: AccountFilter = { + realm: matchAccountEntity.realm, + }; let accounts = mockCache.cacheManager.getAccountsFilteredBy(successFilter); expect(Object.keys(accounts).length).toEqual(1); @@ -1382,37 +1488,23 @@ describe("CacheManager.ts test cases", () => { }); it("removeAllAccounts", async () => { - const ac = new AccountEntity(); - ac.homeAccountId = "someUid.someUtid"; - ac.environment = "login.microsoftonline.com"; - ac.realm = "microsoft"; - ac.localAccountId = "object1234"; - ac.username = "Jane Goodman"; - ac.authorityType = "MSSTS"; - - const cacheRecord = new CacheRecord(); - cacheRecord.account = ac; - await mockCache.cacheManager.saveCacheRecord(cacheRecord); - + const accountsBeforeRemove = mockCache.cacheManager.getAllAccounts(); await mockCache.cacheManager.removeAllAccounts(); + const accountsAfterRemove = mockCache.cacheManager.getAllAccounts(); - // Only app metadata remaining - expect(mockCache.cacheManager.getAllAccounts().length === 0).toBe(true); + expect(accountsBeforeRemove).toHaveLength(3); + expect(accountsAfterRemove).toHaveLength(0); }); it("removeAccount", async () => { + const accountToRemove = buildAccountFromIdTokenClaims(ID_TOKEN_CLAIMS); + const accountToRemoveKey = accountToRemove.generateAccountKey(); expect( - mockCache.cacheManager.getAccount( - "uid.utid-login.microsoftonline.com-utid" - ) + mockCache.cacheManager.getAccount(accountToRemoveKey) ).not.toBeNull(); - await mockCache.cacheManager.removeAccount( - "uid.utid-login.microsoftonline.com-utid" - ); + await mockCache.cacheManager.removeAccount(accountToRemoveKey); expect( - mockCache.cacheManager.getAccount( - "uid.utid-login.microsoftonline.com-utid" - ) + mockCache.cacheManager.getAccount(accountToRemoveKey) ).toBeNull(); }); @@ -1977,17 +2069,17 @@ describe("CacheManager.ts test cases", () => { }); it("readAccountFromCache", () => { + const matchAccountInfo = + buildAccountFromIdTokenClaims(ID_TOKEN_CLAIMS).getAccountInfo(); const account = mockCache.cacheManager.readAccountFromCache( - CACHE_MOCKS.MOCK_ACCOUNT_INFO + matchAccountInfo ) as AccountEntity; if (!account) { throw TestError.createTestSetupError( - "account does not have a value" + "Sccount does not have a value" ); } - expect(account.homeAccountId).toBe( - CACHE_MOCKS.MOCK_ACCOUNT_INFO.homeAccountId - ); + expect(account.homeAccountId).toBe(matchAccountInfo.homeAccountId); }); it("getAccountsFilteredBy nativeAccountId", () => { @@ -2007,15 +2099,29 @@ describe("CacheManager.ts test cases", () => { }); it("getIdToken", () => { + const baseAccountInfo = + buildAccountFromIdTokenClaims(ID_TOKEN_ALT_CLAIMS).getAccountInfo(); + // Get home ID token by default const idToken = mockCache.cacheManager.getIdToken( - CACHE_MOCKS.MOCK_ACCOUNT_INFO + baseAccountInfo ) as IdTokenEntity; if (!idToken) { throw TestError.createTestSetupError( "idToken does not have a value" ); } - expect(idToken.clientId).toBe(CACHE_MOCKS.MOCK_CLIENT_ID); + expect(idToken.realm).toBe(baseAccountInfo.tenantId); + const guestIdToken = mockCache.cacheManager.getIdToken( + baseAccountInfo, + undefined, + GUEST_ID_TOKEN_CLAIMS.tid + ) as IdTokenEntity; + if (!guestIdToken) { + throw TestError.createTestSetupError( + "guest idToken does not have a value" + ); + } + expect(guestIdToken.realm).toBe(GUEST_ID_TOKEN_CLAIMS.tid); }); it("getRefreshToken", () => { diff --git a/lib/msal-common/test/cache/MockCache.ts b/lib/msal-common/test/cache/MockCache.ts index 1a8e559079..5c59da54de 100644 --- a/lib/msal-common/test/cache/MockCache.ts +++ b/lib/msal-common/test/cache/MockCache.ts @@ -14,13 +14,21 @@ import { StaticAuthorityOptions, CredentialType, AuthenticationScheme, + TokenClaims, + IdTokenEntity, } from "../../src"; +import { + AccountInfo, + TenantProfile, + buildTenantProfileFromIdTokenClaims, +} from "../../src/account/AccountInfo"; import { MockStorageClass } from "../client/ClientTestUtils"; import { TEST_TOKENS, TEST_CRYPTO_VALUES, ID_TOKEN_CLAIMS, ID_TOKEN_ALT_CLAIMS, + GUEST_ID_TOKEN_CLAIMS, } from "../test_kit/StringConstants"; export class MockCache { @@ -56,58 +64,38 @@ export class MockCache { // create account entries in the cache createAccountEntries(): void { - const accountData = { - username: "John Doe", - localAccountId: "object1234", - realm: ID_TOKEN_CLAIMS.tid, - environment: "login.microsoftonline.com", - homeAccountId: "uid.utid", - authorityType: "MSSTS", - clientInfo: "eyJ1aWQiOiJ1aWQiLCAidXRpZCI6InV0aWQifQ==", - }; - const account = CacheManager.toObject(new AccountEntity(), accountData); + const account = buildAccountFromIdTokenClaims(ID_TOKEN_CLAIMS, [ + GUEST_ID_TOKEN_CLAIMS, + ]); this.cacheManager.setAccount(account); - const accountDataWithNativeAccountId = { - username: "Jane Doe", - localAccountId: "object1234", - realm: ID_TOKEN_ALT_CLAIMS.tid, - environment: "login.windows.net", - homeAccountId: "uid.utid2", - authorityType: "MSSTS", - clientInfo: "eyJ1aWQiOiJ1aWQiLCAidXRpZCI6InV0aWQifQ==", - nativeAccountId: "mocked_native_account_id", - }; - const accountWithNativeAccountId = CacheManager.toObject( - new AccountEntity(), - accountDataWithNativeAccountId - ); + const accountWithNativeAccountId = + buildAccountFromIdTokenClaims(ID_TOKEN_ALT_CLAIMS); + accountWithNativeAccountId.nativeAccountId = "mocked_native_account_id"; + this.cacheManager.setAccount(accountWithNativeAccountId); } // create id token entries in the cache createIdTokenEntries(): void { - const idToken = { - realm: ID_TOKEN_CLAIMS.tid, - environment: "login.microsoftonline.com", - credentialType: CredentialType.ID_TOKEN, - secret: TEST_TOKENS.IDTOKEN_V2, - clientId: "mock_client_id", - homeAccountId: "uid.utid", - }; + const idToken = buildIdToken(ID_TOKEN_CLAIMS, TEST_TOKENS.IDTOKEN_V2); this.cacheManager.setIdTokenCredential(idToken); - const guestIdToken = { - realm: ID_TOKEN_ALT_CLAIMS.tid, - environment: "login.windows.net", - credentialType: CredentialType.ID_TOKEN, - secret: TEST_TOKENS.IDTOKEN_V2_ALT, - clientId: "mock_client_id", - homeAccountId: "uid.utid2", - }; + const guestIdToken = buildIdToken( + GUEST_ID_TOKEN_CLAIMS, + TEST_TOKENS.ID_TOKEN_V2_GUEST, + { homeAccountId: idToken.homeAccountId } + ); this.cacheManager.setIdTokenCredential(guestIdToken); + + const altIdToken = buildIdToken( + ID_TOKEN_ALT_CLAIMS, + TEST_TOKENS.IDTOKEN_V2_ALT, + { environment: "login.windows.net" } + ); + this.cacheManager.setIdTokenCredential(altIdToken); } // create access token entries in the cache @@ -301,3 +289,63 @@ export class MockCache { this.cacheManager.setAuthorityMetadata(cacheKey, authorityMetadata); } } + +export function buildAccountFromIdTokenClaims( + idTokenClaims: TokenClaims, + guestIdTokenClaimsList?: TokenClaims[] +): AccountEntity { + const { oid, tid, preferred_username, emails, name } = idTokenClaims; + const tenantId = tid || ""; + const email = emails ? emails[0] : null; + + const homeAccountId = `${oid}.${tid}`; + + const accountInfo: AccountInfo = { + homeAccountId: homeAccountId || "", + username: preferred_username || email || "", + localAccountId: oid || "", + tenantId: tenantId, + environment: "login.microsoftonline.com", + authorityType: "MSSTS", + name: name, + tenantProfiles: new Map([ + [ + tenantId, + buildTenantProfileFromIdTokenClaims( + homeAccountId, + idTokenClaims + ), + ], + ]), + }; + guestIdTokenClaimsList?.forEach((guestIdTokenClaims: TokenClaims) => { + const guestTenantId = guestIdTokenClaims.tid || ""; + accountInfo.tenantProfiles?.set( + guestTenantId, + buildTenantProfileFromIdTokenClaims( + accountInfo.homeAccountId, + guestIdTokenClaims + ) + ); + }); + return AccountEntity.createFromAccountInfo(accountInfo); +} + +export function buildIdToken( + idTokenClaims: TokenClaims, + idTokenSecret: string, + options?: Partial +): IdTokenEntity { + const { oid, tid } = idTokenClaims; + const homeAccountId = `${oid}.${tid}`; + const idToken = { + realm: tid || "", + environment: "login.microsoftonline.com", + credentialType: CredentialType.ID_TOKEN, + secret: idTokenSecret, + clientId: "mock_client_id", + homeAccountId: homeAccountId, + }; + + return { ...idToken, ...options }; +} diff --git a/lib/msal-common/test/client/ClientTestUtils.ts b/lib/msal-common/test/client/ClientTestUtils.ts index 37888de80c..c6bb66c48f 100644 --- a/lib/msal-common/test/client/ClientTestUtils.ts +++ b/lib/msal-common/test/client/ClientTestUtils.ts @@ -42,13 +42,16 @@ export class MockStorageClass extends CacheManager { store = {}; // Accounts - getAccount(key: string): AccountEntity | null { - const account: AccountEntity = this.store[key] as AccountEntity; + getCachedAccountEntity(accountKey: string): AccountEntity | null { + const account: AccountEntity = this.store[accountKey] as AccountEntity; if (AccountEntity.isAccountEntity(account)) { return account; } return null; } + getAccount(key: string): AccountEntity | null { + return this.getCachedAccountEntity(key); + } setAccount(value: AccountEntity): void { const key = value.generateAccountKey(); diff --git a/lib/msal-common/test/client/SilentFlowClient.spec.ts b/lib/msal-common/test/client/SilentFlowClient.spec.ts index b16652277b..557be40e49 100644 --- a/lib/msal-common/test/client/SilentFlowClient.spec.ts +++ b/lib/msal-common/test/client/SilentFlowClient.spec.ts @@ -55,15 +55,15 @@ import { } from "../../src/error/InteractionRequiredAuthError"; import { StubPerformanceClient } from "../../src/telemetry/performance/StubPerformanceClient"; import { Logger } from "../../src/logger/Logger"; +import { buildAccountFromIdTokenClaims } from "../cache/MockCache"; -const testAccountEntity: AccountEntity = new AccountEntity(); -testAccountEntity.homeAccountId = `${TEST_DATA_CLIENT_INFO.TEST_ENCODED_HOME_ACCOUNT_ID}`; -testAccountEntity.localAccountId = ID_TOKEN_CLAIMS.oid; -testAccountEntity.environment = "login.windows.net"; -testAccountEntity.realm = ID_TOKEN_CLAIMS.tid; -testAccountEntity.username = ID_TOKEN_CLAIMS.preferred_username; -testAccountEntity.name = ID_TOKEN_CLAIMS.name; -testAccountEntity.authorityType = "MSSTS"; +const testAccountEntity: AccountEntity = + buildAccountFromIdTokenClaims(ID_TOKEN_CLAIMS); + +const testAccount: AccountInfo = { + ...testAccountEntity.getAccountInfo(), + idTokenClaims: ID_TOKEN_CLAIMS, +}; const testIdToken: IdTokenEntity = { homeAccountId: `${TEST_DATA_CLIENT_INFO.TEST_UID}.${TEST_DATA_CLIENT_INFO.TEST_UTID}`, @@ -102,18 +102,6 @@ const testRefreshTokenEntity: RefreshTokenEntity = { }; describe("SilentFlowClient unit tests", () => { - const testAccount: AccountInfo = { - authorityType: "MSSTS", - homeAccountId: TEST_DATA_CLIENT_INFO.TEST_ENCODED_HOME_ACCOUNT_ID, - environment: "login.windows.net", - tenantId: ID_TOKEN_CLAIMS.tid, - username: ID_TOKEN_CLAIMS.preferred_username, - localAccountId: ID_TOKEN_CLAIMS.oid, - idTokenClaims: ID_TOKEN_CLAIMS, - name: ID_TOKEN_CLAIMS.name, - nativeAccountId: undefined, - }; - afterEach(() => { sinon.restore(); }); @@ -719,17 +707,6 @@ describe("SilentFlowClient unit tests", () => { describe("acquireToken tests", () => { let config: ClientConfiguration; let client: SilentFlowClient; - const testAccount: AccountInfo = { - authorityType: "MSSTS", - homeAccountId: `${TEST_DATA_CLIENT_INFO.TEST_ENCODED_HOME_ACCOUNT_ID}`, - tenantId: ID_TOKEN_CLAIMS.tid, - environment: "login.windows.net", - username: ID_TOKEN_CLAIMS.preferred_username, - name: ID_TOKEN_CLAIMS.name, - localAccountId: ID_TOKEN_CLAIMS.oid, - idTokenClaims: ID_TOKEN_CLAIMS, - nativeAccountId: undefined, - }; beforeEach(async () => { sinon @@ -784,11 +761,7 @@ describe("SilentFlowClient unit tests", () => { const authResult = await client.acquireToken(silentFlowRequest); expect(refreshTokenSpy.called).toBe(false); - const expectedScopes = [ - Constants.OPENID_SCOPE, - Constants.PROFILE_SCOPE, - TEST_CONFIG.DEFAULT_GRAPH_SCOPE[0], - ]; + const expectedScopes = testAccessTokenEntity.target.split(" "); expect(authResult.uniqueId).toEqual(ID_TOKEN_CLAIMS.oid); expect(authResult.tenantId).toEqual(ID_TOKEN_CLAIMS.tid); expect(authResult.scopes).toEqual(expectedScopes); @@ -863,11 +836,7 @@ describe("SilentFlowClient unit tests", () => { const response = await client.acquireCachedToken(silentFlowRequest); const authResult: AuthenticationResult = response[0]; - const expectedScopes = [ - Constants.OPENID_SCOPE, - Constants.PROFILE_SCOPE, - TEST_CONFIG.DEFAULT_GRAPH_SCOPE[0], - ]; + const expectedScopes = testAccessTokenEntity.target.split(" "); expect(telemetryCacheHitSpy.calledOnce).toBe(true); expect(authResult.uniqueId).toEqual(ID_TOKEN_CLAIMS.oid); expect(authResult.tenantId).toEqual(ID_TOKEN_CLAIMS.tid); diff --git a/lib/msal-common/test/test_kit/StringConstants.ts b/lib/msal-common/test/test_kit/StringConstants.ts index 15e51eaf96..375add4590 100644 --- a/lib/msal-common/test/test_kit/StringConstants.ts +++ b/lib/msal-common/test/test_kit/StringConstants.ts @@ -18,9 +18,11 @@ export const TEST_TOKENS = { IDTOKEN_V1: "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6IjdfWnVmMXR2a3dMeFlhSFMzcTZsVWpVWUlHdyIsImtpZCI6IjdfWnVmMXR2a3dMeFlhSFMzcTZsVWpVWUlHdyJ9.eyJhdWQiOiJiMTRhNzUwNS05NmU5LTQ5MjctOTFlOC0wNjAxZDBmYzljYWEiLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC9mYTE1ZDY5Mi1lOWM3LTQ0NjAtYTc0My0yOWYyOTU2ZmQ0MjkvIiwiaWF0IjoxNTM2Mjc1MTI0LCJuYmYiOjE1MzYyNzUxMjQsImV4cCI6MTUzNjI3OTAyNCwiYWlvIjoiQVhRQWkvOElBQUFBcXhzdUIrUjREMnJGUXFPRVRPNFlkWGJMRDlrWjh4ZlhhZGVBTTBRMk5rTlQ1aXpmZzN1d2JXU1hodVNTajZVVDVoeTJENldxQXBCNWpLQTZaZ1o5ay9TVTI3dVY5Y2V0WGZMT3RwTnR0Z2s1RGNCdGsrTExzdHovSmcrZ1lSbXY5YlVVNFhscGhUYzZDODZKbWoxRkN3PT0iLCJhbXIiOlsicnNhIl0sImVtYWlsIjoiYWJlbGlAbWljcm9zb2Z0LmNvbSIsImZhbWlseV9uYW1lIjoiTGluY29sbiIsImdpdmVuX25hbWUiOiJBYmUiLCJpZHAiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC83MmY5ODhiZi04NmYxLTQxYWYtOTFhYi0yZDdjZDAxMWRiNDcvIiwiaXBhZGRyIjoiMTMxLjEwNy4yMjIuMjIiLCJuYW1lIjoiYWJlbGkiLCJub25jZSI6IjEyMzUyMyIsIm9pZCI6IjA1ODMzYjZiLWFhMWQtNDJkNC05ZWMwLTFiMmJiOTE5NDQzOCIsInJoIjoiSSIsInN1YiI6IjVfSjlyU3NzOC1qdnRfSWN1NnVlUk5MOHhYYjhMRjRGc2dfS29vQzJSSlEiLCJ0aWQiOiJmYTE1ZDY5Mi1lOWM3LTQ0NjAtYTc0My0yOWYyOTU2ZmQ0MjkiLCJ1bmlxdWVfbmFtZSI6IkFiZUxpQG1pY3Jvc29mdC5jb20iLCJ1dGkiOiJMeGVfNDZHcVRrT3BHU2ZUbG40RUFBIiwidmVyIjoiMS4wIn0=.UJQrCA6qn2bXq57qzGX_-D3HcPHqBMOKDPx4su1yKRLNErVD8xkxJLNLVRdASHqEcpyDctbdHccu6DPpkq5f0ibcaQFhejQNcABidJCTz0Bb2AbdUCTqAzdt9pdgQvMBnVH1xk3SCM6d4BbT4BkLLj10ZLasX7vRknaSjE_C5DI7Fg4WrZPwOhII1dB0HEZ_qpNaYXEiy-o94UJ94zCr07GgrqMsfYQqFR7kn-mn68AjvLcgwSfZvyR_yIK75S_K37vC3QryQ7cNoafDe9upql_6pB2ybMVlgWPs_DmbJ8g0om-sPlwyn74Cc1tW3ze-Xptw_2uVdPgWyqfuWAfq6Q", IDTOKEN_V2: - "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IjFMVE16YWtpaGlSbGFfOHoyQkVKVlhlV01xbyJ9.eyJ2ZXIiOiIyLjAiLCJpc3MiOiJodHRwczovL2xvZ2luLm1pY3Jvc29mdG9ubGluZS5jb20vOTE4ODA0MGQtNmM2Ny00YzViLWIxMTItMzZhMzA0YjY2ZGFkL3YyLjAiLCJzdWIiOiJBQUFBQUFBQUFBQUFBQUFBQUFBQUFJa3pxRlZyU2FTYUZIeTc4MmJidGFRIiwiYXVkIjoiNmNiMDQwMTgtYTNmNS00NmE3LWI5OTUtOTQwYzc4ZjVhZWYzIiwiZXhwIjoxNTM2MzYxNDExLCJpYXQiOjE1MzYyNzQ3MTEsIm5iZiI6MTUzNjI3NDcxMSwibmFtZSI6IkFiZSBMaW5jb2xuIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiQWJlTGlAbWljcm9zb2Z0LmNvbSIsIm9pZCI6IjAwMDAwMDAwLTAwMDAtMDAwMC02NmYzLTMzMzJlY2E3ZWE4MSIsInRpZCI6IjMzMzgwNDBkLTZjNjctNGM1Yi1iMTEyLTM2YTMwNGI2NmRhZCIsIm5vbmNlIjoiMTIzNTIzIiwiYWlvIjoiRGYyVVZYTDFpeCFsTUNXTVNPSkJjRmF0emNHZnZGR2hqS3Y4cTVnMHg3MzJkUjVNQjVCaXN2R1FPN1lXQnlqZDhpUURMcSFlR2JJRGFreXA1bW5PcmNkcUhlWVNubHRlcFFtUnA2QUlaOGpZIn0=.1AFWW-Ck5nROwSlltm7GzZvDwUkqvhSQpm55TQsmVo9Y59cLhRXpvB8n-55HCr9Z6G_31_UbeUkoz612I2j_Sm9FFShSDDjoaLQr54CreGIJvjtmS3EkK9a7SJBbcpL1MpUtlfygow39tFjY7EVNW9plWUvRrTgVk7lYLprvfzw-CIqw3gHC-T7IK_m_xkr08INERBtaecwhTeN4chPC4W3jdmw_lIxzC48YoQ0dB1L9-ImX98Egypfrlbm0IBL5spFzL6JDZIRRJOu8vecJvj1mq-IUhGt0MacxX8jdxYLP-KUu2d9MbNKpCKJuZ7p8gwTL5B7NlUdh_dmSviPWrw", + "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImNlNTIyNzBmNDYyNDNkOWRmMmE5ODBiNGNmNmJhNDA3In0.eyJ2ZXIiOiIyLjAiLCJpc3MiOiJodHRwczovL2xvZ2luLm1pY3Jvc29mdG9ubGluZS5jb20vMzMzODA0MGQtNmM2Ny00YzViLWIxMTItMzZhMzA0YjY2ZGFkL3YyLjAiLCJzdWIiOiJBQUFBQUFBQUFBQUFBQUFBQUFBQUFJa3pxRlZyU2FTYUZIeTc4MmJidGFRIiwiYXVkIjoiNmNiMDQwMTgtYTNmNS00NmE3LWI5OTUtOTQwYzc4ZjVhZWYzIiwiZXhwIjoxNTM2MzYxNDExLCJpYXQiOjE1MzYyNzQ3MTEsIm5iZiI6MTUzNjI3NDcxMSwibmFtZSI6IkFiZSBMaW5jb2xuIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiQWJlTGlAbWljcm9zb2Z0LmNvbSIsImxvZ2luX2hpbnQiOiJBYmVMaUxvZ2luSGludCIsInVwbiI6IkFiZUxpVXBuIiwib2lkIjoiMDAwMDAwMDAtMDAwMC0wMDAwLTY2ZjMtMzMzMmVjYTdlYTgxIiwidGlkIjoiMzMzODA0MGQtNmM2Ny00YzViLWIxMTItMzZhMzA0YjY2ZGFkIiwibm9uY2UiOiIxMjM1MjMiLCJhaW8iOiJEZjJVVlhMMWl4IWxNQ1dNU09KQmNGYXR6Y0dmdkZHaGpLdjhxNWcweDczMmRSNU1CNUJpc3ZHUU83WVdCeWpkOGlRRExxIWVHYklEYWt5cDVtbk9yY2RxSGVZU25sdGVwUW1ScDZBSVo4alkifQ.evae_4e4k-mHaJ4bqvCGJ5kuh7h49pemASIz-A1N4K1hms3dkQ9Fz7PeLPHVrfSDvxi5JFbhKDkplU4Io8PbNxQVpZemUuSHy6JlrmNBubW8UWgM_YhDaoa55d5g12pGizIba35ymeqB43d6snUP819TK6eSpPwZhBgvEyOG5fp69y0ALEiXVKdf-qJ3rcupVKygrk5TqhZLis8r0NLziFrvddVCOIx9SnC72sV-dHGDHn6oDw8CXKl-szwXrSELI8FfcTHLRA_vD6_zTx4fgBICS66CvorcQjWQOzs8AVm6KjsIAjck65xOXltzvnPEYwf1Y73Ryki-F1SfxmPo6u_TkTObcQXtDps5R-ZIk5HdFe8vr-aX-WGiXS3SEQKynV23dTRqDqTATBQzOz9eiJnmjV8unZGe1GdgPgvyJSc9JPK4wfM5ElJMtizGqPaG08VzKnCYTk32hhhsX2krT_6Ii5NFp5l8SNWO8mfCl6OXai8_XsWQhe0q_fTWTqoT", + ID_TOKEN_V2_GUEST: + "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImNlNTIyNzBmNDYyNDNkOWRmMmE5ODBiNGNmNmJhNDA3In0.eyJ2ZXIiOiIyLjAiLCJpc3MiOiJodHRwczovL2xvZ2luLm1pY3Jvc29mdG9ubGluZS5jb20vNzJmOTg4YmYtODZmMS00MWFmLTkxYWItMmQ3Y2QwMTFkYjQ3Iiwic3ViIjoiQUFBQUFBQUFBQUFBQUFBQUFBQUFBSWt6cUZWclNhU2FGSHk3ODJiYnRiUSIsImF1ZCI6IjZjYjA0MDE4LWEzZjUtNDZhNy1iOTk1LTk0MGM3OGY1YWVmMyIsImV4cCI6MTUzNjM2MTQxMSwiaWF0IjoxNTM2Mjc0NzExLCJuYmYiOjE1MzYyNzQ3MTEsIm5hbWUiOiJBYmUgTGluY29sbiBHdWVzdCIsInByZWZlcnJlZF91c2VybmFtZSI6IkFiZUxpR3Vlc3RAbWljcm9zb2Z0LmNvbSIsImxvZ2luX2hpbnQiOiJBYmVMaUd1ZXN0TG9naW5IaW50IiwidXBuIjoiQWJlTGlHdWVzdFVwbiIsIm9pZCI6IjAwMDAwMDAwLTAwMDAtMDAwMC0xMTExLTAwMDAwMDAwMDAwMCIsInRpZCI6IjcyZjk4OGJmLTg2ZjEtNDFhZi05MWFiLTJkN2NkMDExZGI0NyIsIm5vbmNlIjoiMTIzNTIzIiwiYWlvIjoiRGYyVVZYTDFpeCFsTUNXTVNPSkJjRmF0emNHZnZGR2hqS3Y4cTVnMHg3MzJkUjVNQjVCaXN2R1FPN1lXQnlqZDhpUURMcSFlR2JJRGFreXA1bW5PcmNkcUhlWVNubHRlcFFtUnA2QUlaOGpZIn0.JM3BG-MgPQe5RwkPXRyjSK7hGh_wlfbRg50-bBhA1qP8_2kQewX0lmKhaNs99stb8xjRi8SOrq2DlBUCqvr0srfOzGhlfRz346qo82CZWzDw3OE3iaUwPDzSSruvWjPAw5G3uuKMqQE9F51iRmgUX0AP8hovutCNjnHBONruoOJ25v2hA3R5H7U3BEiozjKIxkrPvkJuE6wC2krwAvLiqjtlSDZl1NZ-bvPuVZWv6OtzNDcWSO0EtirYAIL0mqgE4yyVqfGnMD7jqkYdmrR4nJZUKW_Jj7A1ihRLHRO8Ef98zgfHLF4t07-wssYKM3L9Cjms3HaYQ0MUb6LXMUyE3NzqaVvsjfz6W282CraWeeIAjG8D2G0HStlA5zHAGulWQcrKRj4tZFFjQ_lj89IlXWyov7xqcJeTDGqUyydJ69PjD8Ov4L16kuU8x4N_qA9tqb2TLmfBXBqpJCdFc5kcDqVk016eav7pq3jOkbHJqp207aTkJRtMK20OfJ4IAoj1", IDTOKEN_V2_ALT: - "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IjJjNGE2MmYzMGRkNWMxNGE4MGFmMjMxZjY2M2ZmODliIn0.eyJ2ZXIiOiIyLjAiLCJpc3MiOiJodHRwczovL2xvZ2luLm1pY3Jvc29mdG9ubGluZS5jb20vOTE4ODA0MGQtNmM2Ny00YzViLWIxMTItMzZhMzA0YjY2ZGFkL3YyLjAiLCJzdWIiOiJBQUFBQUFBQUFBQUFBQUFBQUFBQUFJa3pxRlZyU2FTYUZIeTc4MmJidGFRIiwiYXVkIjoiNmNiMDQwMTgtYTNmNS00NmE3LWI5OTUtOTQwYzc4ZjVhZWYzIiwiZXhwIjoxNTM2MzYxNDExLCJpYXQiOjE1MzYyNzQ3MTEsIm5iZiI6MTUzNjI3NDcxMSwibmFtZSI6IkFiZSBMaW5jb2xuIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiQWJlTGlUb29AbWljcm9zb2Z0LmNvbSIsIm9pZCI6IjAwMDAwMDAwLTAwMDAtMDAwMC0wMDAwLTAwMDAwMDAwMDAwMCIsInRpZCI6IjcyZjk4OGJmLTg2ZjEtNDFhZi05MWFiLTJkN2NkMDExZGI0NyIsIm5vbmNlIjoiMTIzNTIzIiwiYWlvIjoiRGYyVVZYTDFpeCFsTUNXTVNPSkJjRmF0emNHZnZGR2hqS3Y4cTVnMHg3MzJkUjVNQjVCaXN2R1FPN1lXQnlqZDhpUURMcSFlR2JJRGFreXA1bW5PcmNkcUhlWVNubHRlcFFtUnA2QUlaOGpZIn0.VIsqgZiJfiOx-VkvYlq9X3EKmoRmfmHyyrPtSGww20Ng-eJCyQZqYlXEnsc44tuA9czlFpba9WwB2bCiS5jq4kjcTaQCg7ocxD1Db6PwY2mrAQdKUvDaZ7wSVXgOmR8SkyzqN55EGcMopXDOY12BLx_859lJYBJM-2ICmnscMNEjg2DZ1QvPRz7NX4MoOXWlBwhibxgQ4YEfq5LPOCit3LnSsT2Io1ORQouDPCfe55jRUoQoHyWOuXHA5w8Zr7Su6eHCnXVHv9k8mbAii_qOeLIaDg6Gh8B9Ht-BmBVExzzveUSe2ZLYxMDLj8BOhxlHn156l4bSqeEP40uzFae9uO4uVX4oIrd9p2MdbKm0PUahILtt5HJg4rs2f4xiyPC5Yt22QWPCqE8HFf9p3ZliAWmnAw6LyYIRl6rw3vnZFm031NWcRfSme9CfDbnqdks7qnLfVVgrh-1KoKgi6sTUxpJfXN5-VHw47k0tU7UZyIUDekt-mbXnxoAWy7LhnNQ3", + "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImNlNTIyNzBmNDYyNDNkOWRmMmE5ODBiNGNmNmJhNDA3In0.eyJ2ZXIiOiIyLjAiLCJpc3MiOiJodHRwczovL2xvZ2luLm1pY3Jvc29mdG9ubGluZS5jb20vNzJmOTg4YmYtODZmMS00MWFmLTkxYWItMmQ3Y2QwMTFkYjQ3Iiwic3ViIjoiQUFBQUFBQUFBQUFBQUFBQUFBQUFBSWt6cUZWclNhU2FGSHk3ODJiYnRhUSIsImF1ZCI6IjZjYjA0MDE4LWEzZjUtNDZhNy1iOTk1LTk0MGM3OGY1YWVmMyIsImV4cCI6MTUzNjM2MTQxMSwiaWF0IjoxNTM2Mjc0NzExLCJuYmYiOjE1MzYyNzQ3MTEsIm5hbWUiOiJBYmUgTGluY29sbiBUb28iLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJBYmVMaVRvb0BtaWNyb3NvZnQuY29tIiwibG9naW5faGludCI6IkFiZUxpVG9vTG9naW5IaW50IiwidXBuIjoiQWJlTGlUb29VcG4iLCJvaWQiOiIwMDAwMDAwMC0wMDAwLTAwMDAtMDAwMC0wMDAwMDAwMDAwMDAiLCJ0aWQiOiI3MmY5ODhiZi04NmYxLTQxYWYtOTFhYi0yZDdjZDAxMWRiNDciLCJub25jZSI6IjEyMzUyMyIsImFpbyI6IkRmMlVWWEwxaXghbE1DV01TT0pCY0ZhdHpjR2Z2Rkdoakt2OHE1ZzB4NzMyZFI1TUI1QmlzdkdRTzdZV0J5amQ4aVFETHEhZUdiSURha3lwNW1uT3JjZHFIZVlTbmx0ZXBRbVJwNkFJWjhqWSJ9.Ek_dqta8WJvNO5W9eB5x4hjJ-uUaQcbBA1v2X1NV_7rWiv61lwLCy9aDTNRuflPV4_tG7JGVscgFRz81YTA0s9Gmvb4C0-_jWllTDh15WYlUmNuGjTg5fimgyDsUY_-5OuoCilLB7TCxY6fSktOQQWRyqVrtkTEf_m1yghSq9yT1VbkbACVYS8WyhwoXHJ6F0dwJnlX9CoZEVjv0R-P00VBUZpmI5qyA3udesch_zfshqZxrBp9od44VYDcPRg_2yfl1B48utSSPcTmRh86JVZtgiDN4MO_R5W7KoyzHsVhu6UmWICjaZB7Q4ygKQXHGDhMm2V-21YW_XuHlAnHzF5GmxE7y8p1JMqtVFPCsZz6_3AVF-JRxyArrtVvgE4zRL4fM9yUBRnwvjKyZk0hJzDhYAvWBCozWbYjGBS-6LFE8ByeRSP9UufdKR-p_Q5msdQ9tjpbRRfTf6EuyKV4q1G-v2XZPu3SAuM9n90o1X8zwO6iRa6j90SqMIJUVCcS6", IDTOKEN_V2_NEWCLAIM: "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IjFMVE16YWtpaGlSbGFfOHoyQkVKVlhlV01xbyJ9.ewogICJ2ZXIiOiAiMi4wIiwKICAiaXNzIjogImh0dHBzOi8vbG9naW4ubWljcm9zb2Z0b25saW5lLmNvbS85MTg4MDQwZC02YzY3LTRjNWItYjExMi0zNmEzMDRiNjZkYWQvdjIuMCIsCiAgInN1YiI6ICJBQUFBQUFBQUFBQUFBQUFBQUFBQUFJa3pxRlZyU2FTYUZIeTc4MmJidGFRIiwKICAiYXVkIjogIjZjYjA0MDE4LWEzZjUtNDZhNy1iOTk1LTk0MGM3OGY1YWVmMyIsCiAgImV4cCI6IDE1MzYzNjE0MTEsCiAgImlhdCI6IDE1MzYyNzQ3MTEsCiAgIm5iZiI6IDE1MzYyNzQ3MTEsCiAgIm5hbWUiOiAiQWJlIExpbmNvbG4iLAogICJwcmVmZXJyZWRfdXNlcm5hbWUiOiAiQWJlTGlAbWljcm9zb2Z0LmNvbSIsCiAgIm9pZCI6ICIwMDAwMDAwMC0wMDAwLTAwMDAtNjZmMy0zMzMyZWNhN2VhODEiLAogICJlbWFpbCI6ICJBYmVMaUBtaWNyb3NvZnQuY29tIiwKICAidGlkIjogIjMzMzgwNDBkLTZjNjctNGM1Yi1iMTEyLTM2YTMwNGI2NmRhZCIsCiAgIm5vbmNlIjogIjEyMzUyMyIsCiAgImFpbyI6ICJEZjJVVlhMMWl4IWxNQ1dNU09KQmNGYXR6Y0dmdkZHaGpLdjhxNWcweDczMmRSNU1CNUJpc3ZHUU83WVdCeWpkOGlRRExxIWVHYklEYWt5cDVtbk9yY2RxSGVZU25sdGVwUW1ScDZBSVo4alkiCn0=.1AFWW-Ck5nROwSlltm7GzZvDwUkqvhSQpm55TQsmVo9Y59cLhRXpvB8n-55HCr9Z6G_31_UbeUkoz612I2j_Sm9FFShSDDjoaLQr54CreGIJvjtmS3EkK9a7SJBbcpL1MpUtlfygow39tFjY7EVNW9plWUvRrTgVk7lYLprvfzw-CIqw3gHC-T7IK_m_xkr08INERBtaecwhTeN4chPC4W3jdmw_lIxzC48YoQ0dB1L9-ImX98Egypfrlbm0IBL5spFzL6JDZIRRJOu8vecJvj1mq-IUhGt0MacxX8jdxYLP-KUu2d9MbNKpCKJuZ7p8gwTL5B7NlUdh_dmSviPWrw", LOGIN_AT_STRING: @@ -53,7 +55,7 @@ export const TEST_TOKENS = { export const ID_TOKEN_CLAIMS = { ver: "2.0", - iss: "https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0", + iss: "https://login.microsoftonline.com/3338040d-6c67-4c5b-b112-36a304b66dad/v2.0", sub: "AAAAAAAAAAAAAAAAAAAAAIkzqFVrSaSaFHy782bbtaQ", aud: "6cb04018-a3f5-46a7-b995-940c78f5aef3", exp: 1536361411, @@ -61,19 +63,42 @@ export const ID_TOKEN_CLAIMS = { nbf: 1536274711, name: "Abe Lincoln", preferred_username: "AbeLi@microsoft.com", + login_hint: "AbeLiLoginHint", + upn: "AbeLiUpn", oid: "00000000-0000-0000-66f3-3332eca7ea81", tid: "3338040d-6c67-4c5b-b112-36a304b66dad", nonce: "123523", aio: "Df2UVXL1ix!lMCWMSOJBcFatzcGfvFGhjKv8q5g0x732dR5MB5BisvGQO7YWByjd8iQDLq!eGbIDakyp5mnOrcdqHeYSnltepQmRp6AIZ8jY", }; +export const GUEST_ID_TOKEN_CLAIMS = { + ...ID_TOKEN_CLAIMS, + iss: "https://login.microsoftonline.com/72f988bf-86f1-41af-91ab-2d7cd011db47", + sub: "AAAAAAAAAAAAAAAAAAAAAIkzqFVrSaSaFHy782bbtbQ", + name: "Abe Lincoln Guest", + preferred_username: "AbeLiGuest@microsoft.com", + login_hint: "AbeLiGuestLoginHint", + upn: "AbeLiGuestUpn", + oid: "00000000-0000-0000-1111-000000000000", + tid: "72f988bf-86f1-41af-91ab-2d7cd011db47", +}; + export const ID_TOKEN_ALT_CLAIMS = { ...ID_TOKEN_CLAIMS, + iss: "https://login.microsoftonline.com/72f988bf-86f1-41af-91ab-2d7cd011db47", + name: "Abe Lincoln Too", preferred_username: "AbeLiToo@microsoft.com", + login_hint: "AbeLiTooLoginHint", + upn: "AbeLiTooUpn", oid: "00000000-0000-0000-0000-000000000000", tid: "72f988bf-86f1-41af-91ab-2d7cd011db47", }; +export const ID_TOKEN_EXTRA_CLAIMS = { + tfp: "B2C_1A_signup_signin", + acr: "POLICY", +}; + // Test Expiration Vals export const TEST_TOKEN_LIFETIMES = { DEFAULT_EXPIRES_IN: 3599, diff --git a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/auth.js b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/auth.js index 6cdcf16816..b412fd83b6 100644 --- a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/auth.js +++ b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/auth.js @@ -14,29 +14,27 @@ myMSALObj.initialize().then(() => { function handleResponse() { // when a filter is passed into getAllAccounts, it returns all cached accounts that match the filter. Use isHomeTenant filter to get the home accounts. - const homeAccounts = myMSALObj.getAllAccounts(); - console.log("Get all accounts: ", homeAccounts); - if (!homeAccounts || homeAccounts.length < 1) { + const allAccounts = myMSALObj.getAllAccounts({ tenantId: homeTenant, username: "hemoral@microsoft.com" }); + console.log("Get all accounts: ", allAccounts); + if (!allAccounts || allAccounts.length < 1) { return; - } else if (homeAccounts.length === 1) { + } else if (allAccounts.length >= 1) { // Get all accounts returns the homeAccount with tenantProfiles when multiTenantAccountsEnabled is set to true - pickActiveAccountAndTenantProfile(homeAccounts[0]); - } else if (homeAccounts.length > 1) { - // Select account logic + pickActiveAccountAndTenantProfile(allAccounts[0]); } } // Determines whether there is one or multiple tenant profiles to pick from and sets the active account based on the user selection if necessary. -async function pickActiveAccountAndTenantProfile(homeAccount) { +async function pickActiveAccountAndTenantProfile(account) { // Set home tenant profile as default active account let activeAccount = myMSALObj.getActiveAccount(); if (!activeAccount) { - activeAccount = homeAccount; + activeAccount = account; myMSALObj.setActiveAccount(activeAccount); } accountId = activeAccount.homeAccountId; showWelcomeMessage(activeAccount); - showTenantProfilePicker(homeAccount.tenantProfiles || new Map(), activeAccount); + showTenantProfilePicker(account.tenantProfiles || new Map(), activeAccount); } async function setActiveAccount(tenantId) { diff --git a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/authConfig.js b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/authConfig.js index 77d9a22636..545004a514 100644 --- a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/authConfig.js +++ b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/authConfig.js @@ -1,4 +1,4 @@ -const homeTenant = "common"; +const homeTenant = "72f988bf-86f1-41af-91ab-2d7cd011db47"; const guestTenant = "5d97b14d-c396-4aee-b524-c86d33e9b660" const baseAuthority = "https://login.microsoftonline.com" From 15162caf7feb3c2741c746c5eba7af00b52a1765 Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Sat, 11 Nov 2023 21:07:18 -0800 Subject: [PATCH 50/91] Update common tests --- lib/msal-common/test/cache/MockCache.ts | 2 +- .../test/client/RefreshTokenClient.spec.ts | 33 +++++-------------- .../test/test_kit/StringConstants.ts | 20 +++++------ 3 files changed, 19 insertions(+), 36 deletions(-) diff --git a/lib/msal-common/test/cache/MockCache.ts b/lib/msal-common/test/cache/MockCache.ts index 5c59da54de..642ca116f0 100644 --- a/lib/msal-common/test/cache/MockCache.ts +++ b/lib/msal-common/test/cache/MockCache.ts @@ -305,7 +305,7 @@ export function buildAccountFromIdTokenClaims( username: preferred_username || email || "", localAccountId: oid || "", tenantId: tenantId, - environment: "login.microsoftonline.com", + environment: "login.windows.net", authorityType: "MSSTS", name: name, tenantProfiles: new Map([ diff --git a/lib/msal-common/test/client/RefreshTokenClient.spec.ts b/lib/msal-common/test/client/RefreshTokenClient.spec.ts index 5858ced26a..e593d62e9c 100644 --- a/lib/msal-common/test/client/RefreshTokenClient.spec.ts +++ b/lib/msal-common/test/client/RefreshTokenClient.spec.ts @@ -57,6 +57,7 @@ import { } from "../../src/error/InteractionRequiredAuthError"; import { StubPerformanceClient } from "../../src/telemetry/performance/StubPerformanceClient"; import { ProtocolMode } from "../../src/authority/ProtocolMode"; +import { buildAccountFromIdTokenClaims } from "../cache/MockCache"; const testAccountEntity: AccountEntity = new AccountEntity(); testAccountEntity.homeAccountId = `${TEST_DATA_CLIENT_INFO.TEST_UID}.${TEST_DATA_CLIENT_INFO.TEST_UTID}`; @@ -299,18 +300,9 @@ describe("RefreshTokenClient unit tests", () => { let config: ClientConfiguration; let client: RefreshTokenClient; - const testAccount: AccountInfo = { - authorityType: "MSSTS", - homeAccountId: TEST_DATA_CLIENT_INFO.TEST_DECODED_HOME_ACCOUNT_ID, - tenantId: ID_TOKEN_CLAIMS.tid, - environment: "login.windows.net", - username: ID_TOKEN_CLAIMS.preferred_username, - name: ID_TOKEN_CLAIMS.name, - localAccountId: ID_TOKEN_CLAIMS.oid, - idTokenClaims: ID_TOKEN_CLAIMS, - nativeAccountId: undefined, - tenants: [ID_TOKEN_CLAIMS.tid], - }; + const testAccount: AccountInfo = + buildAccountFromIdTokenClaims(ID_TOKEN_CLAIMS).getAccountInfo(); + testAccount.idTokenClaims = ID_TOKEN_CLAIMS; beforeEach(async () => { sinon @@ -426,7 +418,7 @@ describe("RefreshTokenClient unit tests", () => { expect(authResult.uniqueId).toEqual(ID_TOKEN_CLAIMS.oid); expect(authResult.tenantId).toEqual(ID_TOKEN_CLAIMS.tid); expect(authResult.scopes).toEqual(expectedScopes); - expect(authResult.account).toEqual(testAccount); + expect(authResult.account).toMatchObject(testAccount); expect(authResult.idToken).toEqual( AUTHENTICATION_RESULT.body.id_token ); @@ -1056,18 +1048,9 @@ describe("RefreshTokenClient unit tests", () => { let config: ClientConfiguration; let client: RefreshTokenClient; - const testAccount: AccountInfo = { - authorityType: "MSSTS", - homeAccountId: TEST_DATA_CLIENT_INFO.TEST_DECODED_HOME_ACCOUNT_ID, - tenantId: ID_TOKEN_CLAIMS.tid, - environment: "login.windows.net", - username: ID_TOKEN_CLAIMS.preferred_username, - name: ID_TOKEN_CLAIMS.name, - localAccountId: ID_TOKEN_CLAIMS.oid, - idTokenClaims: ID_TOKEN_CLAIMS, - nativeAccountId: undefined, - tenants: [ID_TOKEN_CLAIMS.tid], - }; + const testAccount: AccountInfo = + buildAccountFromIdTokenClaims(ID_TOKEN_CLAIMS).getAccountInfo(); + testAccount.idTokenClaims = ID_TOKEN_CLAIMS; beforeEach(async () => { sinon diff --git a/lib/msal-common/test/test_kit/StringConstants.ts b/lib/msal-common/test/test_kit/StringConstants.ts index 375add4590..24c3b407fe 100644 --- a/lib/msal-common/test/test_kit/StringConstants.ts +++ b/lib/msal-common/test/test_kit/StringConstants.ts @@ -108,16 +108,18 @@ export const TEST_TOKEN_LIFETIMES = { // Test CLIENT_INFO export const TEST_DATA_CLIENT_INFO = { - TEST_UID: "123-test-uid", - TEST_UTID: "456-test-utid", - TEST_DECODED_CLIENT_INFO: '{"uid":"123-test-uid","utid":"456-test-utid"}', + TEST_UID: "00000000-0000-0000-66f3-3332eca7ea81", + TEST_UTID: "3338040d-6c67-4c5b-b112-36a304b66dad", + TEST_DECODED_CLIENT_INFO: + '{"uid":"00000000-0000-0000-66f3-3332eca7ea81","utid":"3338040d-6c67-4c5b-b112-36a304b66dad"}', TEST_INVALID_JSON_CLIENT_INFO: - '{"uid":"123-test-uid""utid":"456-test-utid"}', + '{"uid":"00000000-0000-0000-66f3-3332eca7ea81""utid":"3338040d-6c67-4c5b-b112-36a304b66dad"}', TEST_RAW_CLIENT_INFO: - "eyJ1aWQiOiIxMjMtdGVzdC11aWQiLCJ1dGlkIjoiNDU2LXRlc3QtdXRpZCJ9", + "eyJ1aWQiOiIwMDAwMDAwMC0wMDAwLTAwMDAtNjZmMy0zMzMyZWNhN2VhODEiLCJ1dGlkIjoiMzMzODA0MGQtNmM2Ny00YzViLWIxMTItMzZhMzA0YjY2ZGFkIn0", TEST_CLIENT_INFO_B64ENCODED: "eyJ1aWQiOiIxMjM0NSIsInV0aWQiOiI2Nzg5MCJ9", TEST_ENCODED_HOME_ACCOUNT_ID: "MTIzLXRlc3QtdWlk.NDU2LXRlc3QtdXRpZA==", - TEST_DECODED_HOME_ACCOUNT_ID: "123-test-uid.456-test-utid", + TEST_DECODED_HOME_ACCOUNT_ID: + "00000000-0000-0000-66f3-3332eca7ea81.3338040d-6c67-4c5b-b112-36a304b66dad", TEST_LOCAL_ACCOUNT_ID: "00000000-0000-0000-66f3-3332eca7ea81s", TEST_CACHE_RAW_CLIENT_INFO: "eyJ1aWQiOiJ1aWQiLCAidXRpZCI6InV0aWQifQ==", TEST_CACHE_DECODED_CLIENT_INFO: '{"uid":"uid", "utid":"utid"}', @@ -503,8 +505,7 @@ export const AUTHENTICATION_RESULT = { ext_expires_in: 3599, access_token: "thisIs.an.accessT0ken", refresh_token: "thisIsARefreshT0ken", - id_token: - "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IjFMVE16YWtpaGlSbGFfOHoyQkVKVlhlV01xbyJ9.eyJ2ZXIiOiIyLjAiLCJpc3MiOiJodHRwczovL2xvZ2luLm1pY3Jvc29mdG9ubGluZS5jb20vOTE4ODA0MGQtNmM2Ny00YzViLWIxMTItMzZhMzA0YjY2ZGFkL3YyLjAiLCJzdWIiOiJBQUFBQUFBQUFBQUFBQUFBQUFBQUFJa3pxRlZyU2FTYUZIeTc4MmJidGFRIiwiYXVkIjoiNmNiMDQwMTgtYTNmNS00NmE3LWI5OTUtOTQwYzc4ZjVhZWYzIiwiZXhwIjoxNTM2MzYxNDExLCJpYXQiOjE1MzYyNzQ3MTEsIm5iZiI6MTUzNjI3NDcxMSwibmFtZSI6IkFiZSBMaW5jb2xuIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiQWJlTGlAbWljcm9zb2Z0LmNvbSIsIm9pZCI6IjAwMDAwMDAwLTAwMDAtMDAwMC02NmYzLTMzMzJlY2E3ZWE4MSIsInRpZCI6IjMzMzgwNDBkLTZjNjctNGM1Yi1iMTEyLTM2YTMwNGI2NmRhZCIsIm5vbmNlIjoiMTIzNTIzIiwiYWlvIjoiRGYyVVZYTDFpeCFsTUNXTVNPSkJjRmF0emNHZnZGR2hqS3Y4cTVnMHg3MzJkUjVNQjVCaXN2R1FPN1lXQnlqZDhpUURMcSFlR2JJRGFreXA1bW5PcmNkcUhlWVNubHRlcFFtUnA2QUlaOGpZIn0=.1AFWW-Ck5nROwSlltm7GzZvDwUkqvhSQpm55TQsmVo9Y59cLhRXpvB8n-55HCr9Z6G_31_UbeUkoz612I2j_Sm9FFShSDDjoaLQr54CreGIJvjtmS3EkK9a7SJBbcpL1MpUtlfygow39tFjY7EVNW9plWUvRrTgVk7lYLprvfzw-CIqw3gHC-T7IK_m_xkr08INERBtaecwhTeN4chPC4W3jdmw_lIxzC48YoQ0dB1L9-ImX98Egypfrlbm0IBL5spFzL6JDZIRRJOu8vecJvj1mq-IUhGt0MacxX8jdxYLP-KUu2d9MbNKpCKJuZ7p8gwTL5B7NlUdh_dmSviPWrw", + id_token: TEST_TOKENS.IDTOKEN_V2, client_info: `${TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO}`, }, }; @@ -567,8 +568,7 @@ export const AUTHENTICATION_RESULT_WITH_FOCI = { ext_expires_in: 3599, access_token: "thisIs.an.accessT0ken", refresh_token: "thisIsARefreshT0ken", - id_token: - "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IjFMVE16YWtpaGlSbGFfOHoyQkVKVlhlV01xbyJ9.eyJ2ZXIiOiIyLjAiLCJpc3MiOiJodHRwczovL2xvZ2luLm1pY3Jvc29mdG9ubGluZS5jb20vOTE4ODA0MGQtNmM2Ny00YzViLWIxMTItMzZhMzA0YjY2ZGFkL3YyLjAiLCJzdWIiOiJBQUFBQUFBQUFBQUFBQUFBQUFBQUFJa3pxRlZyU2FTYUZIeTc4MmJidGFRIiwiYXVkIjoiNmNiMDQwMTgtYTNmNS00NmE3LWI5OTUtOTQwYzc4ZjVhZWYzIiwiZXhwIjoxNTM2MzYxNDExLCJpYXQiOjE1MzYyNzQ3MTEsIm5iZiI6MTUzNjI3NDcxMSwibmFtZSI6IkFiZSBMaW5jb2xuIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiQWJlTGlAbWljcm9zb2Z0LmNvbSIsIm9pZCI6IjAwMDAwMDAwLTAwMDAtMDAwMC02NmYzLTMzMzJlY2E3ZWE4MSIsInRpZCI6IjMzMzgwNDBkLTZjNjctNGM1Yi1iMTEyLTM2YTMwNGI2NmRhZCIsIm5vbmNlIjoiMTIzNTIzIiwiYWlvIjoiRGYyVVZYTDFpeCFsTUNXTVNPSkJjRmF0emNHZnZGR2hqS3Y4cTVnMHg3MzJkUjVNQjVCaXN2R1FPN1lXQnlqZDhpUURMcSFlR2JJRGFreXA1bW5PcmNkcUhlWVNubHRlcFFtUnA2QUlaOGpZIn0=.1AFWW-Ck5nROwSlltm7GzZvDwUkqvhSQpm55TQsmVo9Y59cLhRXpvB8n-55HCr9Z6G_31_UbeUkoz612I2j_Sm9FFShSDDjoaLQr54CreGIJvjtmS3EkK9a7SJBbcpL1MpUtlfygow39tFjY7EVNW9plWUvRrTgVk7lYLprvfzw-CIqw3gHC-T7IK_m_xkr08INERBtaecwhTeN4chPC4W3jdmw_lIxzC48YoQ0dB1L9-ImX98Egypfrlbm0IBL5spFzL6JDZIRRJOu8vecJvj1mq-IUhGt0MacxX8jdxYLP-KUu2d9MbNKpCKJuZ7p8gwTL5B7NlUdh_dmSviPWrw", + id_token: TEST_TOKENS.IDTOKEN_V2, client_info: `${TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO}`, foci: "1", }, From 90f6e021e931361b9fc992e983711d6ca4290433 Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Sun, 12 Nov 2023 22:32:25 -0800 Subject: [PATCH 51/91] Factor setCachedAccount out into an exported function in ResponseHandler to re-use in NativeInteractionClient and TokenCache.loadExternalTokens --- lib/msal-browser/src/cache/TokenCache.ts | 62 +-- .../NativeInteractionClient.ts | 26 +- .../test/app/PublicClientApplication.spec.ts | 351 ++++----------- .../test/cache/TestStorageManager.ts | 92 +++- .../test/cache/TokenCache.spec.ts | 27 +- .../test/event/EventHandler.spec.ts | 36 +- .../NativeInteractionClient.spec.ts | 425 +++++------------- .../interaction_client/RedirectClient.spec.ts | 207 +++------ .../SilentCacheClient.spec.ts | 43 +- .../test/utils/StringConstants.ts | 53 ++- .../src/cache/entities/AccountEntity.ts | 8 +- lib/msal-common/src/index.ts | 6 +- .../src/response/ResponseHandler.ts | 126 +++--- 13 files changed, 524 insertions(+), 938 deletions(-) diff --git a/lib/msal-browser/src/cache/TokenCache.ts b/lib/msal-browser/src/cache/TokenCache.ts index cad72715a9..3fdeb8150a 100644 --- a/lib/msal-browser/src/cache/TokenCache.ts +++ b/lib/msal-browser/src/cache/TokenCache.ts @@ -19,6 +19,7 @@ import { CacheRecord, TokenClaims, CacheHelpers, + buildAccountToCache, } from "@azure/msal-common"; import { BrowserConfiguration } from "../config/Configuration"; import { SilentRequest } from "../request/SilentRequest"; @@ -240,40 +241,43 @@ export class TokenCache implements ITokenCache { clientInfo?: string, requestHomeAccountId?: string ): AccountEntity { - let homeAccountId; - if (requestHomeAccountId) { - homeAccountId = requestHomeAccountId; - } else if (authority.authorityType !== undefined && clientInfo) { - homeAccountId = AccountEntity.generateHomeAccountId( - clientInfo, - authority.authorityType, - this.logger, - this.cryptoObj, - idTokenClaims - ); - } + if (this.isBrowserEnvironment) { + this.logger.verbose("TokenCache - loading account"); + let homeAccountId; + if (requestHomeAccountId) { + homeAccountId = requestHomeAccountId; + } else if (authority.authorityType !== undefined && clientInfo) { + homeAccountId = AccountEntity.generateHomeAccountId( + clientInfo, + authority.authorityType, + this.logger, + this.cryptoObj, + idTokenClaims + ); + } - if (!homeAccountId) { - throw createBrowserAuthError( - BrowserAuthErrorCodes.unableToLoadToken - ); - } + if (!homeAccountId) { + throw createBrowserAuthError( + BrowserAuthErrorCodes.unableToLoadToken + ); + } + const claimsTenantId = idTokenClaims.tid; - const accountEntity = AccountEntity.createAccount( - { + const cachedAccount = buildAccountToCache( + this.storage, + authority, homeAccountId, - idTokenClaims: idTokenClaims, + idTokenClaims, + this.cryptoObj, clientInfo, - environment: authority.hostnameAndPort, - }, - authority - ); - - if (this.isBrowserEnvironment) { - this.logger.verbose("TokenCache - loading account"); + claimsTenantId, + undefined, + undefined, + this.logger + ); - this.storage.setAccount(accountEntity); - return accountEntity; + this.storage.setAccount(cachedAccount); + return cachedAccount; } else { throw createBrowserAuthError( BrowserAuthErrorCodes.unableToLoadToken diff --git a/lib/msal-browser/src/interaction_client/NativeInteractionClient.ts b/lib/msal-browser/src/interaction_client/NativeInteractionClient.ts index 36ea9354a5..30e05018bf 100644 --- a/lib/msal-browser/src/interaction_client/NativeInteractionClient.ts +++ b/lib/msal-browser/src/interaction_client/NativeInteractionClient.ts @@ -35,6 +35,7 @@ import { AuthErrorCodes, updateAccountTenantProfileData, CacheHelpers, + buildAccountToCache, } from "@azure/msal-common"; import { BaseInteractionClient } from "./BaseInteractionClient"; import { BrowserConfiguration } from "../config/Configuration"; @@ -422,16 +423,17 @@ export class NativeInteractionClient extends BaseInteractionClient { idTokenClaims ); - const tenantId = idTokenClaims.tid; - const accountEntity = AccountEntity.createAccount( - { - homeAccountId: homeAccountIdentifier, - idTokenClaims: idTokenClaims, - clientInfo: response.client_info, - nativeAccountId: response.account.id, - tenants: tenantId ? [tenantId] : [], - }, - authority + const baseAccount = buildAccountToCache( + this.browserStorage, + authority, + homeAccountIdentifier, + idTokenClaims, + this.browserCrypto, + response.client_info, + idTokenClaims.tid, + undefined, + response.account.id, + this.logger ); // generate authenticationResult @@ -439,13 +441,13 @@ export class NativeInteractionClient extends BaseInteractionClient { response, request, idTokenClaims, - accountEntity, + baseAccount, authority.canonicalAuthority, reqTimestamp ); // cache accounts and tokens in the appropriate storage - this.cacheAccount(accountEntity); + this.cacheAccount(baseAccount); this.cacheNativeTokens( response, request, diff --git a/lib/msal-browser/test/app/PublicClientApplication.spec.ts b/lib/msal-browser/test/app/PublicClientApplication.spec.ts index 7e4319329e..8ee54e5c76 100644 --- a/lib/msal-browser/test/app/PublicClientApplication.spec.ts +++ b/lib/msal-browser/test/app/PublicClientApplication.spec.ts @@ -44,7 +44,6 @@ import { IdTokenEntity, CacheManager, PersistentCacheKeys, - Authority, AuthError, ProtocolMode, ServerResponseType, @@ -114,6 +113,10 @@ import { Configuration, buildConfiguration, } from "../../src/config/Configuration"; +import { + buildAccountFromIdTokenClaims, + buildIdToken, +} from "../cache/TestStorageManager"; const cacheConfig = { temporaryCacheLocation: BrowserCacheLocation.SessionStorage, @@ -773,16 +776,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, }, }; - const testIdTokenClaims: TokenClaims = { - ver: "2.0", - iss: "https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0", - sub: "AAAAAAAAAAAAAAAAAAAAAIkzqFVrSaSaFHy782bbtaQ", - name: "Abe Lincoln", - preferred_username: "AbeLi@microsoft.com", - oid: "00000000-0000-0000-66f3-3332eca7ea81", - tid: "3338040d-6c67-4c5b-b112-36a304b66dad", - nonce: "123523", - }; + const testIdTokenClaims: TokenClaims = ID_TOKEN_CLAIMS; const testAccount: AccountInfo = { homeAccountId: TEST_DATA_CLIENT_INFO.TEST_HOME_ACCOUNT_ID, localAccountId: TEST_DATA_CLIENT_INFO.TEST_UID, @@ -5074,39 +5068,20 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { describe("clearCache tests", () => { // Account 1 - const testAccountInfo1: AccountInfo = { - authorityType: "MSSTS", - homeAccountId: TEST_DATA_CLIENT_INFO.TEST_HOME_ACCOUNT_ID, - environment: "login.windows.net", - tenantId: TEST_DATA_CLIENT_INFO.TEST_UTID, - username: "example@microsoft.com", - name: "Abe Lincoln", - localAccountId: ID_TOKEN_CLAIMS.oid, - idToken: TEST_TOKENS.IDTOKEN_V2, - idTokenClaims: ID_TOKEN_CLAIMS, - nativeAccountId: undefined, - }; - - const testAccount1: AccountEntity = new AccountEntity(); - testAccount1.homeAccountId = testAccountInfo1.homeAccountId; - testAccount1.localAccountId = testAccountInfo1.localAccountId; - testAccount1.environment = testAccountInfo1.environment; - testAccount1.realm = testAccountInfo1.tenantId; - testAccount1.username = testAccountInfo1.username; - testAccount1.name = testAccountInfo1.name; - testAccount1.authorityType = "MSSTS"; - testAccount1.clientInfo = - TEST_DATA_CLIENT_INFO.TEST_CLIENT_INFO_B64ENCODED; + const testAccount: AccountEntity = + buildAccountFromIdTokenClaims(ID_TOKEN_CLAIMS); - const idToken1: IdTokenEntity = { - realm: testAccountInfo1.tenantId, - environment: testAccountInfo1.environment, - credentialType: "IdToken", - secret: TEST_TOKENS.IDTOKEN_V2, - clientId: TEST_CONFIG.MSAL_CLIENT_ID, - homeAccountId: testAccountInfo1.homeAccountId, + const testAccountInfo: AccountInfo = testAccount.getAccountInfo(); + const matchAccount: AccountInfo = { + ...testAccountInfo, + idTokenClaims: ID_TOKEN_CLAIMS, }; + const testIdToken: IdTokenEntity = buildIdToken( + ID_TOKEN_CLAIMS, + TEST_TOKENS.IDTOKEN_V2, + { clientId: TEST_CONFIG.MSAL_CLIENT_ID } + ); beforeEach(async () => { pca = (pca as any).controller; await pca.initialize(); @@ -5127,9 +5102,9 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { }); // @ts-ignore - pca.getBrowserStorage().setAccount(testAccount1); + pca.getBrowserStorage().setAccount(testAccount); // @ts-ignore - pca.getBrowserStorage().setIdTokenCredential(idToken1); + pca.getBrowserStorage().setIdTokenCredential(testIdToken); }); afterEach(() => { @@ -5140,140 +5115,57 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { it("browser cache cleared when clearCache called without a ClearCacheRequest object", () => { expect(pca.getActiveAccount()).toEqual(null); - pca.setActiveAccount(testAccountInfo1); + pca.setActiveAccount(matchAccount); const activeAccount = pca.getActiveAccount(); - expect(activeAccount?.idTokenClaims).not.toBeUndefined(); - expect(activeAccount).toEqual(testAccountInfo1); + expect(activeAccount).toEqual(matchAccount); pca.clearCache(); expect(pca.getActiveAccount()).toEqual(null); }); it("browser cache cleared when clearCache called with a ClearCacheRequest object", () => { expect(pca.getActiveAccount()).toEqual(null); - pca.setActiveAccount(testAccountInfo1); + pca.setActiveAccount(matchAccount); const activeAccount = pca.getActiveAccount(); - expect(activeAccount?.idTokenClaims).not.toBeUndefined(); - expect(activeAccount).toEqual(testAccountInfo1); + expect(activeAccount).toEqual(matchAccount); pca.clearCache({ - account: testAccountInfo1, + account: matchAccount, correlationId: "test123", }); expect(pca.getActiveAccount()).toEqual(null); }); }); - describe.only("getAccount tests", () => { + describe("getAccount tests", () => { // Account 1 - const testAccountInfo1: AccountInfo = { - authorityType: "MSSTS", - homeAccountId: TEST_DATA_CLIENT_INFO.TEST_HOME_ACCOUNT_ID, - environment: "login.windows.net", - tenantId: ID_TOKEN_CLAIMS.tid, - username: ID_TOKEN_CLAIMS.preferred_username, - name: "Abe Lincoln", - localAccountId: ID_TOKEN_CLAIMS.oid, - idTokenClaims: ID_TOKEN_CLAIMS, - nativeAccountId: undefined, - tenants: [ID_TOKEN_CLAIMS.tid], - }; - const testAccount1: AccountEntity = - AccountEntity.createFromAccountInfo(testAccountInfo1); + buildAccountFromIdTokenClaims(ID_TOKEN_CLAIMS); + const testAccountInfo1: AccountInfo = testAccount1.getAccountInfo(); + testAccountInfo1.idTokenClaims = ID_TOKEN_CLAIMS; + testAccount1.clientInfo = TEST_DATA_CLIENT_INFO.TEST_CLIENT_INFO_B64ENCODED; - const idToken1: IdTokenEntity = { - realm: testAccountInfo1.tenantId, - environment: testAccountInfo1.environment, - credentialType: "IdToken", - secret: TEST_TOKENS.IDTOKEN_V2, - clientId: TEST_CONFIG.MSAL_CLIENT_ID, - homeAccountId: testAccountInfo1.homeAccountId, - }; + const idToken1: IdTokenEntity = buildIdToken( + ID_TOKEN_CLAIMS, + TEST_TOKENS.IDTOKEN_V2, + { clientId: TEST_CONFIG.MSAL_CLIENT_ID } + ); // Account 2 - const testAccountInfo2: AccountInfo = { - authorityType: "MSSTS", - homeAccountId: "different-home-account-id", - environment: "login.windows.net", - tenantId: ID_TOKEN_ALT_CLAIMS.tid, - username: "anotherExample@microsoft.com", - name: "Abe Lincoln", - localAccountId: ID_TOKEN_ALT_CLAIMS.oid, - idTokenClaims: ID_TOKEN_ALT_CLAIMS, - nativeAccountId: undefined, - tenants: [ID_TOKEN_ALT_CLAIMS.tid], - }; const testAccount2: AccountEntity = - AccountEntity.createFromAccountInfo(testAccountInfo2); - testAccount2.clientInfo = - TEST_DATA_CLIENT_INFO.TEST_CLIENT_INFO_B64ENCODED; + buildAccountFromIdTokenClaims(ID_TOKEN_ALT_CLAIMS); + const testAccountInfo2: AccountInfo = testAccount2.getAccountInfo(); + testAccountInfo2.idTokenClaims = ID_TOKEN_ALT_CLAIMS; - const idToken2: IdTokenEntity = { - realm: testAccountInfo2.tenantId, - environment: testAccountInfo2.environment, - credentialType: "IdToken", - secret: TEST_TOKENS.IDTOKEN_V2_ALT, - clientId: TEST_CONFIG.MSAL_CLIENT_ID, - homeAccountId: testAccountInfo2.homeAccountId, - }; - - // Account 3 - const testAccountInfo3: AccountInfo = { - authorityType: "ADFS", - homeAccountId: "another-home-account-id", - environment: "login.windows.net", - tenantId: TEST_DATA_CLIENT_INFO.TEST_UTID, - username: "Unique Username", - name: "Abe Lincoln Two", - localAccountId: TEST_CONFIG.OID, - idToken: TEST_TOKENS.ID_TOKEN_V2_WITH_LOGIN_HINT, - nativeAccountId: undefined, - }; - - const testAccount3: AccountEntity = - AccountEntity.createFromAccountInfo(testAccountInfo3); - - testAccount3.clientInfo = - TEST_DATA_CLIENT_INFO.TEST_CLIENT_INFO_B64ENCODED; - - const idToken3: IdTokenEntity = { - realm: testAccountInfo3.tenantId, - environment: testAccountInfo3.environment, - credentialType: "IdToken", - secret: TEST_TOKENS.ID_TOKEN_V2_WITH_LOGIN_HINT, - clientId: TEST_CONFIG.MSAL_CLIENT_ID, - homeAccountId: testAccountInfo3.homeAccountId, - }; - - // Account 4 - const testAccountInfo4: AccountInfo = { - authorityType: "MSA", - homeAccountId: "upn-account-id", - environment: "login.windows.net", - tenantId: TEST_DATA_CLIENT_INFO.TEST_UTID, - username: "Upn User", - name: "Abe Lincoln Three", - localAccountId: TEST_CONFIG.OID, - idToken: TEST_TOKENS.ID_TOKEN_V2_WITH_UPN, - nativeAccountId: undefined, - }; - - const testAccount4: AccountEntity = - AccountEntity.createFromAccountInfo(testAccountInfo4); - - testAccount4.clientInfo = + testAccount2.clientInfo = TEST_DATA_CLIENT_INFO.TEST_CLIENT_INFO_B64ENCODED; - const idToken4: IdTokenEntity = { - realm: testAccountInfo4.tenantId, - environment: testAccountInfo4.environment, - credentialType: "IdToken", - secret: TEST_TOKENS.ID_TOKEN_V2_WITH_UPN, - clientId: TEST_CONFIG.MSAL_CLIENT_ID, - homeAccountId: testAccountInfo4.homeAccountId, - }; + const idToken2: IdTokenEntity = buildIdToken( + ID_TOKEN_ALT_CLAIMS, + TEST_TOKENS.IDTOKEN_V2_ALT, + { clientId: TEST_CONFIG.MSAL_CLIENT_ID } + ); beforeEach(async () => { pca = (pca as any).controller; @@ -5297,21 +5189,12 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { pca.getBrowserStorage().setAccount(testAccount1); // @ts-ignore pca.getBrowserStorage().setAccount(testAccount2); - // @ts-ignore - pca.getBrowserStorage().setAccount(testAccount3); - // @ts-ignore - pca.getBrowserStorage().setAccount(testAccount4); // @ts-ignore pca.getBrowserStorage().setIdTokenCredential(idToken1); // @ts-ignore pca.getBrowserStorage().setIdTokenCredential(idToken2); - // @ts-ignore - pca.getBrowserStorage().setIdTokenCredential(idToken3); - - // @ts-ignore - pca.getBrowserStorage().setIdTokenCredential(idToken4); }); afterEach(() => { @@ -5321,18 +5204,17 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { it("getAllAccounts with no account filter returns all signed in accounts", () => { const accounts = pca.getAllAccounts(); - expect(accounts).toHaveLength(4); + expect(accounts).toHaveLength(2); expect(accounts[0].idTokenClaims).not.toBeUndefined(); expect(accounts[1].idTokenClaims).not.toBeUndefined(); - expect(accounts[2].idTokenClaims).not.toBeUndefined(); - expect(accounts[3].idTokenClaims).not.toBeUndefined(); }); it("getAllAccounts returns all accounts matching the filter passed in", () => { - const accounts = pca.getAllAccounts({ authorityType: "MSSTS" }); + const authorityType = "MSSTS"; + const accounts = pca.getAllAccounts({ authorityType }); expect(accounts).toHaveLength(2); - expect(accounts[0].authorityType).toBe("MSSTS"); - expect(accounts[1].authorityType).toBe("MSSTS"); + expect(accounts[0].authorityType).toBe(authorityType); + expect(accounts[1].authorityType).toBe(authorityType); }); it("getAllAccounts returns empty array if no accounts signed in", () => { @@ -5342,18 +5224,22 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { }); it("getAccountByUsername returns account specified", () => { - const account = pca.getAccountByUsername("AbeLi@microsoft.com"); + const account = pca.getAccountByUsername( + ID_TOKEN_CLAIMS.preferred_username + ); expect(account?.idTokenClaims).not.toBeUndefined(); expect(account).toEqual(testAccountInfo1); }); it("getAccountByUsername returns account specified with case mismatch", () => { - const account = pca.getAccountByUsername("abeli@Microsoft.com"); + const account = pca.getAccountByUsername( + ID_TOKEN_CLAIMS.preferred_username.toUpperCase() + ); expect(account?.idTokenClaims).not.toBeUndefined(); expect(account).toEqual(testAccountInfo1); const account2 = pca.getAccountByUsername( - "anotherexample@microsoft.com" + ID_TOKEN_ALT_CLAIMS.preferred_username.toUpperCase() ); expect(account2?.idTokenClaims).not.toBeUndefined(); expect(account2).toEqual(testAccountInfo2); @@ -5374,7 +5260,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { it("getAccountByHomeId returns account specified", () => { const account = pca.getAccountByHomeId( - TEST_DATA_CLIENT_INFO.TEST_HOME_ACCOUNT_ID + testAccountInfo1.homeAccountId ); expect(account?.idTokenClaims).not.toBeUndefined(); expect(account).toEqual(testAccountInfo1); @@ -5417,11 +5303,11 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { describe("loginHint filter", () => { it("getAccount returns account specified using login_hint", () => { const account = pca.getAccount({ - loginHint: "testLoginHint", + loginHint: ID_TOKEN_CLAIMS.login_hint, }); expect(account?.idTokenClaims).not.toBeUndefined(); expect(account?.homeAccountId).toEqual( - testAccountInfo3.homeAccountId + testAccountInfo1.homeAccountId ); }); it("getAccount returns account specified using username", () => { @@ -5435,27 +5321,27 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { }); it("getAccount returns account specified using upn", () => { const account = pca.getAccount({ - loginHint: "testUpn", + loginHint: ID_TOKEN_CLAIMS.upn, }); expect(account?.idTokenClaims).not.toBeUndefined(); expect(account?.homeAccountId).toEqual( - testAccountInfo4.homeAccountId + testAccountInfo1.homeAccountId ); }); it("getAccount returns account specified using sid", () => { const account = pca.getAccount({ - sid: "testSid", + sid: ID_TOKEN_CLAIMS.sid, }); expect(account?.idTokenClaims).not.toBeUndefined(); expect(account?.homeAccountId).toEqual( - testAccountInfo3.homeAccountId + testAccountInfo1.homeAccountId ); }); }); it("getAccount returns account specified using homeAccountId", () => { const account = pca.getAccount({ - homeAccountId: TEST_DATA_CLIENT_INFO.TEST_HOME_ACCOUNT_ID, + homeAccountId: testAccountInfo1.homeAccountId, }); expect(account?.idTokenClaims).not.toBeUndefined(); expect(account).toEqual(testAccountInfo1); @@ -5490,73 +5376,33 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { describe("activeAccount API tests", () => { // Account 1 + const testAccount1: AccountEntity = + buildAccountFromIdTokenClaims(ID_TOKEN_CLAIMS); const testAccountInfo1: AccountInfo = { - authorityType: "MSSTS", - homeAccountId: `${ID_TOKEN_CLAIMS.oid}.${ID_TOKEN_CLAIMS.tid}`, - environment: "login.windows.net", - tenantId: ID_TOKEN_CLAIMS.tid, - username: "example@microsoft.com", - name: "Abe Lincoln", - localAccountId: ID_TOKEN_CLAIMS.oid, - idToken: TEST_TOKENS.IDTOKEN_V2, + ...testAccount1.getAccountInfo(), idTokenClaims: ID_TOKEN_CLAIMS, - nativeAccountId: undefined, }; - const testAccount1: AccountEntity = new AccountEntity(); - testAccount1.homeAccountId = testAccountInfo1.homeAccountId; - testAccount1.localAccountId = testAccountInfo1.localAccountId; - testAccount1.environment = testAccountInfo1.environment; - testAccount1.realm = testAccountInfo1.tenantId; - testAccount1.username = testAccountInfo1.username; - testAccount1.name = testAccountInfo1.name; - testAccount1.authorityType = "MSSTS"; - testAccount1.clientInfo = - TEST_DATA_CLIENT_INFO.TEST_CLIENT_INFO_B64ENCODED; - - const idToken1: IdTokenEntity = { - realm: testAccountInfo1.tenantId, - environment: testAccountInfo1.environment, - credentialType: "IdToken", - secret: TEST_TOKENS.IDTOKEN_V2, - clientId: TEST_CONFIG.MSAL_CLIENT_ID, - homeAccountId: testAccountInfo1.homeAccountId, - }; + const idToken1: IdTokenEntity = buildIdToken( + ID_TOKEN_CLAIMS, + TEST_TOKENS.IDTOKEN_V2, + { clientId: TEST_CONFIG.MSAL_CLIENT_ID } + ); // Account 2 + + const testAccount2: AccountEntity = + buildAccountFromIdTokenClaims(ID_TOKEN_ALT_CLAIMS); const testAccountInfo2: AccountInfo = { - authorityType: "MSSTS", - homeAccountId: - TEST_DATA_CLIENT_INFO.TEST_HOME_ACCOUNT_ID + ".flow2", - environment: "login.windows.net", - tenantId: TEST_DATA_CLIENT_INFO.TEST_UTID, - username: "example@microsoft.com", - name: "Abe Lincoln", - localAccountId: TEST_CONFIG.OID, - idToken: TEST_TOKENS.IDTOKEN_V2, - idTokenClaims: ID_TOKEN_CLAIMS, - nativeAccountId: undefined, + ...testAccount2.getAccountInfo(), + idTokenClaims: ID_TOKEN_ALT_CLAIMS, }; - const testAccount2: AccountEntity = new AccountEntity(); - testAccount2.homeAccountId = testAccountInfo2.homeAccountId; - testAccount2.localAccountId = TEST_CONFIG.OID; - testAccount2.environment = testAccountInfo2.environment; - testAccount2.realm = testAccountInfo2.tenantId; - testAccount2.username = testAccountInfo2.username; - testAccount2.name = testAccountInfo2.name; - testAccount2.authorityType = "MSSTS"; - testAccount2.clientInfo = - TEST_DATA_CLIENT_INFO.TEST_CLIENT_INFO_B64ENCODED; - - const idToken2: IdTokenEntity = { - realm: testAccountInfo2.tenantId, - environment: testAccountInfo2.environment, - credentialType: "IdToken", - secret: TEST_TOKENS.IDTOKEN_V2, - clientId: TEST_CONFIG.MSAL_CLIENT_ID, - homeAccountId: testAccountInfo2.homeAccountId, - }; + const idToken2: IdTokenEntity = buildIdToken( + ID_TOKEN_ALT_CLAIMS, + TEST_TOKENS.IDTOKEN_V2_ALT, + { clientId: TEST_CONFIG.MSAL_CLIENT_ID } + ); beforeEach(async () => { pca = (pca as any).controller; @@ -5606,34 +5452,6 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { expect(activeAccount).toEqual(testAccountInfo1); }); - it("getActiveAccount looks up the current account values and returns them", () => { - pca.setActiveAccount(testAccountInfo1); - const activeAccount1 = pca.getActiveAccount(); - expect(activeAccount1?.idToken).not.toBeUndefined(); - expect(activeAccount1).toEqual(testAccountInfo1); - - const newName = "Ben Franklin"; - const newTestAccountInfo1 = { - ...testAccountInfo1, - name: newName, - }; - const newTestAccount1 = { - ...testAccount1, - name: newName, - }; - - const cacheKey = - AccountEntity.generateAccountCacheKey(newTestAccountInfo1); - window.sessionStorage.setItem( - cacheKey, - JSON.stringify(newTestAccount1) - ); - - const activeAccount2 = pca.getActiveAccount(); - expect(activeAccount2?.idToken).not.toBeUndefined(); - expect(activeAccount2).toEqual(newTestAccountInfo1); - }); - it("getActiveAccount picks up legacy account id from local storage", async () => { let pcaLocal = new PublicClientApplication({ auth: { @@ -5918,15 +5736,8 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { describe("hydrateCache tests", () => { const testAccount: AccountInfo = { - authorityType: "MSSTS", - homeAccountId: `${ID_TOKEN_CLAIMS.oid}.${ID_TOKEN_CLAIMS.tid}`, - localAccountId: ID_TOKEN_CLAIMS.oid, - environment: "login.windows.net", - tenantId: ID_TOKEN_CLAIMS.tid, - username: ID_TOKEN_CLAIMS.preferred_username, + ...buildAccountFromIdTokenClaims(ID_TOKEN_CLAIMS).getAccountInfo(), idTokenClaims: ID_TOKEN_CLAIMS, - name: ID_TOKEN_CLAIMS.name, - nativeAccountId: undefined, }; const testAuthenticationResult: AuthenticationResult = { diff --git a/lib/msal-browser/test/cache/TestStorageManager.ts b/lib/msal-browser/test/cache/TestStorageManager.ts index f72c46c1b5..3ef0969267 100644 --- a/lib/msal-browser/test/cache/TestStorageManager.ts +++ b/lib/msal-browser/test/cache/TestStorageManager.ts @@ -16,6 +16,11 @@ import { ValidCredentialType, TokenKeys, CacheHelpers, + TokenClaims, + CredentialType, + buildTenantProfileFromIdTokenClaims, + TenantProfile, + AccountInfo, } from "@azure/msal-common"; const ACCOUNT_KEYS = "ACCOUNT_KEYS"; @@ -33,6 +38,25 @@ export class TestStorageManager extends CacheManager { return null; } + removeAccountKeyFromMap(key: string): void { + const currentAccounts = this.getAccountKeys(); + const removalIndex = currentAccounts.indexOf(key); + if (removalIndex > -1) { + currentAccounts.splice(removalIndex, 1); + this.store[ACCOUNT_KEYS] = currentAccounts; + } + } + + getCachedAccountEntity(key: string): AccountEntity | null { + const account = this.store[key] as AccountEntity; + if (!account) { + this.removeAccountKeyFromMap(key); + return null; + } + + return account; + } + setAccount(value: AccountEntity): void { const key = value.generateAccountKey(); this.store[key] = value; @@ -46,12 +70,7 @@ export class TestStorageManager extends CacheManager { async removeAccount(key: string): Promise { await super.removeAccount(key); - const currentAccounts = this.getAccountKeys(); - const removalIndex = currentAccounts.indexOf(key); - if (removalIndex > -1) { - currentAccounts.splice(removalIndex, 1); - this.store[ACCOUNT_KEYS] = currentAccounts; - } + this.removeAccountKeyFromMap(key); } removeOutdatedAccount(accountKey: string): void { @@ -191,3 +210,64 @@ export class TestStorageManager extends CacheManager { return currentCacheKey; } } + +export function buildAccountFromIdTokenClaims( + idTokenClaims: TokenClaims, + guestIdTokenClaimsList?: TokenClaims[], + options?: Partial +): AccountEntity { + const { oid, tid, preferred_username, emails, name } = idTokenClaims; + const tenantId = tid || ""; + const email = emails ? emails[0] : null; + + const homeAccountId = `${oid}.${tid}`; + + const accountInfo: AccountInfo = { + homeAccountId: homeAccountId || "", + username: preferred_username || email || "", + localAccountId: oid || "", + tenantId: tenantId, + environment: "login.windows.net", + authorityType: "MSSTS", + name: name, + tenantProfiles: new Map([ + [ + tenantId, + buildTenantProfileFromIdTokenClaims( + homeAccountId, + idTokenClaims + ), + ], + ]), + }; + guestIdTokenClaimsList?.forEach((guestIdTokenClaims: TokenClaims) => { + const guestTenantId = guestIdTokenClaims.tid || ""; + accountInfo.tenantProfiles?.set( + guestTenantId, + buildTenantProfileFromIdTokenClaims( + accountInfo.homeAccountId, + guestIdTokenClaims + ) + ); + }); + return AccountEntity.createFromAccountInfo({ ...accountInfo, ...options }); +} + +export function buildIdToken( + idTokenClaims: TokenClaims, + idTokenSecret: string, + options?: Partial +): IdTokenEntity { + const { oid, tid } = idTokenClaims; + const homeAccountId = `${oid}.${tid}`; + const idToken = { + realm: tid || "", + environment: "login.microsoftonline.com", + credentialType: CredentialType.ID_TOKEN, + secret: idTokenSecret, + clientId: "mock_client_id", + homeAccountId: homeAccountId, + }; + + return { ...idToken, ...options }; +} diff --git a/lib/msal-browser/test/cache/TokenCache.spec.ts b/lib/msal-browser/test/cache/TokenCache.spec.ts index 4bd9b26f41..5c0e7bf8b9 100644 --- a/lib/msal-browser/test/cache/TokenCache.spec.ts +++ b/lib/msal-browser/test/cache/TokenCache.spec.ts @@ -17,6 +17,7 @@ import { RefreshTokenEntity, TokenClaims, CacheHelpers, + Authority, } from "@azure/msal-common"; import { TokenCache, LoadTokenOptions } from "../../src/cache/TokenCache"; import { CryptoOps } from "../../src/crypto/CryptoOps"; @@ -28,6 +29,7 @@ import { } from "../../src/config/Configuration"; import { BrowserCacheLocation } from "../../src/utils/BrowserConstants"; import { + ID_TOKEN_CLAIMS, TEST_CONFIG, TEST_DATA_CLIENT_INFO, TEST_TOKENS, @@ -36,6 +38,7 @@ import { } from "../utils/StringConstants"; import { BrowserAuthErrorMessage, SilentRequest } from "../../src"; import { base64Decode } from "../../src/encode/Base64Decode"; +import { buildAccountFromIdTokenClaims } from "./TestStorageManager"; describe("TokenCache tests", () => { let configuration: BrowserConfiguration; @@ -111,7 +114,7 @@ describe("TokenCache tests", () => { ); testEnvironment = "login.microsoftonline.com"; - testClientInfo = `${TEST_DATA_CLIENT_INFO.TEST_UID_ENCODED}.${TEST_DATA_CLIENT_INFO.TEST_UTID_ENCODED}`; + testClientInfo = TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO; testIdToken = TEST_TOKENS.IDTOKEN_V2; testIdTokenClaims = AuthToken.extractTokenClaims( testIdToken, @@ -161,10 +164,16 @@ describe("TokenCache tests", () => { ); refreshTokenKey = CacheHelpers.generateCredentialKey(refreshTokenEntity); + + jest.spyOn( + Authority.prototype, + "getPreferredCache" + ).mockReturnValue(testEnvironment); }); afterEach(() => { browserStorage.clear(); + jest.restoreAllMocks(); }); it("loads id token with a request account", () => { @@ -242,17 +251,11 @@ describe("TokenCache tests", () => { clientInfo: testClientInfo, }; - const testAccountInfo = { - authorityType: "MSSTS", - homeAccountId: testHomeAccountId, - environment: testEnvironment, - tenantId: TEST_CONFIG.MSAL_TENANT_ID, - username: "AbeLi@microsoft.com", - localAccountId: TEST_DATA_CLIENT_INFO.TEST_LOCAL_ACCOUNT_ID, - name: testIdTokenClaims.name, - nativeAccountId: undefined, - tenants: [TEST_CONFIG.MSAL_TENANT_ID], - }; + const testAccountInfo = buildAccountFromIdTokenClaims( + ID_TOKEN_CLAIMS, + undefined, + { environment: testEnvironment } + ).getAccountInfo(); const testAccountKey = AccountEntity.generateAccountCacheKey(testAccountInfo); const result = tokenCache.loadExternalTokens( diff --git a/lib/msal-browser/test/event/EventHandler.spec.ts b/lib/msal-browser/test/event/EventHandler.spec.ts index d16a6e5562..f6c41abba5 100644 --- a/lib/msal-browser/test/event/EventHandler.spec.ts +++ b/lib/msal-browser/test/event/EventHandler.spec.ts @@ -5,6 +5,8 @@ import sinon from "sinon"; import { EventHandler } from "../../src/event/EventHandler"; import { Logger, LogLevel, AccountInfo, AccountEntity } from "../../src"; import { CryptoOps } from "../../src/crypto/CryptoOps"; +import { buildAccountFromIdTokenClaims } from "../cache/TestStorageManager"; +import { ID_TOKEN_CLAIMS } from "../utils/StringConstants"; describe("Event API tests", () => { const loggerOptions = { @@ -126,20 +128,10 @@ describe("Event API tests", () => { const eventHandler = new EventHandler(logger, browserCrypto); eventHandler.addEventCallback(subscriber); - const account: AccountInfo = { - authorityType: "MSSTS", - homeAccountId: "test-home-accountId-1", - localAccountId: "test-local-accountId-1", - username: "user-1@example.com", - environment: "test-environment-1", - tenantId: "test-tenantId-1", - name: "name-1", - idTokenClaims: {}, - nativeAccountId: undefined, - tenants: ["test-tenantId-1"], - }; + const accountEntity: AccountEntity = + buildAccountFromIdTokenClaims(ID_TOKEN_CLAIMS); - const accountEntity = AccountEntity.createFromAccountInfo(account); + const account: AccountInfo = accountEntity.getAccountInfo(); const cacheKey1 = AccountEntity.generateAccountCacheKey(account); @@ -155,7 +147,7 @@ describe("Event API tests", () => { const subscriber = (message: EventMessage) => { expect(message.eventType).toEqual(EventType.ACCOUNT_REMOVED); expect(message.interactionType).toBeNull(); - expect(message.payload).toEqual(accountEntity.getAccountInfo()); + expect(message.payload).toEqual(account); expect(message.error).toBeNull(); expect(message.timestamp).not.toBeNull(); done(); @@ -164,20 +156,10 @@ describe("Event API tests", () => { const eventHandler = new EventHandler(logger, browserCrypto); eventHandler.addEventCallback(subscriber); - const account: AccountInfo = { - homeAccountId: "test-home-accountId-1", - localAccountId: "test-local-accountId-1", - username: "user-1@example.com", - environment: "test-environment-1", - tenantId: "test-tenantId-1", - name: "name-1", - idTokenClaims: {}, - authorityType: "MSSTS", - nativeAccountId: undefined, - tenants: ["test-tenantId-1"], - }; + const accountEntity: AccountEntity = + buildAccountFromIdTokenClaims(ID_TOKEN_CLAIMS); - const accountEntity = AccountEntity.createFromAccountInfo(account); + const account: AccountInfo = accountEntity.getAccountInfo(); const cacheKey1 = AccountEntity.generateAccountCacheKey(account); diff --git a/lib/msal-browser/test/interaction_client/NativeInteractionClient.spec.ts b/lib/msal-browser/test/interaction_client/NativeInteractionClient.spec.ts index 9d2b7c8756..68e2e5f99c 100644 --- a/lib/msal-browser/test/interaction_client/NativeInteractionClient.spec.ts +++ b/lib/msal-browser/test/interaction_client/NativeInteractionClient.spec.ts @@ -42,6 +42,10 @@ import { getDefaultPerformanceClient } from "../utils/TelemetryUtils"; import { CryptoOps } from "../../src/crypto/CryptoOps"; import { BrowserCacheManager } from "../../src/cache/BrowserCacheManager"; import { IPublicClientApplication } from "../../src"; +import { + buildAccountFromIdTokenClaims, + buildIdToken, +} from "../cache/TestStorageManager"; const networkInterface = { sendGetRequestAsync(): T { @@ -52,30 +56,35 @@ const networkInterface = { }, }; -const testAccountEntity: AccountEntity = new AccountEntity(); -testAccountEntity.homeAccountId = `${ID_TOKEN_CLAIMS.oid}.${ID_TOKEN_CLAIMS.tid}`; -testAccountEntity.localAccountId = ID_TOKEN_CLAIMS.oid; -testAccountEntity.environment = "login.microsoftonline.com"; -testAccountEntity.realm = ID_TOKEN_CLAIMS.tid; -testAccountEntity.username = ID_TOKEN_CLAIMS.preferred_username; -testAccountEntity.name = ID_TOKEN_CLAIMS.name; -testAccountEntity.authorityType = "MSSTS"; -testAccountEntity.nativeAccountId = "nativeAccountId"; -testAccountEntity.tenants = [ID_TOKEN_CLAIMS.tid]; - -const testAccountInfo: AccountInfo = { +const MOCK_WAM_RESPONSE = { + access_token: TEST_TOKENS.ACCESS_TOKEN, + id_token: TEST_TOKENS.IDTOKEN_V2, + scope: "User.Read", + expires_in: 3600, + client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, + account: { + id: "nativeAccountId", + }, + properties: {}, +}; + +const testAccountEntity: AccountEntity = buildAccountFromIdTokenClaims( + ID_TOKEN_CLAIMS, + undefined, + { + nativeAccountId: MOCK_WAM_RESPONSE.account.id, + } +); + +const TEST_ACCOUNT_INFO: AccountInfo = { ...testAccountEntity.getAccountInfo(), idTokenClaims: ID_TOKEN_CLAIMS, }; -const testIdToken: IdTokenEntity = { - homeAccountId: `${ID_TOKEN_CLAIMS.oid}.${ID_TOKEN_CLAIMS.tid}`, - clientId: TEST_CONFIG.MSAL_CLIENT_ID, - environment: testAccountEntity.environment, - realm: ID_TOKEN_CLAIMS.tid, - secret: TEST_TOKENS.IDTOKEN_V2, - credentialType: CredentialType.ID_TOKEN, -}; +const TEST_ID_TOKEN: IdTokenEntity = buildIdToken( + ID_TOKEN_CLAIMS, + TEST_TOKENS.IDTOKEN_V2 +); const testAccessTokenEntity: AccessTokenEntity = { homeAccountId: `${ID_TOKEN_CLAIMS.oid}.${ID_TOKEN_CLAIMS.tid}`, @@ -92,7 +101,7 @@ const testAccessTokenEntity: AccessTokenEntity = { const testCacheRecord: CacheRecord = { account: testAccountEntity, - idToken: testIdToken, + idToken: TEST_ID_TOKEN, accessToken: testAccessTokenEntity, refreshToken: null, appMetadata: null, @@ -212,10 +221,10 @@ describe("NativeInteractionClient Tests", () => { describe("acquireTokensFromInternalCache Tests", () => { const response: AuthenticationResult = { authority: TEST_CONFIG.validAuthority, - uniqueId: testAccountInfo.localAccountId, - tenantId: testAccountInfo.tenantId, + uniqueId: TEST_ACCOUNT_INFO.localAccountId, + tenantId: TEST_ACCOUNT_INFO.tenantId, scopes: TEST_CONFIG.DEFAULT_SCOPES, - account: testAccountInfo, + account: TEST_ACCOUNT_INFO, idToken: TEST_TOKENS.IDTOKEN_V2, accessToken: TEST_TOKENS.ACCESS_TOKEN, idTokenClaims: ID_TOKEN_CLAIMS, @@ -227,7 +236,7 @@ describe("NativeInteractionClient Tests", () => { sinon .stub(CacheManager.prototype, "getBaseAccountInfo") - .returns(testAccountInfo); + .returns(TEST_ACCOUNT_INFO); sinon .stub(CacheManager.prototype, "readCacheRecord") @@ -238,61 +247,39 @@ describe("NativeInteractionClient Tests", () => { scopes: TEST_CONFIG.DEFAULT_SCOPES, }); expect(response.accessToken).toEqual(testAccessTokenEntity.secret); - expect(response.idToken).toEqual(testIdToken.secret); + expect(response.idToken).toEqual(TEST_ID_TOKEN.secret); expect(response.uniqueId).toEqual(ID_TOKEN_CLAIMS.oid); expect(response.tenantId).toEqual(ID_TOKEN_CLAIMS.tid); expect(response.idTokenClaims).toEqual(ID_TOKEN_CLAIMS); expect(response.authority).toEqual(TEST_CONFIG.validAuthority); expect(response.scopes).toEqual(TEST_CONFIG.DEFAULT_SCOPES); expect(response.correlationId).toEqual(RANDOM_TEST_GUID); - expect(response.account).toEqual(testAccountInfo); + expect(response.account).toEqual(TEST_ACCOUNT_INFO); expect(response.tokenType).toEqual(AuthenticationScheme.BEARER); }); }); describe("acquireToken Tests", () => { it("acquires token successfully", async () => { - const mockWamResponse = { - access_token: TEST_TOKENS.ACCESS_TOKEN, - id_token: TEST_TOKENS.IDTOKEN_V2, - scope: "User.Read", - expires_in: 3600, - client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, - account: { - id: "nativeAccountId", - }, - properties: {}, - }; - - const testAccount: AccountInfo = { - authorityType: "MSSTS", - homeAccountId: `${TEST_DATA_CLIENT_INFO.TEST_UID}.${TEST_DATA_CLIENT_INFO.TEST_UTID}`, - localAccountId: ID_TOKEN_CLAIMS.oid, - environment: "login.windows.net", - tenantId: ID_TOKEN_CLAIMS.tid, - username: ID_TOKEN_CLAIMS.preferred_username, - name: ID_TOKEN_CLAIMS.name, - idTokenClaims: ID_TOKEN_CLAIMS, - nativeAccountId: mockWamResponse.account.id, - tenants: [ID_TOKEN_CLAIMS.tid], - }; sinon .stub(NativeMessageHandler.prototype, "sendMessage") .callsFake((): Promise => { - return Promise.resolve(mockWamResponse); + return Promise.resolve(MOCK_WAM_RESPONSE); }); const response = await nativeInteractionClient.acquireToken({ scopes: ["User.Read"], }); - expect(response.accessToken).toEqual(mockWamResponse.access_token); - expect(response.idToken).toEqual(mockWamResponse.id_token); + expect(response.accessToken).toEqual( + MOCK_WAM_RESPONSE.access_token + ); + expect(response.idToken).toEqual(MOCK_WAM_RESPONSE.id_token); expect(response.uniqueId).toEqual(ID_TOKEN_CLAIMS.oid); expect(response.tenantId).toEqual(ID_TOKEN_CLAIMS.tid); expect(response.idTokenClaims).toEqual(ID_TOKEN_CLAIMS); expect(response.authority).toEqual(TEST_CONFIG.validAuthority); - expect(response.scopes).toContain(mockWamResponse.scope); + expect(response.scopes).toContain(MOCK_WAM_RESPONSE.scope); expect(response.correlationId).toEqual(RANDOM_TEST_GUID); - expect(response.account).toEqual(testAccount); + expect(response.account).toEqual(TEST_ACCOUNT_INFO); expect(response.tokenType).toEqual(AuthenticationScheme.BEARER); }); @@ -331,140 +318,74 @@ describe("NativeInteractionClient Tests", () => { }); it("prompt: none succeeds", async () => { - const mockWamResponse = { - access_token: TEST_TOKENS.ACCESS_TOKEN, - id_token: TEST_TOKENS.IDTOKEN_V2, - scope: "User.Read", - expires_in: 3600, - client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, - account: { - id: "nativeAccountId", - }, - properties: {}, - }; - - const testAccount: AccountInfo = { - authorityType: "MSSTS", - homeAccountId: `${TEST_DATA_CLIENT_INFO.TEST_UID}.${TEST_DATA_CLIENT_INFO.TEST_UTID}`, - localAccountId: ID_TOKEN_CLAIMS.oid, - environment: "login.windows.net", - tenantId: ID_TOKEN_CLAIMS.tid, - username: ID_TOKEN_CLAIMS.preferred_username, - name: ID_TOKEN_CLAIMS.name, - idTokenClaims: ID_TOKEN_CLAIMS, - nativeAccountId: mockWamResponse.account.id, - tenants: [ID_TOKEN_CLAIMS.tid], - }; sinon .stub(NativeMessageHandler.prototype, "sendMessage") .callsFake((): Promise => { - return Promise.resolve(mockWamResponse); + return Promise.resolve(MOCK_WAM_RESPONSE); }); const response = await nativeInteractionClient.acquireToken({ scopes: ["User.Read"], prompt: PromptValue.NONE, }); - expect(response.accessToken).toEqual(mockWamResponse.access_token); - expect(response.idToken).toEqual(mockWamResponse.id_token); + expect(response.accessToken).toEqual( + MOCK_WAM_RESPONSE.access_token + ); + expect(response.idToken).toEqual(MOCK_WAM_RESPONSE.id_token); expect(response.uniqueId).toEqual(ID_TOKEN_CLAIMS.oid); expect(response.tenantId).toEqual(ID_TOKEN_CLAIMS.tid); expect(response.idTokenClaims).toEqual(ID_TOKEN_CLAIMS); expect(response.authority).toEqual(TEST_CONFIG.validAuthority); - expect(response.scopes).toContain(mockWamResponse.scope); + expect(response.scopes).toContain(MOCK_WAM_RESPONSE.scope); expect(response.correlationId).toEqual(RANDOM_TEST_GUID); - expect(response.account).toEqual(testAccount); + expect(response.account).toEqual(TEST_ACCOUNT_INFO); expect(response.tokenType).toEqual(AuthenticationScheme.BEARER); }); it("prompt: consent succeeds", async () => { - const mockWamResponse = { - access_token: TEST_TOKENS.ACCESS_TOKEN, - id_token: TEST_TOKENS.IDTOKEN_V2, - scope: "User.Read", - expires_in: 3600, - client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, - account: { - id: "nativeAccountId", - }, - properties: {}, - }; - - const testAccount: AccountInfo = { - authorityType: "MSSTS", - homeAccountId: `${TEST_DATA_CLIENT_INFO.TEST_UID}.${TEST_DATA_CLIENT_INFO.TEST_UTID}`, - localAccountId: ID_TOKEN_CLAIMS.oid, - environment: "login.windows.net", - tenantId: ID_TOKEN_CLAIMS.tid, - username: ID_TOKEN_CLAIMS.preferred_username, - name: ID_TOKEN_CLAIMS.name, - idTokenClaims: ID_TOKEN_CLAIMS, - nativeAccountId: mockWamResponse.account.id, - tenants: [ID_TOKEN_CLAIMS.tid], - }; sinon .stub(NativeMessageHandler.prototype, "sendMessage") .callsFake((): Promise => { - return Promise.resolve(mockWamResponse); + return Promise.resolve(MOCK_WAM_RESPONSE); }); const response = await nativeInteractionClient.acquireToken({ scopes: ["User.Read"], prompt: PromptValue.CONSENT, }); - expect(response.accessToken).toEqual(mockWamResponse.access_token); - expect(response.idToken).toEqual(mockWamResponse.id_token); + expect(response.accessToken).toEqual( + MOCK_WAM_RESPONSE.access_token + ); + expect(response.idToken).toEqual(MOCK_WAM_RESPONSE.id_token); expect(response.uniqueId).toEqual(ID_TOKEN_CLAIMS.oid); expect(response.tenantId).toEqual(ID_TOKEN_CLAIMS.tid); expect(response.idTokenClaims).toEqual(ID_TOKEN_CLAIMS); expect(response.authority).toEqual(TEST_CONFIG.validAuthority); - expect(response.scopes).toContain(mockWamResponse.scope); + expect(response.scopes).toContain(MOCK_WAM_RESPONSE.scope); expect(response.correlationId).toEqual(RANDOM_TEST_GUID); - expect(response.account).toEqual(testAccount); + expect(response.account).toEqual(TEST_ACCOUNT_INFO); expect(response.tokenType).toEqual(AuthenticationScheme.BEARER); }); it("prompt: login succeeds", async () => { - const mockWamResponse = { - access_token: TEST_TOKENS.ACCESS_TOKEN, - id_token: TEST_TOKENS.IDTOKEN_V2, - scope: "User.Read", - expires_in: 3600, - client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, - account: { - id: "nativeAccountId", - }, - properties: {}, - }; - - const testAccount: AccountInfo = { - authorityType: "MSSTS", - homeAccountId: `${TEST_DATA_CLIENT_INFO.TEST_UID}.${TEST_DATA_CLIENT_INFO.TEST_UTID}`, - localAccountId: ID_TOKEN_CLAIMS.oid, - environment: "login.windows.net", - tenantId: ID_TOKEN_CLAIMS.tid, - username: ID_TOKEN_CLAIMS.preferred_username, - name: ID_TOKEN_CLAIMS.name, - idTokenClaims: ID_TOKEN_CLAIMS, - nativeAccountId: mockWamResponse.account.id, - tenants: [ID_TOKEN_CLAIMS.tid], - }; sinon .stub(NativeMessageHandler.prototype, "sendMessage") .callsFake((): Promise => { - return Promise.resolve(mockWamResponse); + return Promise.resolve(MOCK_WAM_RESPONSE); }); const response = await nativeInteractionClient.acquireToken({ scopes: ["User.Read"], prompt: PromptValue.LOGIN, }); - expect(response.accessToken).toEqual(mockWamResponse.access_token); - expect(response.idToken).toEqual(mockWamResponse.id_token); + expect(response.accessToken).toEqual( + MOCK_WAM_RESPONSE.access_token + ); + expect(response.idToken).toEqual(MOCK_WAM_RESPONSE.id_token); expect(response.uniqueId).toEqual(ID_TOKEN_CLAIMS.oid); expect(response.tenantId).toEqual(ID_TOKEN_CLAIMS.tid); expect(response.idTokenClaims).toEqual(ID_TOKEN_CLAIMS); expect(response.authority).toEqual(TEST_CONFIG.validAuthority); - expect(response.scopes).toContain(mockWamResponse.scope); + expect(response.scopes).toContain(MOCK_WAM_RESPONSE.scope); expect(response.correlationId).toEqual(RANDOM_TEST_GUID); - expect(response.account).toEqual(testAccount); + expect(response.account).toEqual(TEST_ACCOUNT_INFO); expect(response.tokenType).toEqual(AuthenticationScheme.BEARER); }); @@ -500,37 +421,13 @@ describe("NativeInteractionClient Tests", () => { }); it("ssoSilent overwrites prompt to be 'none' and succeeds", async () => { - const mockWamResponse = { - access_token: TEST_TOKENS.ACCESS_TOKEN, - id_token: TEST_TOKENS.IDTOKEN_V2, - scope: "User.Read", - expires_in: 3600, - client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, - account: { - id: "nativeAccountId", - }, - properties: {}, - }; - - const testAccount: AccountInfo = { - authorityType: "MSSTS", - homeAccountId: `${TEST_DATA_CLIENT_INFO.TEST_UID}.${TEST_DATA_CLIENT_INFO.TEST_UTID}`, - localAccountId: ID_TOKEN_CLAIMS.oid, - environment: "login.windows.net", - tenantId: ID_TOKEN_CLAIMS.tid, - username: ID_TOKEN_CLAIMS.preferred_username, - name: ID_TOKEN_CLAIMS.name, - idTokenClaims: ID_TOKEN_CLAIMS, - nativeAccountId: mockWamResponse.account.id, - tenants: [ID_TOKEN_CLAIMS.tid], - }; sinon .stub(NativeMessageHandler.prototype, "sendMessage") .callsFake((nativeRequest): Promise => { expect( nativeRequest.request && nativeRequest.request.prompt ).toBe(PromptValue.NONE); - return Promise.resolve(mockWamResponse); + return Promise.resolve(MOCK_WAM_RESPONSE); }); // @ts-ignore const nativeInteractionClient = new NativeInteractionClient( @@ -559,50 +456,28 @@ describe("NativeInteractionClient Tests", () => { scopes: ["User.Read"], prompt: PromptValue.SELECT_ACCOUNT, }); - expect(response.accessToken).toEqual(mockWamResponse.access_token); - expect(response.idToken).toEqual(mockWamResponse.id_token); + expect(response.accessToken).toEqual( + MOCK_WAM_RESPONSE.access_token + ); + expect(response.idToken).toEqual(MOCK_WAM_RESPONSE.id_token); expect(response.uniqueId).toEqual(ID_TOKEN_CLAIMS.oid); expect(response.tenantId).toEqual(ID_TOKEN_CLAIMS.tid); expect(response.idTokenClaims).toEqual(ID_TOKEN_CLAIMS); expect(response.authority).toEqual(TEST_CONFIG.validAuthority); - expect(response.scopes).toContain(mockWamResponse.scope); + expect(response.scopes).toContain(MOCK_WAM_RESPONSE.scope); expect(response.correlationId).toEqual(RANDOM_TEST_GUID); - expect(response.account).toEqual(testAccount); + expect(response.account).toEqual(TEST_ACCOUNT_INFO); expect(response.tokenType).toEqual(AuthenticationScheme.BEARER); }); it("acquireTokenSilent overwrites prompt to be 'none' and succeeds", async () => { - const mockWamResponse = { - access_token: TEST_TOKENS.ACCESS_TOKEN, - id_token: TEST_TOKENS.IDTOKEN_V2, - scope: "User.Read", - expires_in: 3600, - client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, - account: { - id: "nativeAccountId", - }, - properties: {}, - }; - - const testAccount: AccountInfo = { - authorityType: "MSSTS", - homeAccountId: `${TEST_DATA_CLIENT_INFO.TEST_UID}.${TEST_DATA_CLIENT_INFO.TEST_UTID}`, - localAccountId: ID_TOKEN_CLAIMS.oid, - environment: "login.windows.net", - tenantId: ID_TOKEN_CLAIMS.tid, - username: ID_TOKEN_CLAIMS.preferred_username, - name: ID_TOKEN_CLAIMS.name, - idTokenClaims: ID_TOKEN_CLAIMS, - nativeAccountId: mockWamResponse.account.id, - tenants: [ID_TOKEN_CLAIMS.tid], - }; sinon .stub(NativeMessageHandler.prototype, "sendMessage") .callsFake((nativeRequest): Promise => { expect( nativeRequest.request && nativeRequest.request.prompt ).toBe(PromptValue.NONE); - return Promise.resolve(mockWamResponse); + return Promise.resolve(MOCK_WAM_RESPONSE); }); // @ts-ignore const nativeInteractionClient = new NativeInteractionClient( @@ -631,36 +506,28 @@ describe("NativeInteractionClient Tests", () => { scopes: ["User.Read"], prompt: PromptValue.SELECT_ACCOUNT, }); - expect(response.accessToken).toEqual(mockWamResponse.access_token); - expect(response.idToken).toEqual(mockWamResponse.id_token); + expect(response.accessToken).toEqual( + MOCK_WAM_RESPONSE.access_token + ); + expect(response.idToken).toEqual(MOCK_WAM_RESPONSE.id_token); expect(response.uniqueId).toEqual(ID_TOKEN_CLAIMS.oid); expect(response.tenantId).toEqual(ID_TOKEN_CLAIMS.tid); expect(response.idTokenClaims).toEqual(ID_TOKEN_CLAIMS); expect(response.authority).toEqual(TEST_CONFIG.validAuthority); - expect(response.scopes).toContain(mockWamResponse.scope); + expect(response.scopes).toContain(MOCK_WAM_RESPONSE.scope); expect(response.correlationId).toEqual(RANDOM_TEST_GUID); - expect(response.account).toEqual(testAccount); + expect(response.account).toEqual(TEST_ACCOUNT_INFO); expect(response.tokenType).toEqual(AuthenticationScheme.BEARER); }); describe("storeInCache tests", () => { - const mockWamResponse = { - access_token: TEST_TOKENS.ACCESS_TOKEN, - id_token: TEST_TOKENS.IDTOKEN_V2, - scope: "User.Read", - expires_in: 3600, - client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, - account: { - id: "nativeAccountId", - }, - properties: {}, - }; + //here beforeEach(() => { jest.spyOn( NativeMessageHandler.prototype, "sendMessage" - ).mockResolvedValue(mockWamResponse); + ).mockResolvedValue(MOCK_WAM_RESPONSE); }); it("does not store idToken if storeInCache.idToken = false", async () => { @@ -671,9 +538,9 @@ describe("NativeInteractionClient Tests", () => { }, }); expect(response.accessToken).toEqual( - mockWamResponse.access_token + MOCK_WAM_RESPONSE.access_token ); - expect(response.idToken).toEqual(mockWamResponse.id_token); + expect(response.idToken).toEqual(MOCK_WAM_RESPONSE.id_token); // Browser Storage should not contain tokens const tokenKeys = browserCacheManager.getTokenKeys(); @@ -696,9 +563,9 @@ describe("NativeInteractionClient Tests", () => { }, }); expect(response.accessToken).toEqual( - mockWamResponse.access_token + MOCK_WAM_RESPONSE.access_token ); - expect(response.idToken).toEqual(mockWamResponse.id_token); + expect(response.idToken).toEqual(MOCK_WAM_RESPONSE.id_token); // Cache should not contain tokens which were turned off const tokenKeys = browserCacheManager.getTokenKeys(); @@ -721,9 +588,9 @@ describe("NativeInteractionClient Tests", () => { }, }); expect(response.accessToken).toEqual( - mockWamResponse.access_token + MOCK_WAM_RESPONSE.access_token ); - expect(response.idToken).toEqual(mockWamResponse.id_token); + expect(response.idToken).toEqual(MOCK_WAM_RESPONSE.id_token); // Browser Storage should not contain tokens const tokenKeys = browserCacheManager.getTokenKeys(); @@ -742,17 +609,7 @@ describe("NativeInteractionClient Tests", () => { describe("acquireTokenRedirect tests", () => { it("acquires token successfully then redirects to start page", (done) => { - const mockWamResponse = { - access_token: TEST_TOKENS.ACCESS_TOKEN, - id_token: TEST_TOKENS.IDTOKEN_V2, - scope: "User.Read", - expires_in: 3600, - client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, - account: { - id: "nativeAccountId", - }, - properties: {}, - }; + //here sinon .stub(NavigationClient.prototype, "navigateExternal") @@ -764,7 +621,7 @@ describe("NativeInteractionClient Tests", () => { sinon .stub(NativeMessageHandler.prototype, "sendMessage") .callsFake((): Promise => { - return Promise.resolve(mockWamResponse); + return Promise.resolve(MOCK_WAM_RESPONSE); }); nativeInteractionClient.acquireTokenRedirect({ scopes: ["User.Read"], @@ -793,31 +650,6 @@ describe("NativeInteractionClient Tests", () => { describe("handleRedirectPromise tests", () => { it("successfully returns response from native broker", async () => { - const mockWamResponse = { - access_token: TEST_TOKENS.ACCESS_TOKEN, - id_token: TEST_TOKENS.IDTOKEN_V2, - scope: "User.Read", - expires_in: 3600, - client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, - account: { - id: "nativeAccountId", - }, - properties: {}, - }; - - const testAccount: AccountInfo = { - authorityType: "MSSTS", - homeAccountId: `${TEST_DATA_CLIENT_INFO.TEST_UID}.${TEST_DATA_CLIENT_INFO.TEST_UTID}`, - localAccountId: ID_TOKEN_CLAIMS.oid, - environment: "login.windows.net", - tenantId: ID_TOKEN_CLAIMS.tid, - username: ID_TOKEN_CLAIMS.preferred_username, - name: ID_TOKEN_CLAIMS.name, - idTokenClaims: ID_TOKEN_CLAIMS, - nativeAccountId: mockWamResponse.account.id, - tenants: [ID_TOKEN_CLAIMS.tid], - }; - sinon .stub(NavigationClient.prototype, "navigateExternal") .callsFake((url: string) => { @@ -827,7 +659,7 @@ describe("NativeInteractionClient Tests", () => { sinon .stub(NativeMessageHandler.prototype, "sendMessage") .callsFake((): Promise => { - return Promise.resolve(mockWamResponse); + return Promise.resolve(MOCK_WAM_RESPONSE); }); // @ts-ignore pca.browserStorage.setInteractionInProgress(true); @@ -840,17 +672,17 @@ describe("NativeInteractionClient Tests", () => { const testTokenResponse: AuthenticationResult = { authority: TEST_CONFIG.validAuthority, - uniqueId: testAccount.localAccountId, - tenantId: testAccount.tenantId, - scopes: mockWamResponse.scope.split(" "), - idToken: mockWamResponse.id_token, + uniqueId: TEST_ACCOUNT_INFO.localAccountId, + tenantId: TEST_ACCOUNT_INFO.tenantId, + scopes: MOCK_WAM_RESPONSE.scope.split(" "), + idToken: MOCK_WAM_RESPONSE.id_token, idTokenClaims: ID_TOKEN_CLAIMS, - accessToken: mockWamResponse.access_token, + accessToken: MOCK_WAM_RESPONSE.access_token, fromCache: false, state: undefined, correlationId: RANDOM_TEST_GUID, expiresOn: response && response.expiresOn, // Steal the expires on from the response as this is variable - account: testAccount, + account: TEST_ACCOUNT_INFO, tokenType: AuthenticationScheme.BEARER, fromNativeBroker: true, }; @@ -859,31 +691,6 @@ describe("NativeInteractionClient Tests", () => { it("If request includes a prompt value it is ignored on the 2nd call to native broker", async () => { // The user should not be prompted twice, prompt value should only be used on the first call to the native broker (before returning to the redirect uri). Native broker calls from handleRedirectPromise should ignore the prompt. - const mockWamResponse = { - access_token: TEST_TOKENS.ACCESS_TOKEN, - id_token: TEST_TOKENS.IDTOKEN_V2, - scope: "User.Read", - expires_in: 3600, - client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, - account: { - id: "nativeAccountId", - }, - properties: {}, - }; - - const testAccount: AccountInfo = { - authorityType: "MSSTS", - homeAccountId: `${TEST_DATA_CLIENT_INFO.TEST_UID}.${TEST_DATA_CLIENT_INFO.TEST_UTID}`, - localAccountId: ID_TOKEN_CLAIMS.oid, - environment: "login.windows.net", - tenantId: ID_TOKEN_CLAIMS.tid, - username: ID_TOKEN_CLAIMS.preferred_username, - name: ID_TOKEN_CLAIMS.name, - idTokenClaims: ID_TOKEN_CLAIMS, - nativeAccountId: mockWamResponse.account.id, - tenants: [ID_TOKEN_CLAIMS.tid], - }; - sinon .stub(NavigationClient.prototype, "navigateExternal") .callsFake((url: string) => { @@ -899,7 +706,7 @@ describe("NativeInteractionClient Tests", () => { expect( messageBody.request && messageBody.request.prompt ).toBe(undefined); - return Promise.resolve(mockWamResponse); + return Promise.resolve(MOCK_WAM_RESPONSE); } ); // @ts-ignore @@ -914,17 +721,17 @@ describe("NativeInteractionClient Tests", () => { const testTokenResponse: AuthenticationResult = { authority: TEST_CONFIG.validAuthority, - uniqueId: testAccount.localAccountId, - tenantId: testAccount.tenantId, - scopes: mockWamResponse.scope.split(" "), - idToken: mockWamResponse.id_token, + uniqueId: TEST_ACCOUNT_INFO.localAccountId, + tenantId: TEST_ACCOUNT_INFO.tenantId, + scopes: MOCK_WAM_RESPONSE.scope.split(" "), + idToken: MOCK_WAM_RESPONSE.id_token, idTokenClaims: ID_TOKEN_CLAIMS, - accessToken: mockWamResponse.access_token, + accessToken: MOCK_WAM_RESPONSE.access_token, fromCache: false, state: undefined, correlationId: RANDOM_TEST_GUID, expiresOn: response && response.expiresOn, // Steal the expires on from the response as this is variable - account: testAccount, + account: TEST_ACCOUNT_INFO, tokenType: AuthenticationScheme.BEARER, fromNativeBroker: true, }; @@ -932,17 +739,7 @@ describe("NativeInteractionClient Tests", () => { }); it("clears interaction in progress if native broker call fails", (done) => { - const mockWamResponse = { - access_token: TEST_TOKENS.ACCESS_TOKEN, - id_token: TEST_TOKENS.IDTOKEN_V2, - scope: "User.Read", - expires_in: 3600, - client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, - account: { - id: "nativeAccountId", - }, - properties: {}, - }; + //here sinon .stub(NavigationClient.prototype, "navigateExternal") @@ -956,7 +753,7 @@ describe("NativeInteractionClient Tests", () => { .callsFake((): Promise => { if (firstTime) { firstTime = false; - return Promise.resolve(mockWamResponse); // The acquireTokenRedirect call should succeed + return Promise.resolve(MOCK_WAM_RESPONSE); // The acquireTokenRedirect call should succeed } return Promise.reject( new NativeAuthError( @@ -988,17 +785,7 @@ describe("NativeInteractionClient Tests", () => { }); it("returns null if interaction is not in progress", async () => { - const mockWamResponse = { - access_token: TEST_TOKENS.ACCESS_TOKEN, - id_token: TEST_TOKENS.IDTOKEN_V2, - scope: "User.Read", - expires_in: 3600, - client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, - account: { - id: "nativeAccountId", - }, - properties: {}, - }; + //here sinon .stub(NavigationClient.prototype, "navigateExternal") @@ -1009,7 +796,7 @@ describe("NativeInteractionClient Tests", () => { sinon .stub(NativeMessageHandler.prototype, "sendMessage") .callsFake((): Promise => { - return Promise.resolve(mockWamResponse); + return Promise.resolve(MOCK_WAM_RESPONSE); }); await nativeInteractionClient.acquireTokenRedirect({ scopes: ["User.Read"], diff --git a/lib/msal-browser/test/interaction_client/RedirectClient.spec.ts b/lib/msal-browser/test/interaction_client/RedirectClient.spec.ts index abd23c7e17..9dab45e02b 100644 --- a/lib/msal-browser/test/interaction_client/RedirectClient.spec.ts +++ b/lib/msal-browser/test/interaction_client/RedirectClient.spec.ts @@ -80,6 +80,10 @@ import { NativeInteractionClient } from "../../src/interaction_client/NativeInte import { NativeMessageHandler } from "../../src/broker/nativeBroker/NativeMessageHandler"; import { getDefaultPerformanceClient } from "../utils/TelemetryUtils"; import { AuthenticationResult } from "../../src/response/AuthenticationResult"; +import { + buildAccountFromIdTokenClaims, + buildIdToken, +} from "../cache/TestStorageManager"; const cacheConfig = { cacheLocation: BrowserCacheLocation.SessionStorage, @@ -348,30 +352,17 @@ describe("RedirectClient", () => { client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, }, }; - const testIdTokenClaims: TokenClaims = { - ver: "2.0", - iss: "https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0", - sub: "AAAAAAAAAAAAAAAAAAAAAIkzqFVrSaSaFHy782bbtaQ", - name: "Abe Lincoln", - preferred_username: "AbeLi@microsoft.com", - oid: "00000000-0000-0000-66f3-3332eca7ea81", - tid: "3338040d-6c67-4c5b-b112-36a304b66dad", - nonce: "123523", - }; - const testAccount: AccountInfo = { - homeAccountId: TEST_DATA_CLIENT_INFO.TEST_HOME_ACCOUNT_ID, - localAccountId: TEST_DATA_CLIENT_INFO.TEST_UID, - environment: "login.windows.net", - tenantId: testIdTokenClaims.tid || "", - username: testIdTokenClaims.preferred_username || "", - }; + + const testAccount: AccountInfo = + buildAccountFromIdTokenClaims(ID_TOKEN_CLAIMS).getAccountInfo(); + const testTokenResponse: AuthenticationResult = { authority: TEST_CONFIG.validAuthority, - uniqueId: testIdTokenClaims.oid || "", - tenantId: testIdTokenClaims.tid || "", + uniqueId: ID_TOKEN_CLAIMS.oid, + tenantId: ID_TOKEN_CLAIMS.tid, scopes: TEST_CONFIG.DEFAULT_SCOPES, idToken: testServerTokenResponse.body.id_token, - idTokenClaims: testIdTokenClaims, + idTokenClaims: ID_TOKEN_CLAIMS, accessToken: testServerTokenResponse.body.access_token, fromCache: false, correlationId: RANDOM_TEST_GUID, @@ -381,6 +372,7 @@ describe("RedirectClient", () => { account: testAccount, tokenType: AuthenticationScheme.BEARER, }; + sinon .stub(FetchClient.prototype, "sendGetRequestAsync") .callsFake((url): any => { @@ -516,31 +508,20 @@ describe("RedirectClient", () => { client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, }, }; - const testIdTokenClaims: TokenClaims = { - ver: "2.0", - iss: "https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0", - sub: "AAAAAAAAAAAAAAAAAAAAAIkzqFVrSaSaFHy782bbtaQ", - name: "Abe Lincoln", - preferred_username: "AbeLi@microsoft.com", - oid: "00000000-0000-0000-66f3-3332eca7ea81", - tid: "3338040d-6c67-4c5b-b112-36a304b66dad", - nonce: "123523", - }; - const testAccount: AccountInfo = { - homeAccountId: TEST_DATA_CLIENT_INFO.TEST_HOME_ACCOUNT_ID, - localAccountId: TEST_DATA_CLIENT_INFO.TEST_UID, - environment: "login.windows.net", - tenantId: testIdTokenClaims.tid || "", - username: testIdTokenClaims.preferred_username || "", - nativeAccountId: "test-nativeAccountId", - }; + + const testAccount: AccountInfo = buildAccountFromIdTokenClaims( + ID_TOKEN_CLAIMS, + undefined, + { nativeAccountId: "test-nativeAccountId" } + ).getAccountInfo(); + const testTokenResponse: AuthenticationResult = { authority: TEST_CONFIG.validAuthority, - uniqueId: testIdTokenClaims.oid || "", - tenantId: testIdTokenClaims.tid || "", + uniqueId: ID_TOKEN_CLAIMS.oid, + tenantId: ID_TOKEN_CLAIMS.tid, scopes: TEST_CONFIG.DEFAULT_SCOPES, idToken: testServerTokenResponse.body.id_token, - idTokenClaims: testIdTokenClaims, + idTokenClaims: ID_TOKEN_CLAIMS, accessToken: testServerTokenResponse.body.access_token, fromCache: false, correlationId: RANDOM_TEST_GUID, @@ -550,6 +531,7 @@ describe("RedirectClient", () => { account: testAccount, tokenType: AuthenticationScheme.BEARER, }; + sinon .stub(FetchClient.prototype, "sendGetRequestAsync") .callsFake((url): any => { @@ -836,32 +818,16 @@ describe("RedirectClient", () => { }, }; - const testIdTokenClaims: TokenClaims = { - ver: "2.0", - iss: "https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0", - sub: "AAAAAAAAAAAAAAAAAAAAAIkzqFVrSaSaFHy782bbtaQ", - name: "Abe Lincoln", - preferred_username: "AbeLi@microsoft.com", - oid: "00000000-0000-0000-66f3-3332eca7ea81", - tid: "3338040d-6c67-4c5b-b112-36a304b66dad", - nonce: "123523", - }; - - const testAccount: AccountInfo = { - homeAccountId: TEST_DATA_CLIENT_INFO.TEST_HOME_ACCOUNT_ID, - localAccountId: TEST_DATA_CLIENT_INFO.TEST_UID, - environment: "login.windows.net", - tenantId: testIdTokenClaims.tid || "", - username: testIdTokenClaims.preferred_username || "", - }; + const testAccount: AccountInfo = + buildAccountFromIdTokenClaims(ID_TOKEN_CLAIMS).getAccountInfo(); const testTokenResponse: AuthenticationResult = { authority: TEST_CONFIG.validAuthority, - uniqueId: testIdTokenClaims.oid || "", - tenantId: testIdTokenClaims.tid || "", + uniqueId: ID_TOKEN_CLAIMS.oid, + tenantId: ID_TOKEN_CLAIMS.tid, scopes: TEST_CONFIG.DEFAULT_SCOPES, idToken: testServerTokenResponse.body.id_token, - idTokenClaims: testIdTokenClaims, + idTokenClaims: ID_TOKEN_CLAIMS, accessToken: testServerTokenResponse.body.access_token, fromCache: false, correlationId: RANDOM_TEST_GUID, @@ -999,32 +965,16 @@ describe("RedirectClient", () => { }, }; - const testIdTokenClaims: TokenClaims = { - ver: "2.0", - iss: "https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0", - sub: "AAAAAAAAAAAAAAAAAAAAAIkzqFVrSaSaFHy782bbtaQ", - name: "Abe Lincoln", - preferred_username: "AbeLi@microsoft.com", - oid: "00000000-0000-0000-66f3-3332eca7ea81", - tid: "3338040d-6c67-4c5b-b112-36a304b66dad", - nonce: "123523", - }; - - const testAccount: AccountInfo = { - homeAccountId: TEST_DATA_CLIENT_INFO.TEST_HOME_ACCOUNT_ID, - localAccountId: TEST_DATA_CLIENT_INFO.TEST_UID, - environment: "login.windows.net", - tenantId: testIdTokenClaims.tid!, - username: testIdTokenClaims.preferred_username!, - }; + const testAccount: AccountInfo = + buildAccountFromIdTokenClaims(ID_TOKEN_CLAIMS).getAccountInfo(); const testTokenResponse: AuthenticationResult = { authority: TEST_CONFIG.validAuthority, - uniqueId: testIdTokenClaims.oid!, - tenantId: testIdTokenClaims.tid!, + uniqueId: ID_TOKEN_CLAIMS.oid, + tenantId: ID_TOKEN_CLAIMS.tid, scopes: TEST_CONFIG.DEFAULT_SCOPES, idToken: testServerTokenResponse.body.id_token!, - idTokenClaims: testIdTokenClaims, + idTokenClaims: ID_TOKEN_CLAIMS, accessToken: testServerTokenResponse.body.access_token!, fromCache: false, correlationId: RANDOM_TEST_GUID, @@ -1177,32 +1127,16 @@ describe("RedirectClient", () => { }, }; - const testIdTokenClaims: TokenClaims = { - ver: "2.0", - iss: "https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0", - sub: "AAAAAAAAAAAAAAAAAAAAAIkzqFVrSaSaFHy782bbtaQ", - name: "Abe Lincoln", - preferred_username: "AbeLi@microsoft.com", - oid: "00000000-0000-0000-66f3-3332eca7ea81", - tid: "3338040d-6c67-4c5b-b112-36a304b66dad", - nonce: "123523", - }; - - const testAccount: AccountInfo = { - homeAccountId: TEST_DATA_CLIENT_INFO.TEST_HOME_ACCOUNT_ID, - localAccountId: TEST_DATA_CLIENT_INFO.TEST_UID, - environment: "login.windows.net", - tenantId: testIdTokenClaims.tid || "", - username: testIdTokenClaims.preferred_username || "", - }; + const testAccount: AccountInfo = + buildAccountFromIdTokenClaims(ID_TOKEN_CLAIMS).getAccountInfo(); const testTokenResponse: AuthenticationResult = { authority: TEST_CONFIG.validAuthority, - uniqueId: testIdTokenClaims.oid || "", - tenantId: testIdTokenClaims.tid || "", + uniqueId: ID_TOKEN_CLAIMS.oid, + tenantId: ID_TOKEN_CLAIMS.tid, scopes: TEST_CONFIG.DEFAULT_SCOPES, idToken: testServerTokenResponse.body.id_token, - idTokenClaims: testIdTokenClaims, + idTokenClaims: ID_TOKEN_CLAIMS, accessToken: testServerTokenResponse.body.access_token, fromCache: false, correlationId: RANDOM_TEST_GUID, @@ -1955,18 +1889,8 @@ describe("RedirectClient", () => { }); it("Adds login_hint as CCS cache entry to the cache and urlNavigate", async () => { - const testIdTokenClaims: TokenClaims = { - ver: "2.0", - iss: "https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0", - sub: "AAAAAAAAAAAAAAAAAAAAAIkzqFVrSaSaFHy782bbtaQ", - name: "Abe Lincoln", - preferred_username: "AbeLi@microsoft.com", - oid: "00000000-0000-0000-66f3-3332eca7ea81", - tid: "3338040d-6c67-4c5b-b112-36a304b66dad", - nonce: "123523", - }; const testCcsCred: CcsCredential = { - credential: testIdTokenClaims.preferred_username || "", + credential: ID_TOKEN_CLAIMS.preferred_username || "", type: CcsCredentialType.UPN, }; const emptyRequest: CommonAuthorizationUrlRequest = { @@ -1979,7 +1903,7 @@ describe("RedirectClient", () => { nonce: "", authenticationScheme: TEST_CONFIG.TOKEN_TYPE_BEARER as AuthenticationScheme, - loginHint: testIdTokenClaims.preferred_username || "", + loginHint: ID_TOKEN_CLAIMS.preferred_username || "", }; jest.spyOn(PkceGenerator, "generatePkceCodes").mockResolvedValue({ @@ -2039,23 +1963,8 @@ describe("RedirectClient", () => { }); it("Adds account homeAccountId as CCS cache entry to the cache and urlNavigate", async () => { - const testIdTokenClaims: TokenClaims = { - ver: "2.0", - iss: "https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0", - sub: "AAAAAAAAAAAAAAAAAAAAAIkzqFVrSaSaFHy782bbtaQ", - name: "Abe Lincoln", - preferred_username: "AbeLi@microsoft.com", - oid: "00000000-0000-0000-66f3-3332eca7ea81", - tid: "3338040d-6c67-4c5b-b112-36a304b66dad", - nonce: "123523", - }; - const testAccount: AccountInfo = { - homeAccountId: TEST_DATA_CLIENT_INFO.TEST_HOME_ACCOUNT_ID, - localAccountId: TEST_DATA_CLIENT_INFO.TEST_UID, - environment: "login.windows.net", - tenantId: testIdTokenClaims.tid || "", - username: testIdTokenClaims.preferred_username || "", - }; + const testAccount: AccountInfo = + buildAccountFromIdTokenClaims(ID_TOKEN_CLAIMS).getAccountInfo(); const testCcsCred: CcsCredential = { credential: testAccount.homeAccountId, type: CcsCredentialType.HOME_ACCOUNT_ID, @@ -3670,32 +3579,18 @@ describe("RedirectClient", () => { }); it("clears active account entry from the cache", async () => { + const testAccountEntity = + buildAccountFromIdTokenClaims(ID_TOKEN_CLAIMS); const testAccountInfo: AccountInfo = { - authorityType: "MSSTS", - homeAccountId: `${ID_TOKEN_CLAIMS.oid}.${ID_TOKEN_CLAIMS.tid}`, - localAccountId: ID_TOKEN_CLAIMS.oid, - environment: "login.windows.net", - tenantId: ID_TOKEN_CLAIMS.tid, - username: ID_TOKEN_CLAIMS.preferred_username, + ...testAccountEntity.getAccountInfo(), idTokenClaims: ID_TOKEN_CLAIMS, - name: ID_TOKEN_CLAIMS.name, - nativeAccountId: undefined, - tenants: [ID_TOKEN_CLAIMS.tid], }; - const testAccount: AccountEntity = - AccountEntity.createFromAccountInfo(testAccountInfo); - testAccount.clientInfo = - TEST_DATA_CLIENT_INFO.TEST_CLIENT_INFO_B64ENCODED; - - const testIdToken: IdTokenEntity = { - homeAccountId: `${ID_TOKEN_CLAIMS.oid}.${ID_TOKEN_CLAIMS.tid}`, - clientId: TEST_CONFIG.MSAL_CLIENT_ID, - environment: testAccount.environment, - realm: ID_TOKEN_CLAIMS.tid, - secret: TEST_TOKENS.IDTOKEN_V2, - credentialType: CredentialType.ID_TOKEN, - }; + const testIdToken: IdTokenEntity = buildIdToken( + ID_TOKEN_CLAIMS, + TEST_TOKENS.IDTOKEN_V2, + { clientId: TEST_CONFIG.MSAL_CLIENT_ID } + ); const validatedLogoutRequest: CommonEndSessionRequest = { correlationId: RANDOM_TEST_GUID, @@ -3714,7 +3609,7 @@ describe("RedirectClient", () => { } ); - browserStorage.setAccount(testAccount); + browserStorage.setAccount(testAccountEntity); browserStorage.setIdTokenCredential(testIdToken); pca.setActiveAccount(testAccountInfo); diff --git a/lib/msal-browser/test/interaction_client/SilentCacheClient.spec.ts b/lib/msal-browser/test/interaction_client/SilentCacheClient.spec.ts index dd532f064b..2219f1cef7 100644 --- a/lib/msal-browser/test/interaction_client/SilentCacheClient.spec.ts +++ b/lib/msal-browser/test/interaction_client/SilentCacheClient.spec.ts @@ -24,31 +24,29 @@ import { AuthenticationResult, AccountInfo, } from "@azure/msal-common"; +import { + buildAccountFromIdTokenClaims, + buildIdToken, +} from "../cache/TestStorageManager"; +const testAccountEntity: AccountEntity = buildAccountFromIdTokenClaims( + ID_TOKEN_CLAIMS, + undefined, + { environment: "login.microsoftonline.com" } +); const testAccount: AccountInfo = { - homeAccountId: `${ID_TOKEN_CLAIMS.oid}.${ID_TOKEN_CLAIMS.tid}`, - environment: "login.microsoftonline.com", - tenantId: ID_TOKEN_CLAIMS.tid, - username: ID_TOKEN_CLAIMS.preferred_username, - localAccountId: ID_TOKEN_CLAIMS.oid, + ...testAccountEntity.getAccountInfo(), idTokenClaims: ID_TOKEN_CLAIMS, - name: ID_TOKEN_CLAIMS.name, - authorityType: "MSSTS", - nativeAccountId: undefined, - tenants: [ID_TOKEN_CLAIMS.tid], }; -const testAccountEntity: AccountEntity = - AccountEntity.createFromAccountInfo(testAccount); - -const testIdToken: IdTokenEntity = { - homeAccountId: `${ID_TOKEN_CLAIMS.oid}.${ID_TOKEN_CLAIMS.tid}`, - clientId: TEST_CONFIG.MSAL_CLIENT_ID, - environment: testAccountEntity.environment, - realm: ID_TOKEN_CLAIMS.tid, - secret: TEST_TOKENS.IDTOKEN_V2, - credentialType: CredentialType.ID_TOKEN, -}; +const testIdToken: IdTokenEntity = buildIdToken( + ID_TOKEN_CLAIMS, + TEST_TOKENS.IDTOKEN_V2, + { + clientId: TEST_CONFIG.MSAL_CLIENT_ID, + environment: testAccount.environment, + } +); const testAccessTokenEntity: AccessTokenEntity = { homeAccountId: `${ID_TOKEN_CLAIMS.oid}.${ID_TOKEN_CLAIMS.tid}`, @@ -161,10 +159,7 @@ describe("SilentCacheClient", () => { pca.browserStorage.setIdTokenCredential(testIdToken); pca.setActiveAccount(testAccount); - expect(pca.getActiveAccount()).toEqual({ - ...testAccount, - idTokenClaims: ID_TOKEN_CLAIMS, - }); + expect(pca.getActiveAccount()).toEqual(testAccount); silentCacheClient.logout({ account: testAccount }); //@ts-ignore expect(pca.getActiveAccount()).toEqual(null); diff --git a/lib/msal-browser/test/utils/StringConstants.ts b/lib/msal-browser/test/utils/StringConstants.ts index f0975a28d8..b5540c7f44 100644 --- a/lib/msal-browser/test/utils/StringConstants.ts +++ b/lib/msal-browser/test/utils/StringConstants.ts @@ -77,10 +77,11 @@ export const TEST_TOKENS = { IDTOKEN_V1: "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6IjdfWnVmMXR2a3dMeFlhSFMzcTZsVWpVWUlHdyIsImtpZCI6IjdfWnVmMXR2a3dMeFlhSFMzcTZsVWpVWUlHdyJ9.ewogICJhdWQiOiAiYjE0YTc1MDUtOTZlOS00OTI3LTkxZTgtMDYwMWQwZmM5Y2FhIiwKICAiaXNzIjogImh0dHBzOi8vc3RzLndpbmRvd3MubmV0L2ZhMTVkNjkyLWU5YzctNDQ2MC1hNzQzLTI5ZjI5NTZmZDQyOS8iLAogICJpYXQiOiAxNTM2Mjc1MTI0LAogICJuYmYiOiAxNTM2Mjc1MTI0LAogICJleHAiOiAxNTM2Mjc5MDI0LAogICJhaW8iOiAiQVhRQWkvOElBQUFBcXhzdUIrUjREMnJGUXFPRVRPNFlkWGJMRDlrWjh4ZlhhZGVBTTBRMk5rTlQ1aXpmZzN1d2JXU1hodVNTajZVVDVoeTJENldxQXBCNWpLQTZaZ1o5ay9TVTI3dVY5Y2V0WGZMT3RwTnR0Z2s1RGNCdGsrTExzdHovSmcrZ1lSbXY5YlVVNFhscGhUYzZDODZKbWoxRkN3PT0iLAogICJhbXIiOiBbCiAgICAicnNhIgogIF0sCiAgImVtYWlsIjogImFiZWxpQG1pY3Jvc29mdC5jb20iLAogICJmYW1pbHlfbmFtZSI6ICJMaW5jb2xuIiwKICAiZ2l2ZW5fbmFtZSI6ICJBYmUiLAogICJpZHAiOiAiaHR0cHM6Ly9zdHMud2luZG93cy5uZXQvNzJmOTg4YmYtODZmMS00MWFmLTkxYWItMmQ3Y2QwMTFkYjQ3LyIsCiAgImlwYWRkciI6ICIxMzEuMTA3LjIyMi4yMiIsCiAgIm5hbWUiOiAiYWJlbGkiLAogICJub25jZSI6ICIxMjM1MjMiLAogICJvaWQiOiAiMDU4MzNiNmItYWExZC00MmQ0LTllYzAtMWIyYmI5MTk0NDM4IiwKICAicmgiOiAiSSIsCiAgInN1YiI6ICI1X0o5clNzczgtanZ0X0ljdTZ1ZVJOTDh4WGI4TEY0RnNnX0tvb0MyUkpRIiwKICAidGlkIjogImZhMTVkNjkyLWU5YzctNDQ2MC1hNzQzLTI5ZjI5NTZmZDQyOSIsCiAgInVuaXF1ZV9uYW1lIjogIkFiZUxpQG1pY3Jvc29mdC5jb20iLAogICJ1dGkiOiAiTHhlXzQ2R3FUa09wR1NmVGxuNEVBQSIsCiAgInZlciI6ICIxLjAiLAogICJ1cG4iOiAiQWJlTGluY29sbkBjb250b3NvLmNvbSIKfQ==.UJQrCA6qn2bXq57qzGX_-D3HcPHqBMOKDPx4su1yKRLNErVD8xkxJLNLVRdASHqEcpyDctbdHccu6DPpkq5f0ibcaQFhejQNcABidJCTz0Bb2AbdUCTqAzdt9pdgQvMBnVH1xk3SCM6d4BbT4BkLLj10ZLasX7vRknaSjE_C5DI7Fg4WrZPwOhII1dB0HEZ_qpNaYXEiy-o94UJ94zCr07GgrqMsfYQqFR7kn-mn68AjvLcgwSfZvyR_yIK75S_K37vC3QryQ7cNoafDe9upql_6pB2ybMVlgWPs_DmbJ8g0om-sPlwyn74Cc1tW3ze-Xptw_2uVdPgWyqfuWAfq6Q", IDTOKEN_V2: - "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IjFMVE16YWtpaGlSbGFfOHoyQkVKVlhlV01xbyJ9.eyJ2ZXIiOiIyLjAiLCJpc3MiOiJodHRwczovL2xvZ2luLm1pY3Jvc29mdG9ubGluZS5jb20vOTE4ODA0MGQtNmM2Ny00YzViLWIxMTItMzZhMzA0YjY2ZGFkL3YyLjAiLCJzdWIiOiJBQUFBQUFBQUFBQUFBQUFBQUFBQUFJa3pxRlZyU2FTYUZIeTc4MmJidGFRIiwiYXVkIjoiNmNiMDQwMTgtYTNmNS00NmE3LWI5OTUtOTQwYzc4ZjVhZWYzIiwiZXhwIjoxNTM2MzYxNDExLCJpYXQiOjE1MzYyNzQ3MTEsIm5iZiI6MTUzNjI3NDcxMSwibmFtZSI6IkFiZSBMaW5jb2xuIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiQWJlTGlAbWljcm9zb2Z0LmNvbSIsIm9pZCI6IjAwMDAwMDAwLTAwMDAtMDAwMC02NmYzLTMzMzJlY2E3ZWE4MSIsInRpZCI6IjMzMzgwNDBkLTZjNjctNGM1Yi1iMTEyLTM2YTMwNGI2NmRhZCIsIm5vbmNlIjoiMTIzNTIzIiwiYWlvIjoiRGYyVVZYTDFpeCFsTUNXTVNPSkJjRmF0emNHZnZGR2hqS3Y4cTVnMHg3MzJkUjVNQjVCaXN2R1FPN1lXQnlqZDhpUURMcSFlR2JJRGFreXA1bW5PcmNkcUhlWVNubHRlcFFtUnA2QUlaOGpZIn0=.1AFWW-Ck5nROwSlltm7GzZvDwUkqvhSQpm55TQsmVo9Y59cLhRXpvB8n-55HCr9Z6G_31_UbeUkoz612I2j_Sm9FFShSDDjoaLQr54CreGIJvjtmS3EkK9a7SJBbcpL1MpUtlfygow39tFjY7EVNW9plWUvRrTgVk7lYLprvfzw-CIqw3gHC-T7IK_m_xkr08INERBtaecwhTeN4chPC4W3jdmw_lIxzC48YoQ0dB1L9-ImX98Egypfrlbm0IBL5spFzL6JDZIRRJOu8vecJvj1mq-IUhGt0MacxX8jdxYLP-KUu2d9MbNKpCKJuZ7p8gwTL5B7NlUdh_dmSviPWrw", - // [SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Fake credential used for testing purposes")] + "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImNlNTIyNzBmNDYyNDNkOWRmMmE5ODBiNGNmNmJhNDA3In0.eyJ2ZXIiOiIyLjAiLCJpc3MiOiJodHRwczovL2xvZ2luLm1pY3Jvc29mdG9ubGluZS5jb20vMzMzODA0MGQtNmM2Ny00YzViLWIxMTItMzZhMzA0YjY2ZGFkL3YyLjAiLCJzdWIiOiJBQUFBQUFBQUFBQUFBQUFBQUFBQUFJa3pxRlZyU2FTYUZIeTc4MmJidGFRIiwiYXVkIjoiNmNiMDQwMTgtYTNmNS00NmE3LWI5OTUtOTQwYzc4ZjVhZWYzIiwiZXhwIjoxNTM2MzYxNDExLCJpYXQiOjE1MzYyNzQ3MTEsIm5iZiI6MTUzNjI3NDcxMSwibmFtZSI6IkFiZSBMaW5jb2xuIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiQWJlTGlAbWljcm9zb2Z0LmNvbSIsImxvZ2luX2hpbnQiOiJBYmVMaUxvZ2luSGludCIsInVwbiI6IkFiZUxpVXBuIiwic2lkIjoiQWJlTGlTaWQiLCJvaWQiOiIwMDAwMDAwMC0wMDAwLTAwMDAtNjZmMy0zMzMyZWNhN2VhODEiLCJ0aWQiOiIzMzM4MDQwZC02YzY3LTRjNWItYjExMi0zNmEzMDRiNjZkYWQiLCJub25jZSI6IjEyMzUyMyIsImFpbyI6IkRmMlVWWEwxaXghbE1DV01TT0pCY0ZhdHpjR2Z2Rkdoakt2OHE1ZzB4NzMyZFI1TUI1QmlzdkdRTzdZV0J5amQ4aVFETHEhZUdiSURha3lwNW1uT3JjZHFIZVlTbmx0ZXBRbVJwNkFJWjhqWSJ9.bHjd-6nlislN839OwQTMXdgt3Q36mzOD5XVRgFr1AHh9MKSQe_HxT_J3P5FRuWsKIADVQm4JhLyMKKLtbFATIwkB-orv10Cs4-3F5IFirbyQQpkHXkrhVxytHBqS3leC7toL0TQygv4EO7bDFePfAcsaQhVp-ckxNJLwWsBoNzxI3WD4RaQ4njDmybGRXfJhKtTo1xIDBFohFCPuJiC7hBLxrVH7cDcpdljnBcGGsqUVIaB0qS5187Bv9iMt8wkvM6pLb8Ps1J6PXUQQIwWXhDslaZHTUx3ErRclx5BsX24yaXUlxG7IZLK69ALkl-hHt6K07rZazbjR3slwyTiGpCKLSJ7GDQX3TAJvaGw1uKl1yBMBKw-i3NXOBDsXC6sVtp09DmxvhHdkARDM-2rLQ46jV4Yp3nIC_Kc7zzIrh7ABRc_wJoXOy2H6nzY1IxAcseZRL108vks4Ckdhw0Om781zCmlwDfDg6ML5BwL2es9FOntHmgSBthrQeNjaalzg", + ID_TOKEN_V2_GUEST: + "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImNlNTIyNzBmNDYyNDNkOWRmMmE5ODBiNGNmNmJhNDA3In0.eyJ2ZXIiOiIyLjAiLCJpc3MiOiJodHRwczovL2xvZ2luLm1pY3Jvc29mdG9ubGluZS5jb20vNzJmOTg4YmYtODZmMS00MWFmLTkxYWItMmQ3Y2QwMTFkYjQ3Iiwic3ViIjoiQUFBQUFBQUFBQUFBQUFBQUFBQUFBSWt6cUZWclNhU2FGSHk3ODJiYnRiUSIsImF1ZCI6IjZjYjA0MDE4LWEzZjUtNDZhNy1iOTk1LTk0MGM3OGY1YWVmMyIsImV4cCI6MTUzNjM2MTQxMSwiaWF0IjoxNTM2Mjc0NzExLCJuYmYiOjE1MzYyNzQ3MTEsIm5hbWUiOiJBYmUgTGluY29sbiBHdWVzdCIsInByZWZlcnJlZF91c2VybmFtZSI6IkFiZUxpR3Vlc3RAbWljcm9zb2Z0LmNvbSIsImxvZ2luX2hpbnQiOiJBYmVMaUd1ZXN0TG9naW5IaW50IiwidXBuIjoiQWJlTGlHdWVzdFVwbiIsInNpZCI6IkFiZUxpR3Vlc3RTaWQiLCJvaWQiOiIwMDAwMDAwMC0wMDAwLTAwMDAtMTExMS0wMDAwMDAwMDAwMDAiLCJ0aWQiOiI3MmY5ODhiZi04NmYxLTQxYWYtOTFhYi0yZDdjZDAxMWRiNDciLCJub25jZSI6IjEyMzUyMyIsImFpbyI6IkRmMlVWWEwxaXghbE1DV01TT0pCY0ZhdHpjR2Z2Rkdoakt2OHE1ZzB4NzMyZFI1TUI1QmlzdkdRTzdZV0J5amQ4aVFETHEhZUdiSURha3lwNW1uT3JjZHFIZVlTbmx0ZXBRbVJwNkFJWjhqWSJ9.edpGJC3rVkFDGpNZ4SivvxkXM95NxGoCr6rI5F4Piz6Y2R4_RyI-giqkGUVQIHBe_ibBs944Qv3bPfM6OCgmnRcUitoc1RbdFndTh221tFixUsTnHWVhe5CkvDZ_F1NR3V1GO-06A9rB7M-V2yz0RNMW6jTwh_389mdUh-HtagdSkzEV2OUMPtep7EBqb25xZruTiyGYUW40eUBlEBFWBBhppLbkghUi5GLZ2ltLCKqtNxx6KcXe-3nD6mpE44wzkI6bsVSxS3ZhA5ZzFAn_yIo8GGBOdRo-vTJHf-sY7MLyAOX5rZfxEuSsXVwBH4flDycIHhe5YsqOXubCiJy1pxIQOVjf_jZC7FP26mjerXiNoiNU2id3BipdK3enMxS9Y-Y9zEvV-vvIuVpVppsWNzhocCJZ9gMthDhjLcY6G3uGGwTjSRqsQU6pUSDy_K5nUJNZSk8YuKkUw9nXDovE8rw147ldPXJ3m7ODu-zAjHtUd0YtHqLJyZkChQastX3P", IDTOKEN_V2_ALT: - "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IjJjNGE2MmYzMGRkNWMxNGE4MGFmMjMxZjY2M2ZmODliIn0.eyJ2ZXIiOiIyLjAiLCJpc3MiOiJodHRwczovL2xvZ2luLm1pY3Jvc29mdG9ubGluZS5jb20vOTE4ODA0MGQtNmM2Ny00YzViLWIxMTItMzZhMzA0YjY2ZGFkL3YyLjAiLCJzdWIiOiJBQUFBQUFBQUFBQUFBQUFBQUFBQUFJa3pxRlZyU2FTYUZIeTc4MmJidGFRIiwiYXVkIjoiNmNiMDQwMTgtYTNmNS00NmE3LWI5OTUtOTQwYzc4ZjVhZWYzIiwiZXhwIjoxNTM2MzYxNDExLCJpYXQiOjE1MzYyNzQ3MTEsIm5iZiI6MTUzNjI3NDcxMSwibmFtZSI6IkFiZSBMaW5jb2xuIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiQWJlTGlUb29AbWljcm9zb2Z0LmNvbSIsIm9pZCI6IjAwMDAwMDAwLTAwMDAtMDAwMC0wMDAwLTAwMDAwMDAwMDAwMCIsInRpZCI6IjcyZjk4OGJmLTg2ZjEtNDFhZi05MWFiLTJkN2NkMDExZGI0NyIsIm5vbmNlIjoiMTIzNTIzIiwiYWlvIjoiRGYyVVZYTDFpeCFsTUNXTVNPSkJjRmF0emNHZnZGR2hqS3Y4cTVnMHg3MzJkUjVNQjVCaXN2R1FPN1lXQnlqZDhpUURMcSFlR2JJRGFreXA1bW5PcmNkcUhlWVNubHRlcFFtUnA2QUlaOGpZIn0.VIsqgZiJfiOx-VkvYlq9X3EKmoRmfmHyyrPtSGww20Ng-eJCyQZqYlXEnsc44tuA9czlFpba9WwB2bCiS5jq4kjcTaQCg7ocxD1Db6PwY2mrAQdKUvDaZ7wSVXgOmR8SkyzqN55EGcMopXDOY12BLx_859lJYBJM-2ICmnscMNEjg2DZ1QvPRz7NX4MoOXWlBwhibxgQ4YEfq5LPOCit3LnSsT2Io1ORQouDPCfe55jRUoQoHyWOuXHA5w8Zr7Su6eHCnXVHv9k8mbAii_qOeLIaDg6Gh8B9Ht-BmBVExzzveUSe2ZLYxMDLj8BOhxlHn156l4bSqeEP40uzFae9uO4uVX4oIrd9p2MdbKm0PUahILtt5HJg4rs2f4xiyPC5Yt22QWPCqE8HFf9p3ZliAWmnAw6LyYIRl6rw3vnZFm031NWcRfSme9CfDbnqdks7qnLfVVgrh-1KoKgi6sTUxpJfXN5-VHw47k0tU7UZyIUDekt-mbXnxoAWy7LhnNQ3", + "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImNlNTIyNzBmNDYyNDNkOWRmMmE5ODBiNGNmNmJhNDA3In0.eyJ2ZXIiOiIyLjAiLCJpc3MiOiJodHRwczovL2xvZ2luLm1pY3Jvc29mdG9ubGluZS5jb20vNzJmOTg4YmYtODZmMS00MWFmLTkxYWItMmQ3Y2QwMTFkYjQ3Iiwic3ViIjoiQUFBQUFBQUFBQUFBQUFBQUFBQUFBSWt6cUZWclNhU2FGSHk3ODJiYnRhUSIsImF1ZCI6IjZjYjA0MDE4LWEzZjUtNDZhNy1iOTk1LTk0MGM3OGY1YWVmMyIsImV4cCI6MTUzNjM2MTQxMSwiaWF0IjoxNTM2Mjc0NzExLCJuYmYiOjE1MzYyNzQ3MTEsIm5hbWUiOiJBYmUgTGluY29sbiBUb28iLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJBYmVMaVRvb0BtaWNyb3NvZnQuY29tIiwibG9naW5faGludCI6IkFiZUxpVG9vTG9naW5IaW50IiwidXBuIjoiQWJlTGlUb29VcG4iLCJzaWQiOiJBYmVMaVRvb1NpZCIsIm9pZCI6IjAwMDAwMDAwLTAwMDAtMDAwMC0wMDAwLTAwMDAwMDAwMDAwMCIsInRpZCI6IjcyZjk4OGJmLTg2ZjEtNDFhZi05MWFiLTJkN2NkMDExZGI0NyIsIm5vbmNlIjoiMTIzNTIzIiwiYWlvIjoiRGYyVVZYTDFpeCFsTUNXTVNPSkJjRmF0emNHZnZGR2hqS3Y4cTVnMHg3MzJkUjVNQjVCaXN2R1FPN1lXQnlqZDhpUURMcSFlR2JJRGFreXA1bW5PcmNkcUhlWVNubHRlcFFtUnA2QUlaOGpZIn0.aXNizpVxIF6owdG4CHOCl41lqqtQeYujg7OOtNf6L1Zj8x4jPDih2EnQ6015ybJV4ujxMP57pHr2fc5UaCXk6Nfvm78z3cgd605drgWBf_QcpWVkYxJfkYogZNbl757IhgDvIlhj2DidLaaRrLYhO75MKy-3M4wjvELXnL-fXetrqkjfCDiiWnC3LWA9R8tXjtsBK0GsIVSysW6789iFHZY_taruGQs7MIXzgmOtQpMuwYyFw3hO389cfoZ003qJgv6w4vc8pMpEQJW1LPQ4Bwhq2e-ZATo4TqvWhY9f4VErIVNfvWsDVmYWExtc4a9uY8KYKAinVDdyJlphqvIZKoJ7Z3lF6wNSUns2kaNgaVq_MJaWCKIv4yMPq_JJwa41sVLfTMVIphDR0cJp_8MkXdTBDSOZMEZB9xnI-E9IA4j49NkKYuGWUUfek6wRkOkoIUrDWuJQAgw_5nFfsOp8Mf5vw-jSl1UVtaIIukIU5cH0wmTSFf4indzMb1vP5GAQ", IDTOKEN_V2_NEWCLAIM: "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IjFMVE16YWtpaGlSbGFfOHoyQkVKVlhlV01xbyJ9.ewogICJ2ZXIiOiAiMi4wIiwKICAiaXNzIjogImh0dHBzOi8vbG9naW4ubWljcm9zb2Z0b25saW5lLmNvbS85MTg4MDQwZC02YzY3LTRjNWItYjExMi0zNmEzMDRiNjZkYWQvdjIuMCIsCiAgInN1YiI6ICJBQUFBQUFBQUFBQUFBQUFBQUFBQUFJa3pxRlZyU2FTYUZIeTc4MmJidGFRIiwKICAiYXVkIjogIjZjYjA0MDE4LWEzZjUtNDZhNy1iOTk1LTk0MGM3OGY1YWVmMyIsCiAgImV4cCI6IDE1MzYzNjE0MTEsCiAgImlhdCI6IDE1MzYyNzQ3MTEsCiAgIm5iZiI6IDE1MzYyNzQ3MTEsCiAgIm5hbWUiOiAiQWJlIExpbmNvbG4iLAogICJwcmVmZXJyZWRfdXNlcm5hbWUiOiAiQWJlTGlAbWljcm9zb2Z0LmNvbSIsCiAgIm9pZCI6ICIwMDAwMDAwMC0wMDAwLTAwMDAtNjZmMy0zMzMyZWNhN2VhODEiLAogICJlbWFpbCI6ICJBYmVMaUBtaWNyb3NvZnQuY29tIiwKICAidGlkIjogIjMzMzgwNDBkLTZjNjctNGM1Yi1iMTEyLTM2YTMwNGI2NmRhZCIsCiAgIm5vbmNlIjogIjEyMzUyMyIsCiAgImFpbyI6ICJEZjJVVlhMMWl4IWxNQ1dNU09KQmNGYXR6Y0dmdkZHaGpLdjhxNWcweDczMmRSNU1CNUJpc3ZHUU83WVdCeWpkOGlRRExxIWVHYklEYWt5cDVtbk9yY2RxSGVZU25sdGVwUW1ScDZBSVo4alkiCn0=.1AFWW-Ck5nROwSlltm7GzZvDwUkqvhSQpm55TQsmVo9Y59cLhRXpvB8n-55HCr9Z6G_31_UbeUkoz612I2j_Sm9FFShSDDjoaLQr54CreGIJvjtmS3EkK9a7SJBbcpL1MpUtlfygow39tFjY7EVNW9plWUvRrTgVk7lYLprvfzw-CIqw3gHC-T7IK_m_xkr08INERBtaecwhTeN4chPC4W3jdmw_lIxzC48YoQ0dB1L9-ImX98Egypfrlbm0IBL5spFzL6JDZIRRJOu8vecJvj1mq-IUhGt0MacxX8jdxYLP-KUu2d9MbNKpCKJuZ7p8gwTL5B7NlUdh_dmSviPWrw", LOGIN_AT_STRING: @@ -89,17 +90,11 @@ export const TEST_TOKENS = { POP_TOKEN: "eyJ0eXAiOiJKV1QiLCJub25jZSI6InFMZmZKT255c2dnVnhhdGxSbVhvR0dnYkx3NDV5LTdsWkswaHJWSm9zeDQiLCJhbGciOiJSUzI1NiIsIng1dCI6InZhcF9pdmtIdHRMRmNubm9CWEF3SjVIWDBLNCIsImtpZCI6InZhcF9pdmtIdHRMRmNubm9CWEF3SjVIWDBLNCJ9.eyJhdWQiOiIwMDAwMDAwMy0wMDAwLTAwMDAtYzAwMC0wMDAwMDAwMDAwMDAiLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLXBwZS5uZXQvMTllZWEyZjgtZTE3YS00NzBmLTk1NGQtZDg5N2M0N2YzMTFjLyIsImlhdCI6MTYxODAwNzU2MCwibmJmIjoxNjE4MDA3NTYwLCJleHAiOjE2MTgwMTE0NjAsImFjY3QiOjAsImFjciI6IjEiLCJhY3JzIjpbInVybjp1c2VyOnJlZ2lzdGVyc2VjdXJpdHlpbmZvIiwidXJuOm1pY3Jvc29mdDpyZXExIiwidXJuOm1pY3Jvc29mdDpyZXEyIiwidXJuOm1pY3Jvc29mdDpyZXEzIiwiYzEiLCJjMiIsImMzIiwiYzQiLCJjNSIsImM2IiwiYzciLCJjOCIsImM5IiwiYzEwIiwiYzExIiwiYzEyIiwiYzEzIiwiYzE0IiwiYzE1IiwiYzE2IiwiYzE3IiwiYzE4IiwiYzE5IiwiYzIwIiwiYzIxIiwiYzIyIiwiYzIzIiwiYzI0IiwiYzI1Il0sImFpbyI6IkUyTmdZRGo3Y0dVWEczT1p4OXhXSjVNRWg5MXU2NVFTZnAzVXYycVpLSHByaHRtVkY2d0EiLCJhbXIiOlsicHdkIl0sImFwcF9kaXNwbGF5bmFtZSI6IlBLLU1TQUxUZXN0Mi4wIiwiYXBwaWQiOiIzZmJhNTU2ZS01ZDRhLTQ4ZTMtOGUxYS1mZDU3YzEyY2I4MmUiLCJhcHBpZGFjciI6IjAiLCJjbmYiOnsia2lkIjoiVjZOX0hNUGFnTnBZU193eE0xNFg3M3EzZVd6YlRyOVozMVJ5SGtJY04wWSIsInhtc19rc2wiOiJzdyJ9LCJmYW1pbHlfbmFtZSI6IkJhc2ljIFVzZXIiLCJnaXZlbl9uYW1lIjoiQ2xvdWQgSURMQUIiLCJpZHR5cCI6InVzZXIiLCJpcGFkZHIiOiIyNC4xNy4yNDYuMjA5IiwibmFtZSI6IkNsb3VkIElETEFCIEJhc2ljIFVzZXIiLCJvaWQiOiJiZTA2NGMzNy0yNjE3LTQ2OGMtYjYyNy0yNWI0ZTQ4MTdhZGYiLCJwbGF0ZiI6IjMiLCJwdWlkIjoiMTAwMzQwMDAwMDU0NzdCQSIsInJoIjoiMC5BQUFBLUtMdUdYcmhEMGVWVGRpWHhIOHhIRzVWdWo5S1hlTklqaHI5VjhFc3VDNEJBTmsuIiwic2NwIjoiRmlsZXMuUmVhZCBNYWlsLlJlYWQgb3BlbmlkIHByb2ZpbGUgVXNlci5SZWFkIGVtYWlsIiwic2lnbmluX3N0YXRlIjpbImttc2kiXSwic3ViIjoidExjaFl1bUczSXZZT1VrQlprU0EzbWhnOEVfYnNGZDhuU2licUlOX0UxVSIsInRpZCI6IjE5ZWVhMmY4LWUxN2EtNDcwZi05NTRkLWQ4OTdjNDdmMzExYyIsInVuaXF1ZV9uYW1lIjoiSURMQUJAbXNpZGxhYjAuY2NzY3RwLm5ldCIsInVwbiI6IklETEFCQG1zaWRsYWIwLmNjc2N0cC5uZXQiLCJ1dGkiOiJ5a2tPd3dyTkFVT1k5SUVXejRITEFBIiwidmVyIjoiMS4wIiwid2lkcyI6WyJiNzlmYmY0ZC0zZWY5LTQ2ODktODE0My03NmIxOTRlODU1MDkiXSwieG1zX3N0Ijp7InN1YiI6Imx2QnRkdmVkdDRkT1pyeGZvQjdjbV9UTkU3THFucG5lcGFHc3EtUmZkS2MifSwieG1zX3RjZHQiOjE1NDQ1NzQzNjN9.VPBqUrMDc-H1T4paZoSbGaec0lBoJqSiu13chxJmgee1lDxUFr2pM52tqzPPH6N_Yk-VQ0_AKTyvfnbAQw4mryhp3SJytZbU7FedrXX7oq2laLh9s0K_Hz1EZSj5xg3SSUxXmKEjdePN6d0_5MLlt1P-LcL2PAGgkEEBhUfDm6pAxyTMO8Mw1DUYbq7kr_IzyQ71V-kuoYHDjazghSIwOkidoWMCPP-HIENVbFEKUDKFGDiOzU76IagUBAYUQ4JD1bC9hHA-OO6AV8xLK7UoPyx9UH7fLbiImzhARBxMkmAQu9v2kwn5Hl9hoBEBhlu48YOYOr4O3GxwKisff87R9Q", REFRESH_TOKEN: "thisIsARefreshT0ken", - // [SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Fake credential used for testing purposes")] - ID_TOKEN_V2_WITH_LOGIN_HINT: - "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6ImEzYmRhYjIzYTVlMDI4NmM2NmM1MjRiZWFmNDQzZGJhIn0.eyJzdWIiOiJBQUFBQUFBQUFBQUFBQUFBQUFBQUFJa3pxRlZyU2FTYUZIeTc4MmJidGFRIiwidmVyIjoiMi4wIiwiYWlvIjoiRGYyVVZYTDFpeCFsTUNXTVNPSkJjRmF0emNHZnZGR2hqS3Y4cTVnMHg3MzJkUjVNQjVCaXN2R1FPN1lXQnlqZDhpUURMcSFlR2JJRGFreXA1bW5PcmNkcUhlWVNubHRlcFFtUnA2QUlaOGpZIiwiaXNzIjoiaHR0cHM6Ly9sb2dpbi5taWNyb3NvZnRvbmxpbmUuY29tLzkxODgwNDBkLTZjNjctNGM1Yi1iMTEyLTM2YTMwNGI2NmRhZC92Mi4wIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiVW5pcXVlIFVzZXJuYW1lIiwib2lkIjoiMDAwMDAwMDAtMDAwMC0wMDAwLTY2ZjMtMzMzMmVjYTdlYTgxIiwibm9uY2UiOiIxMjM1MjMiLCJ0aWQiOiIzMzM4MDQwZC02YzY3LTRjNWItYjExMi0zNmEzMDRiNjZkYWQiLCJhdWQiOiI2Y2IwNDAxOC1hM2Y1LTQ2YTctYjk5NS05NDBjNzhmNWFlZjMiLCJsb2dpbl9oaW50IjoidGVzdExvZ2luSGludCIsInNpZCI6InRlc3RTaWQiLCJuYmYiOiIxNTM2MzYxNDExIiwibmFtZSI6IkFiZSBMaW5jb2xuIiwiZXhwIjoiMTUzNjM2MTQxMSIsImlhdCI6IjE1MzYzNjE0MTEifQ.ZfEosstCNsNiOnFd7WXJWMSkIKzticb97qZVwoprztMBeT_szHFhZM1RZCnAbmHlC8J8b7RPpwm09RdjN31iDA", - // [SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Fake credential used for testing purposes")] - ID_TOKEN_V2_WITH_UPN: - "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJBQUFBQUFBQUFBQUFBQUFBQUFBQUFJa3pxRlZyU2FTYUZIeTc4MmJidGFRIiwidmVyIjoiMi4wIiwiYWlvIjoiRGYyVVZYTDFpeCFsTUNXTVNPSkJjRmF0emNHZnZGR2hqS3Y4cTVnMHg3MzJkUjVNQjVCaXN2R1FPN1lXQnlqZDhpUURMcSFlR2JJRGFreXA1bW5PcmNkcUhlWVNubHRlcFFtUnA2QUlaOGpZIiwiaXNzIjoiaHR0cHM6Ly9sb2dpbi5taWNyb3NvZnRvbmxpbmUuY29tLzkxODgwNDBkLTZjNjctNGM1Yi1iMTEyLTM2YTMwNGI2NmRhZC92Mi4wIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiQWJlTGlAbWljcm9zb2Z0LmNvbSIsIm9pZCI6IjAwMDAwMDAwLTAwMDAtMDAwMC02NmYzLTMzMzJlY2E3ZWE4MSIsIm5vbmNlIjoiMTIzNTIzIiwidGlkIjoiMzMzODA0MGQtNmM2Ny00YzViLWIxMTItMzZhMzA0YjY2ZGFkIiwiYXVkIjoiNmNiMDQwMTgtYTNmNS00NmE3LWI5OTUtOTQwYzc4ZjVhZWYzIiwibmJmIjoiMTUzNjM2MTQxMSIsIm5hbWUiOiJBYmUgTGluY29sbiIsImV4cCI6IjE1MzYzNjE0MTEiLCJpYXQiOiIxNTM2MzYxNDExIiwidXBuIjoidGVzdFVwbiJ9._8gXqY4qn4A8v_et5fESz-82BU4ad2F_v2msO9CcvIA", }; export const ID_TOKEN_CLAIMS = { ver: "2.0", - iss: "https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0", + iss: "https://login.microsoftonline.com/3338040d-6c67-4c5b-b112-36a304b66dad/v2.0", sub: "AAAAAAAAAAAAAAAAAAAAAIkzqFVrSaSaFHy782bbtaQ", aud: "6cb04018-a3f5-46a7-b995-940c78f5aef3", exp: 1536361411, @@ -107,15 +102,36 @@ export const ID_TOKEN_CLAIMS = { nbf: 1536274711, name: "Abe Lincoln", preferred_username: "AbeLi@microsoft.com", + login_hint: "AbeLiLoginHint", + upn: "AbeLiUpn", + sid: "AbeLiSid", oid: "00000000-0000-0000-66f3-3332eca7ea81", tid: "3338040d-6c67-4c5b-b112-36a304b66dad", nonce: "123523", aio: "Df2UVXL1ix!lMCWMSOJBcFatzcGfvFGhjKv8q5g0x732dR5MB5BisvGQO7YWByjd8iQDLq!eGbIDakyp5mnOrcdqHeYSnltepQmRp6AIZ8jY", }; +export const GUEST_ID_TOKEN_CLAIMS = { + ...ID_TOKEN_CLAIMS, + iss: "https://login.microsoftonline.com/72f988bf-86f1-41af-91ab-2d7cd011db47", + sub: "AAAAAAAAAAAAAAAAAAAAAIkzqFVrSaSaFHy782bbtbQ", + name: "Abe Lincoln Guest", + preferred_username: "AbeLiGuest@microsoft.com", + login_hint: "AbeLiGuestLoginHint", + upn: "AbeLiGuestUpn", + sid: "AbeLiGuestSid", + oid: "00000000-0000-0000-1111-000000000000", + tid: "72f988bf-86f1-41af-91ab-2d7cd011db47", +}; + export const ID_TOKEN_ALT_CLAIMS = { ...ID_TOKEN_CLAIMS, + iss: "https://login.microsoftonline.com/72f988bf-86f1-41af-91ab-2d7cd011db47", + name: "Abe Lincoln Too", preferred_username: "AbeLiToo@microsoft.com", + login_hint: "AbeLiTooLoginHint", + upn: "AbeLiTooUpn", + sid: "AbeLiTooSid", oid: "00000000-0000-0000-0000-000000000000", tid: "72f988bf-86f1-41af-91ab-2d7cd011db47", }; @@ -129,18 +145,17 @@ export const TEST_TOKEN_LIFETIMES = { // Test CLIENT_INFO export const TEST_DATA_CLIENT_INFO = { - TEST_UID: "123-test-uid", - TEST_UID_ENCODED: "MTIzLXRlc3QtdWlk", - TEST_UTID: "456-test-utid", - TEST_UTID_ENCODED: "NDU2LXRlc3QtdXRpZA==", - TEST_UTID_URLENCODED: "NDU2LXRlc3QtdXRpZA", - TEST_DECODED_CLIENT_INFO: '{"uid":"123-test-uid","utid":"456-test-utid"}', + TEST_UID: "00000000-0000-0000-66f3-3332eca7ea81", + TEST_UTID: "3338040d-6c67-4c5b-b112-36a304b66dad", + TEST_DECODED_CLIENT_INFO: + '{"uid":"00000000-0000-0000-66f3-3332eca7ea81","utid":"3338040d-6c67-4c5b-b112-36a304b66dad"}', TEST_INVALID_JSON_CLIENT_INFO: - '{"uid":"123-test-uid""utid":"456-test-utid"}', + '{"uid":"00000000-0000-0000-66f3-3332eca7ea81""utid":"3338040d-6c67-4c5b-b112-36a304b66dad"}', TEST_RAW_CLIENT_INFO: - "eyJ1aWQiOiIxMjMtdGVzdC11aWQiLCJ1dGlkIjoiNDU2LXRlc3QtdXRpZCJ9", + "eyJ1aWQiOiIwMDAwMDAwMC0wMDAwLTAwMDAtNjZmMy0zMzMyZWNhN2VhODEiLCJ1dGlkIjoiMzMzODA0MGQtNmM2Ny00YzViLWIxMTItMzZhMzA0YjY2ZGFkIn0", TEST_CLIENT_INFO_B64ENCODED: "eyJ1aWQiOiIxMjM0NSIsInV0aWQiOiI2Nzg5MCJ9", - TEST_HOME_ACCOUNT_ID: "MTIzLXRlc3QtdWlk.NDU2LXRlc3QtdXRpZA==", + TEST_HOME_ACCOUNT_ID: + "00000000-0000-0000-66f3-3332eca7ea81.3338040d-6c67-4c5b-b112-36a304b66dad", TEST_LOCAL_ACCOUNT_ID: "00000000-0000-0000-66f3-3332eca7ea81", }; diff --git a/lib/msal-common/src/cache/entities/AccountEntity.ts b/lib/msal-common/src/cache/entities/AccountEntity.ts index 4e95c74e2a..121d3e9da6 100644 --- a/lib/msal-common/src/cache/entities/AccountEntity.ts +++ b/lib/msal-common/src/cache/entities/AccountEntity.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. */ -import { Separators, CacheAccountType } from "../../utils/Constants"; +import { CacheAccountType } from "../../utils/Constants"; import { Authority } from "../../authority/Authority"; import { ICrypto } from "../../crypto/ICrypto"; import { ClientInfo, buildClientInfo } from "../../account/ClientInfo"; @@ -68,7 +68,7 @@ export class AccountEntity { */ generateAccountId(): string { const accountId: Array = [this.homeAccountId, this.environment]; - return accountId.join(Separators.CACHE_KEY_SEPARATOR).toLowerCase(); + return accountId.join("-").toLowerCase(); } /** @@ -125,7 +125,7 @@ export class AccountEntity { homeTenantId || accountInterface.tenantId || "", ]; - return accountKey.join(Separators.CACHE_KEY_SEPARATOR).toLowerCase(); + return accountKey.join("-").toLowerCase(); } /** @@ -288,7 +288,7 @@ export class AccountEntity { cryptoObj ); if (clientInfo.uid && clientInfo.utid) { - return `${clientInfo.uid}${Separators.CLIENT_INFO_SEPARATOR}${clientInfo.utid}`; + return `${clientInfo.uid}.${clientInfo.utid}`; } } catch (e) {} } diff --git a/lib/msal-common/src/index.ts b/lib/msal-common/src/index.ts index b0ed697f84..0ca83701bb 100644 --- a/lib/msal-common/src/index.ts +++ b/lib/msal-common/src/index.ts @@ -34,6 +34,7 @@ export { TenantProfile, updateAccountTenantProfileData, tenantIdMatchesHomeTenant, + buildTenantProfileFromIdTokenClaims, } from "./account/AccountInfo"; export * as AuthToken from "./account/AuthToken"; export { @@ -145,7 +146,10 @@ export { DeviceCodeResponse, ServerDeviceCodeResponse, } from "./response/DeviceCodeResponse"; -export { ResponseHandler } from "./response/ResponseHandler"; +export { + ResponseHandler, + buildAccountToCache, +} from "./response/ResponseHandler"; export { ScopeSet } from "./request/ScopeSet"; export { AuthenticationHeaderParser } from "./request/AuthenticationHeaderParser"; // Logger Callback diff --git a/lib/msal-common/src/response/ResponseHandler.ts b/lib/msal-common/src/response/ResponseHandler.ts index d1702d842a..1e1eeadaab 100644 --- a/lib/msal-common/src/response/ResponseHandler.ts +++ b/lib/msal-common/src/response/ResponseHandler.ts @@ -428,12 +428,17 @@ export class ResponseHandler { claimsTenantId || "" ); - cachedAccount = this.setCachedAccount( + cachedAccount = buildAccountToCache( + this.cacheStorage, authority, - serverTokenResponse, + this.homeAccountIdentifier, idTokenClaims, + this.cryptoObj, + serverTokenResponse.client_info, claimsTenantId, - authCodePayload + authCodePayload, + undefined, + this.logger ); } @@ -521,62 +526,6 @@ export class ResponseHandler { ); } - private setCachedAccount( - authority: Authority, - serverTokenResponse: ServerAuthorizationTokenResponse, - idTokenClaims: TokenClaims, - claimsTenantId: string | null, - authCodePayload?: AuthorizationCodePayload - ): AccountEntity | undefined { - this.logger.verbose("setCachedAccount called"); - - // Check if base account is already cached - const accountKeys = this.cacheStorage.getAccountKeys(); - const baseAccountKey = accountKeys.find((accountKey: string) => { - return accountKey.startsWith(this.homeAccountIdentifier); - }); - - let cachedAccount: AccountEntity | null = null; - if (baseAccountKey) { - cachedAccount = this.cacheStorage.getAccount( - baseAccountKey, - this.logger - ); - } - - const baseAccount = - cachedAccount || - AccountEntity.createAccount( - { - homeAccountId: this.homeAccountIdentifier, - idTokenClaims: idTokenClaims, - clientInfo: serverTokenResponse.client_info, - cloudGraphHostName: authCodePayload?.cloud_graph_host_name, - msGraphHost: authCodePayload?.msgraph_host, - }, - authority, - this.cryptoObj - ); - - const tenantProfiles = baseAccount.tenantProfiles || []; - - if ( - claimsTenantId && - !tenantProfiles.find((tenantProfile) => { - return tenantProfile.tenantId === claimsTenantId; - }) - ) { - const newTenantProfile = buildTenantProfileFromIdTokenClaims( - this.homeAccountIdentifier, - idTokenClaims - ); - tenantProfiles.push(newTenantProfile); - } - baseAccount.tenantProfiles = tenantProfiles; - - return baseAccount; - } - /** * Creates an @AuthenticationResult from @CacheRecord , @IdToken , and a boolean that states whether or not the result is from cache. * @@ -697,3 +646,62 @@ export class ResponseHandler { }; } } + +export function buildAccountToCache( + cacheStorage: CacheManager, + authority: Authority, + homeAccountId: string, + idTokenClaims: TokenClaims, + cryptoObj: ICrypto, + clientInfo?: string, + claimsTenantId?: string | null, + authCodePayload?: AuthorizationCodePayload, + nativeAccountId?: string, + logger?: Logger +): AccountEntity { + logger?.verbose("setCachedAccount called"); + + // Check if base account is already cached + const accountKeys = cacheStorage.getAccountKeys(); + const baseAccountKey = accountKeys.find((accountKey: string) => { + return accountKey.startsWith(homeAccountId); + }); + + let cachedAccount: AccountEntity | null = null; + if (baseAccountKey) { + cachedAccount = cacheStorage.getAccount(baseAccountKey, logger); + } + + const baseAccount = + cachedAccount || + AccountEntity.createAccount( + { + homeAccountId: homeAccountId, + idTokenClaims: idTokenClaims, + clientInfo: clientInfo, + cloudGraphHostName: authCodePayload?.cloud_graph_host_name, + msGraphHost: authCodePayload?.msgraph_host, + nativeAccountId: nativeAccountId, + }, + authority, + cryptoObj + ); + + const tenantProfiles = baseAccount.tenantProfiles || []; + + if ( + claimsTenantId && + !tenantProfiles.find((tenantProfile) => { + return tenantProfile.tenantId === claimsTenantId; + }) + ) { + const newTenantProfile = buildTenantProfileFromIdTokenClaims( + homeAccountId, + idTokenClaims + ); + tenantProfiles.push(newTenantProfile); + } + baseAccount.tenantProfiles = tenantProfiles; + + return baseAccount; +} From 572c28dab8dac02809fc2ecc230c39ae83f16842 Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Sun, 12 Nov 2023 23:36:05 -0800 Subject: [PATCH 52/91] Update msal-node to use tenantProfiles in serialization instead of tenants array --- .../src/cache/serializer/Deserializer.ts | 4 +-- lib/msal-node/test/cache/Storage.spec.ts | 28 +++++++++++++++---- .../cache-unrecognized-entities.json | 2 +- lib/msal-node/test/cache/cacheConstants.ts | 13 +++++++-- .../cache/serializer/Deserializer.spec.ts | 13 +++++++-- .../test/cache/serializer/cache.json | 8 ++++-- lib/msal-node/test/client/ClientTestUtils.ts | 4 +++ lib/msal-node/test/utils/TestConstants.ts | 13 ++++++++- 8 files changed, 67 insertions(+), 18 deletions(-) diff --git a/lib/msal-node/src/cache/serializer/Deserializer.ts b/lib/msal-node/src/cache/serializer/Deserializer.ts index 070ee50fdc..71048d1874 100644 --- a/lib/msal-node/src/cache/serializer/Deserializer.ts +++ b/lib/msal-node/src/cache/serializer/Deserializer.ts @@ -64,8 +64,8 @@ export class Deserializer { lastModificationTime: serializedAcc.last_modification_time, lastModificationApp: serializedAcc.last_modification_app, tenantProfiles: serializedAcc.tenantProfiles?.map( - (tenantProfile) => { - return JSON.parse(tenantProfile); + (serializedTenantProfile) => { + return JSON.parse(serializedTenantProfile); } ), }; diff --git a/lib/msal-node/test/cache/Storage.spec.ts b/lib/msal-node/test/cache/Storage.spec.ts index 2c1ca21c28..d9fc75a9aa 100644 --- a/lib/msal-node/test/cache/Storage.spec.ts +++ b/lib/msal-node/test/cache/Storage.spec.ts @@ -153,13 +153,20 @@ describe("Storage tests for msal-node: ", () => { const mockAccountData = { username: "Jane Doe", - localAccountId: "object5678", + localAccountId: "uid", realm: "samplerealm", environment: "login.windows.net", homeAccountId: "uid1.utid1", authorityType: "MSSTS", clientInfo: "eyJ1aWQiOiJ1aWQxIiwgInV0aWQiOiJ1dGlkMSJ9", - tenants: ["samplerealm"], + tenantProfiles: [ + { + tenantId: "utid1", + localAccountId: "uid", + name: "Jane Doe", + isHomeTenant: true, + }, + ], }; let mockAccountEntity = CacheManager.toObject( @@ -183,8 +190,9 @@ describe("Storage tests for msal-node: ", () => { const outdatedAccountKey = "uid.utid3-login.microsoftonline.com-utid3"; const outdatedAccountData = { - username: "Jane Doe", - localAccountId: "object5678", + username: "janedoe@microsoft.com", + name: "Jane Doe", + localAccountId: "uid", realm: "utid3", environment: "login.microsoftonline.com", homeAccountId: "uid.utid3", @@ -199,7 +207,17 @@ describe("Storage tests for msal-node: ", () => { let updatedMockAccountEntity = CacheManager.toObject( new AccountEntity(), - { ...outdatedAccountData, tenants: ["utid3"] } + { + ...outdatedAccountData, + tenantProfiles: [ + { + tenantId: "utid3", + localAccountId: "uid", + name: "Jane Doe", + isHomeTenant: true, + }, + ], + } ); const updatedAccountKey = updatedMockAccountEntity.generateAccountKey(); expect(outdatedMockAccountEntity).toBeInstanceOf(AccountEntity); diff --git a/lib/msal-node/test/cache/cache-test-files/cache-unrecognized-entities.json b/lib/msal-node/test/cache/cache-test-files/cache-unrecognized-entities.json index 3612e24fd7..30392da91b 100644 --- a/lib/msal-node/test/cache/cache-test-files/cache-unrecognized-entities.json +++ b/lib/msal-node/test/cache/cache-test-files/cache-unrecognized-entities.json @@ -16,7 +16,7 @@ "home_account_id": "uid.utid", "authority_type": "MSSTS", "client_info": "eyJ1aWQiOiJ1aWQiLCAidXRpZCI6InV0aWQifQ==", - "tenants": ["microsoft"] + "tenantProfiles": ["{\"tenantId\":\"utid\",\"localAccountId\":\"object1234\",\"isHomeTenant\":true}"] } }, "RefreshToken": { diff --git a/lib/msal-node/test/cache/cacheConstants.ts b/lib/msal-node/test/cache/cacheConstants.ts index 2596d2db1a..19b6bd9b07 100644 --- a/lib/msal-node/test/cache/cacheConstants.ts +++ b/lib/msal-node/test/cache/cacheConstants.ts @@ -68,11 +68,18 @@ export const mockAccountEntity = { homeAccountId: "uid.utid", environment: "login.microsoftonline.com", realm: "utid", - localAccountId: "object1234", - username: "John Doe", + localAccountId: "uid", + username: "johndoe@microsoft.com", authorityType: "MSSTS", clientInfo: "eyJ1aWQiOiJ1aWQiLCAidXRpZCI6InV0aWQifQ==", - tenants: ["utid"], + tenantProfiles: [ + { + tenantId: "utid", + localAccountId: "uid", + name: "John Doe", + isHomeTenant: true, + }, + ], }; export const mockAppMetaDataEntity = { diff --git a/lib/msal-node/test/cache/serializer/Deserializer.spec.ts b/lib/msal-node/test/cache/serializer/Deserializer.spec.ts index 16439ed450..0802b87e72 100644 --- a/lib/msal-node/test/cache/serializer/Deserializer.spec.ts +++ b/lib/msal-node/test/cache/serializer/Deserializer.spec.ts @@ -14,14 +14,21 @@ describe("Deserializer test cases", () => { test("deserializeJSONBlob", () => { const mockAccount = { "uid.utid-login.microsoftonline.com-utid": { - username: "John Doe", - local_account_id: "object1234", + username: "johndoe@microsoft.com", + local_account_id: "uid", realm: "utid", environment: "login.microsoftonline.com", home_account_id: "uid.utid", authority_type: "MSSTS", client_info: "eyJ1aWQiOiJ1aWQiLCAidXRpZCI6InV0aWQifQ==", - tenants: ["utid"], + tenantProfiles: [ + JSON.stringify({ + tenantId: "utid", + localAccountId: "uid", + name: "John Doe", + isHomeTenant: true, + }), + ], }, }; const acc = Deserializer.deserializeJSONBlob(cache); diff --git a/lib/msal-node/test/cache/serializer/cache.json b/lib/msal-node/test/cache/serializer/cache.json index 17ae3cbe49..2e8b2a11ea 100644 --- a/lib/msal-node/test/cache/serializer/cache.json +++ b/lib/msal-node/test/cache/serializer/cache.json @@ -1,14 +1,16 @@ { "Account": { "uid.utid-login.microsoftonline.com-utid": { - "username": "John Doe", - "local_account_id": "object1234", + "username": "johndoe@microsoft.com", + "local_account_id": "uid", "realm": "utid", "environment": "login.microsoftonline.com", "home_account_id": "uid.utid", "authority_type": "MSSTS", "client_info": "eyJ1aWQiOiJ1aWQiLCAidXRpZCI6InV0aWQifQ==", - "tenants": ["utid"] + "tenantProfiles": [ + "{\"tenantId\":\"utid\",\"localAccountId\":\"uid\",\"name\":\"John Doe\",\"isHomeTenant\":true}" + ] } }, "RefreshToken": { diff --git a/lib/msal-node/test/client/ClientTestUtils.ts b/lib/msal-node/test/client/ClientTestUtils.ts index a21f751176..24c056468e 100644 --- a/lib/msal-node/test/client/ClientTestUtils.ts +++ b/lib/msal-node/test/client/ClientTestUtils.ts @@ -56,6 +56,10 @@ export class MockStorageClass extends CacheManager { return null; } + getCachedAccountEntity(accountKey: string): AccountEntity | null { + return this.getAccount(accountKey); + } + setAccount(value: AccountEntity): void { const key = value.generateAccountKey(); this.store[key] = value; diff --git a/lib/msal-node/test/utils/TestConstants.ts b/lib/msal-node/test/utils/TestConstants.ts index c5a18d6573..9e77c8bc76 100644 --- a/lib/msal-node/test/utils/TestConstants.ts +++ b/lib/msal-node/test/utils/TestConstants.ts @@ -9,6 +9,7 @@ import { AuthenticationResult, createClientAuthError, ClientAuthErrorCodes, + TenantProfile, } from "@azure/msal-common"; export const TEST_CONSTANTS = { @@ -232,7 +233,17 @@ export const mockAccountInfo: AccountInfo = { idTokenClaims: ID_TOKEN_CLAIMS, name: ID_TOKEN_CLAIMS.name, nativeAccountId: undefined, - tenants: [ID_TOKEN_CLAIMS.tid], + tenantProfiles: new Map([ + [ + ID_TOKEN_CLAIMS.tid, + { + tenantId: ID_TOKEN_CLAIMS.tid, + localAccountId: ID_TOKEN_CLAIMS.oid, + name: ID_TOKEN_CLAIMS.name, + isHomeTenant: true, + } as TenantProfile, + ], + ]), }; export const mockNativeAccountInfo: AccountInfo = { From c9e8a18b3cc45e21ea6a87366c35eb8572915d03 Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Mon, 13 Nov 2023 00:51:57 -0800 Subject: [PATCH 53/91] Replace encoding test constants --- lib/msal-browser/test/utils/StringConstants.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/msal-browser/test/utils/StringConstants.ts b/lib/msal-browser/test/utils/StringConstants.ts index b5540c7f44..7f8e38c455 100644 --- a/lib/msal-browser/test/utils/StringConstants.ts +++ b/lib/msal-browser/test/utils/StringConstants.ts @@ -146,7 +146,10 @@ export const TEST_TOKEN_LIFETIMES = { // Test CLIENT_INFO export const TEST_DATA_CLIENT_INFO = { TEST_UID: "00000000-0000-0000-66f3-3332eca7ea81", + TEST_UID_ENCODED: "MDAwMDAwMDAtMDAwMC0wMDAwLTY2ZjMtMzMzMmVjYTdlYTgx", TEST_UTID: "3338040d-6c67-4c5b-b112-36a304b66dad", + TEST_UTID_ENCODED: "MzMzODA0MGQtNmM2Ny00YzViLWIxMTItMzZhMzA0YjY2ZGFk", + TEST_UTID_URLENCODED: "MzMzODA0MGQtNmM2Ny00YzViLWIxMTItMzZhMzA0YjY2ZGFk", TEST_DECODED_CLIENT_INFO: '{"uid":"00000000-0000-0000-66f3-3332eca7ea81","utid":"3338040d-6c67-4c5b-b112-36a304b66dad"}', TEST_INVALID_JSON_CLIENT_INFO: From 6721a3a514d203e0a9e377e0fe9b773b7e27e166 Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Mon, 13 Nov 2023 00:56:04 -0800 Subject: [PATCH 54/91] Prettify test cache file --- .../cache/cache-test-files/cache-unrecognized-entities.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/msal-node/test/cache/cache-test-files/cache-unrecognized-entities.json b/lib/msal-node/test/cache/cache-test-files/cache-unrecognized-entities.json index 30392da91b..1f96041401 100644 --- a/lib/msal-node/test/cache/cache-test-files/cache-unrecognized-entities.json +++ b/lib/msal-node/test/cache/cache-test-files/cache-unrecognized-entities.json @@ -16,7 +16,9 @@ "home_account_id": "uid.utid", "authority_type": "MSSTS", "client_info": "eyJ1aWQiOiJ1aWQiLCAidXRpZCI6InV0aWQifQ==", - "tenantProfiles": ["{\"tenantId\":\"utid\",\"localAccountId\":\"object1234\",\"isHomeTenant\":true}"] + "tenantProfiles": [ + "{\"tenantId\":\"utid\",\"localAccountId\":\"object1234\",\"isHomeTenant\":true}" + ] } }, "RefreshToken": { From 3ea2dd8d7dd39cf429b4d64e81379f93b331d208 Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Mon, 13 Nov 2023 15:03:26 -0800 Subject: [PATCH 55/91] Fix b2c tests by using msal-node serializer instead of outdated e2e-test-utils serializer --- samples/e2eTestUtils/src/Deserializer.ts | 191 ------------------ .../e2eTestUtils/src/NodeCacheTestUtils.ts | 4 +- samples/e2eTestUtils/src/Serializer.ts | 146 ------------- .../msal-node-samples/silent-flow/index.js | 18 +- 4 files changed, 14 insertions(+), 345 deletions(-) delete mode 100644 samples/e2eTestUtils/src/Deserializer.ts delete mode 100644 samples/e2eTestUtils/src/Serializer.ts diff --git a/samples/e2eTestUtils/src/Deserializer.ts b/samples/e2eTestUtils/src/Deserializer.ts deleted file mode 100644 index 0bed739c4f..0000000000 --- a/samples/e2eTestUtils/src/Deserializer.ts +++ /dev/null @@ -1,191 +0,0 @@ -/* - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -/** - * This class deserializes cache entities read from the file into in memory object types defined internally - */ -export class Deserializer { - /** - * Parse the JSON blob in memory and deserialize the content - * @param cachedJson - */ - static deserializeJSONBlob(jsonFile: string): any { - const deserializedCache = !jsonFile ? {} : JSON.parse(jsonFile); - return deserializedCache; - } - - static toObject(obj: any, json: any): T { - for (const propertyName in json) { - obj[propertyName] = json[propertyName]; - } - return obj; - } - - /** - * Deserializes accounts to AccountEntity objects - * @param accounts - */ - static deserializeAccounts(accounts: Record): any { - const accountObjects: any = {}; - if (accounts) { - Object.keys(accounts).map(function (key) { - const serializedAcc = accounts[key]; - const mappedAcc = { - homeAccountId: serializedAcc.home_account_id, - environment: serializedAcc.environment, - realm: serializedAcc.realm, - localAccountId: serializedAcc.local_account_id, - username: serializedAcc.username, - authorityType: serializedAcc.authority_type, - name: serializedAcc.name, - clientInfo: serializedAcc.client_info, - lastModificationTime: serializedAcc.last_modification_time, - lastModificationApp: serializedAcc.last_modification_app, - }; - const account = {}; - Deserializer.toObject(account, mappedAcc); - accountObjects[key] = account; - }); - } - - return accountObjects; - } - - /** - * Deserializes id tokens to IdTokenEntity objects - * @param idTokens - */ - static deserializeIdTokens(idTokens: Record): any { - const idObjects: any = {}; - if (idTokens) { - Object.keys(idTokens).map(function (key) { - const serializedIdT = idTokens[key]; - const mappedIdT = { - homeAccountId: serializedIdT.home_account_id, - environment: serializedIdT.environment, - credentialType: serializedIdT.credential_type, - clientId: serializedIdT.client_id, - secret: serializedIdT.secret, - realm: serializedIdT.realm, - }; - const idToken = {}; - Deserializer.toObject(idToken, mappedIdT); - idObjects[key] = idToken; - }); - } - return idObjects; - } - - /** - * Deserializes access tokens to AccessTokenEntity objects - * @param accessTokens - */ - static deserializeAccessTokens(accessTokens: Record): any { - const atObjects: any = {}; - if (accessTokens) { - Object.keys(accessTokens).map(function (key) { - const serializedAT = accessTokens[key]; - const mappedAT = { - homeAccountId: serializedAT.home_account_id, - environment: serializedAT.environment, - credentialType: serializedAT.credential_type, - clientId: serializedAT.client_id, - secret: serializedAT.secret, - realm: serializedAT.realm, - target: serializedAT.target, - cachedAt: serializedAT.cached_at, - expiresOn: serializedAT.expires_on, - extendedExpiresOn: serializedAT.extended_expires_on, - refreshOn: serializedAT.refresh_on, - keyId: serializedAT.key_id, - tokenType: serializedAT.token_type, - requestedClaims: serializedAT.requestedClaims, - requestedClaimsHash: serializedAT.requestedClaimsHash, - userAssertionHash: serializedAT.userAssertionHash, - }; - const accessToken: any = {}; - Deserializer.toObject(accessToken, mappedAT); - atObjects[key] = accessToken; - }); - } - - return atObjects; - } - - /** - * Deserializes refresh tokens to RefreshTokenEntity objects - * @param refreshTokens - */ - static deserializeRefreshTokens(refreshTokens: Record): any { - const rtObjects: any = {}; - if (refreshTokens) { - Object.keys(refreshTokens).map(function (key) { - const serializedRT = refreshTokens[key]; - const mappedRT = { - homeAccountId: serializedRT.home_account_id, - environment: serializedRT.environment, - credentialType: serializedRT.credential_type, - clientId: serializedRT.client_id, - secret: serializedRT.secret, - familyId: serializedRT.family_id, - target: serializedRT.target, - realm: serializedRT.realm, - }; - const refreshToken = {}; - Deserializer.toObject(refreshToken, mappedRT); - rtObjects[key] = refreshToken; - }); - } - - return rtObjects; - } - - /** - * Deserializes appMetadata to AppMetaData objects - * @param appMetadata - */ - static deserializeAppMetadata(appMetadata: Record): any { - const appMetadataObjects: any = {}; - if (appMetadata) { - Object.keys(appMetadata).map(function (key) { - const serializedAmdt = appMetadata[key]; - const mappedAmd = { - clientId: serializedAmdt.client_id, - environment: serializedAmdt.environment, - familyId: serializedAmdt.family_id, - }; - const amd = {}; - Deserializer.toObject(amd, mappedAmd); - appMetadataObjects[key] = amd; - }); - } - - return appMetadataObjects; - } - - /** - * Deserialize an inMemory Cache - * @param jsonCache - */ - static deserializeAllCache(jsonCache: any): any { - return { - accounts: jsonCache.Account - ? this.deserializeAccounts(jsonCache.Account) - : {}, - idTokens: jsonCache.IdToken - ? this.deserializeIdTokens(jsonCache.IdToken) - : {}, - accessTokens: jsonCache.AccessToken - ? this.deserializeAccessTokens(jsonCache.AccessToken) - : {}, - refreshTokens: jsonCache.RefreshToken - ? this.deserializeRefreshTokens(jsonCache.RefreshToken) - : {}, - appMetadata: jsonCache.AppMetadata - ? this.deserializeAppMetadata(jsonCache.AppMetadata) - : {}, - }; - } -} diff --git a/samples/e2eTestUtils/src/NodeCacheTestUtils.ts b/samples/e2eTestUtils/src/NodeCacheTestUtils.ts index 12a522e96a..d6465f85e5 100644 --- a/samples/e2eTestUtils/src/NodeCacheTestUtils.ts +++ b/samples/e2eTestUtils/src/NodeCacheTestUtils.ts @@ -1,6 +1,6 @@ import fs from "fs"; -import { Deserializer } from "./Deserializer"; -import { Serializer } from "./Serializer"; +import { Deserializer } from "../../../lib/msal-node/src/cache/serializer/Deserializer"; +import { Serializer } from "../../../lib/msal-node/src/cache/serializer/Serializer"; export type tokenMap = { idTokens: any[]; diff --git a/samples/e2eTestUtils/src/Serializer.ts b/samples/e2eTestUtils/src/Serializer.ts deleted file mode 100644 index fb7f4287a0..0000000000 --- a/samples/e2eTestUtils/src/Serializer.ts +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -export class Serializer { - /** - * serialize the JSON blob - * @param data - */ - static serializeJSONBlob(data: any): string { - return JSON.stringify(data); - } - - /** - * Serialize Accounts - * @param accCache - */ - static serializeAccounts(accCache: any): Record { - const accounts: Record = {}; - Object.keys(accCache).map(function (key) { - const accountEntity = accCache[key]; - accounts[key] = { - home_account_id: accountEntity.homeAccountId, - environment: accountEntity.environment, - realm: accountEntity.realm, - local_account_id: accountEntity.localAccountId, - username: accountEntity.username, - authority_type: accountEntity.authorityType, - name: accountEntity.name, - client_info: accountEntity.clientInfo, - last_modification_time: accountEntity.lastModificationTime, - last_modification_app: accountEntity.lastModificationApp, - }; - }); - - return accounts; - } - - /** - * Serialize IdTokens - * @param idTCache - */ - static serializeIdTokens(idTCache: any): Record { - const idTokens: Record = {}; - Object.keys(idTCache).map(function (key) { - const idTEntity = idTCache[key]; - idTokens[key] = { - home_account_id: idTEntity.homeAccountId, - environment: idTEntity.environment, - credential_type: idTEntity.credentialType, - client_id: idTEntity.clientId, - secret: idTEntity.secret, - realm: idTEntity.realm, - }; - }); - - return idTokens; - } - - /** - * Serializes AccessTokens - * @param atCache - */ - static serializeAccessTokens(atCache: any): Record { - const accessTokens: Record = {}; - Object.keys(atCache).map(function (key) { - const atEntity = atCache[key]; - accessTokens[key] = { - home_account_id: atEntity.homeAccountId, - environment: atEntity.environment, - credential_type: atEntity.credentialType, - client_id: atEntity.clientId, - secret: atEntity.secret, - realm: atEntity.realm, - target: atEntity.target, - cached_at: atEntity.cachedAt, - expires_on: atEntity.expiresOn, - extended_expires_on: atEntity.extendedExpiresOn, - refresh_on: atEntity.refreshOn, - key_id: atEntity.keyId, - token_type: atEntity.tokenType, - requestedClaims: atEntity.requestedClaims, - requestedClaimsHash: atEntity.requestedClaimsHash, - userAssertionHash: atEntity.userAssertionHash, - }; - }); - - return accessTokens; - } - - /** - * Serialize refreshTokens - * @param rtCache - */ - static serializeRefreshTokens(rtCache: any): Record { - const refreshTokens: Record = {}; - Object.keys(rtCache).map(function (key) { - const rtEntity = rtCache[key]; - refreshTokens[key] = { - home_account_id: rtEntity.homeAccountId, - environment: rtEntity.environment, - credential_type: rtEntity.credentialType, - client_id: rtEntity.clientId, - secret: rtEntity.secret, - family_id: rtEntity.familyId, - target: rtEntity.target, - realm: rtEntity.realm, - }; - }); - - return refreshTokens; - } - - /** - * Serialize amdtCache - * @param amdtCache - */ - static serializeAppMetadata(amdtCache: any): Record { - const appMetadata: Record = {}; - Object.keys(amdtCache).map(function (key) { - const amdtEntity = amdtCache[key]; - appMetadata[key] = { - client_id: amdtEntity.clientId, - environment: amdtEntity.environment, - family_id: amdtEntity.familyId, - }; - }); - - return appMetadata; - } - - /** - * Serialize the cache - * @param jsonContent - */ - static serializeAllCache(inMemCache: any): any { - return { - Account: this.serializeAccounts(inMemCache.accounts), - IdToken: this.serializeIdTokens(inMemCache.idTokens), - AccessToken: this.serializeAccessTokens(inMemCache.accessTokens), - RefreshToken: this.serializeRefreshTokens(inMemCache.refreshTokens), - AppMetadata: this.serializeAppMetadata(inMemCache.appMetadata), - }; - } -} diff --git a/samples/msal-node-samples/silent-flow/index.js b/samples/msal-node-samples/silent-flow/index.js index 01eee6eeab..15438504f5 100644 --- a/samples/msal-node-samples/silent-flow/index.js +++ b/samples/msal-node-samples/silent-flow/index.js @@ -185,11 +185,17 @@ const getTokenSilent = function (scenarioConfig, clientApplication, port, msalTo // Displays all cached accounts router.get('/allAccounts', async (req, res) => { const accounts = await msalTokenCache.getAllAccounts(); - - if (accounts.length > 0) { - res.render("authenticated", { accounts: JSON.stringify(accounts, null, 4) }) - } else if (accounts.length === 0) { - res.render("authenticated", { accounts: JSON.stringify(accounts), noAccounts: true, showSignInButton: true }); + const formattedAccounts = accounts.map((account) => { + let tenantProfiles = []; + account.tenantProfiles.forEach((profile) => { + tenantProfiles.push(profile); + }); + return { ...account, tenantProfiles }; + }); + if (formattedAccounts.length > 0) { + res.render("authenticated", { accounts: JSON.stringify(formattedAccounts, null, 4) }) + } else if (formattedAccounts.length === 0) { + res.render("authenticated", { accounts: JSON.stringify(formattedAccounts), noAccounts: true, showSignInButton: true }); } else { res.render("authenticated", { failedToGetAccounts: true, showSignInButton: true }) } @@ -231,7 +237,7 @@ if (argv.$0 === "index.js") { authority: config.authOptions.authority, redirectUri: config.authOptions.redirectUri, clientSecret: process.env.CLIENT_SECRET, - knownAuthorities: [config.authOptions.knownAuthorities] + knownAuthorities: config.authOptions.knownAuthorities }, cache: { cachePlugin From be6b7ab6966e752904b739a10cd2f2b80c9b1a8b Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Mon, 13 Nov 2023 17:26:11 -0800 Subject: [PATCH 56/91] Add tests for Authority Utility functions --- .vscode/settings.json | 2 +- .../test/authority/Authority.spec.ts | 117 +++++++++++++++++- .../test/test_kit/StringConstants.ts | 4 + 3 files changed, 120 insertions(+), 3 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index b30b48511c..8f10c9f968 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -19,7 +19,7 @@ "editor.defaultFormatter": "esbenp.prettier-vscode" }, "editor.formatOnSave": true, - "jest.autoRun": "off", + "jest.runMode": "on-demand", "jest.jestCommandLine": "npm test --", "jest.virtualFolders": [ { diff --git a/lib/msal-common/test/authority/Authority.spec.ts b/lib/msal-common/test/authority/Authority.spec.ts index 07b604f48c..ccc6dd0411 100644 --- a/lib/msal-common/test/authority/Authority.spec.ts +++ b/lib/msal-common/test/authority/Authority.spec.ts @@ -1,4 +1,9 @@ -import { Authority } from "../../src/authority/Authority"; +import { + Authority, + buildStaticAuthorityOptions, + formatAuthorityUri, + getTenantFromAuthorityString, +} from "../../src/authority/Authority"; import { INetworkModule, NetworkRequestOptions, @@ -15,6 +20,8 @@ import { import { ClientConfigurationErrorMessage, ClientConfigurationError, + createClientConfigurationError, + ClientConfigurationErrorCodes, } from "../../src/error/ClientConfigurationError"; import { MockStorageClass, mockCrypto } from "../client/ClientTestUtils"; import { @@ -22,7 +29,10 @@ import { createClientAuthError, ClientAuthErrorCodes, } from "../../src/error/ClientAuthError"; -import { AuthorityOptions } from "../../src/authority/AuthorityOptions"; +import { + AuthorityOptions, + StaticAuthorityOptions, +} from "../../src/authority/AuthorityOptions"; import { ProtocolMode } from "../../src/authority/ProtocolMode"; import { AuthorityMetadataEntity } from "../../src/cache/entities/AuthorityMetadataEntity"; import { OpenIdConfigResponse } from "../../src/authority/OpenIdConfigResponse"; @@ -2727,4 +2737,107 @@ describe("Authority.ts Class Unit Tests", () => { expect(regionalResponse.end_session_endpoint).toBeUndefined(); }); }); + + describe("getTenantFromAuthorityString", () => { + it("returns tenantId if authority is a tenant-specific authority", () => { + expect( + getTenantFromAuthorityString(TEST_CONFIG.tenantedValidAuthority) + ).toBe(TEST_CONFIG.MSAL_TENANT_ID); + }); + it("returns undefined if authority is a named authority (common, organizations, consumers", () => { + expect( + getTenantFromAuthorityString(TEST_CONFIG.validAuthority) + ).toBeUndefined(); + expect( + getTenantFromAuthorityString(TEST_CONFIG.organizationsAuthority) + ).toBeUndefined(); + expect( + getTenantFromAuthorityString(TEST_CONFIG.consumersAuthority) + ).toBeUndefined(); + }); + }); + + describe("formatAuthorityUri", () => { + it("returns the same authority URL if it already ends with a forward slash", () => { + const authorityUrl = "https://login.microsoftonline.com/common/"; + expect(formatAuthorityUri(authorityUrl)).toBe(authorityUrl); + }); + + it("appends forward slash if authority URL does not end with a forward slash", () => { + const authorityUrl = "https://login.microsoftonline.com/common"; + const formattedAuthorityUrl = authorityUrl + "/"; + expect(formatAuthorityUri(authorityUrl)).toBe( + formattedAuthorityUrl + ); + }); + }); + + describe("buildStaticAuthorityOptions", () => { + const fullAuthorityOptions: Partial = { + authority: TEST_CONFIG.validAuthority, + knownAuthorities: [TEST_CONFIG.validAuthority], + cloudDiscoveryMetadata: TEST_CONFIG.CLOUD_DISCOVERY_METADATA, + }; + + const matchStaticAuthorityOptions: StaticAuthorityOptions = { + canonicalAuthority: TEST_CONFIG.validAuthority + "/", + knownAuthorities: [TEST_CONFIG.validAuthority], + cloudDiscoveryMetadata: JSON.parse( + TEST_CONFIG.CLOUD_DISCOVERY_METADATA + ), + }; + + it("correctly builds static authority options when all optional fields are correctly provided", () => { + const staticAuthorityOptions = + buildStaticAuthorityOptions(fullAuthorityOptions); + expect(staticAuthorityOptions).toEqual(matchStaticAuthorityOptions); + }); + + it("doesn't set canonicalAuthority if authority is not provided", () => { + const { authority, ...partialAuthorityOptions } = + fullAuthorityOptions; + const staticAuthorityOptions = buildStaticAuthorityOptions( + partialAuthorityOptions + ); + expect(staticAuthorityOptions.canonicalAuthority).toBeUndefined(); + }); + + it("doesn't set knownAuthorities if knownAuthorities array is not provided", () => { + const { knownAuthorities, ...partialAuthorityOptions } = + fullAuthorityOptions; + const staticAuthorityOptions = buildStaticAuthorityOptions( + partialAuthorityOptions + ); + expect(staticAuthorityOptions.knownAuthorities).toBeUndefined(); + }); + + it("doesn't set cloudDiscoveryMetadata if cloudDiscoveryMetadata string is not provided", () => { + const { cloudDiscoveryMetadata, ...partialAuthorityOptions } = + fullAuthorityOptions; + const staticAuthorityOptions = buildStaticAuthorityOptions( + partialAuthorityOptions + ); + expect( + staticAuthorityOptions.cloudDiscoveryMetadata + ).toBeUndefined(); + }); + + it("throws if cloudDiscoveryMetadata string is not valid JSON", () => { + const invalidCloudDiscoveryMetadata = "this-is-not-valid-json"; + const invalidCloudDiscoveryMetadataOptions: Partial = + { + ...fullAuthorityOptions, + cloudDiscoveryMetadata: invalidCloudDiscoveryMetadata, + }; + expect(() => { + buildStaticAuthorityOptions( + invalidCloudDiscoveryMetadataOptions + ); + }).toThrow( + createClientConfigurationError( + ClientConfigurationErrorCodes.invalidCloudDiscoveryMetadata + ) + ); + }); + }); }); diff --git a/lib/msal-common/test/test_kit/StringConstants.ts b/lib/msal-common/test/test_kit/StringConstants.ts index 24c3b407fe..d7bbf94359 100644 --- a/lib/msal-common/test/test_kit/StringConstants.ts +++ b/lib/msal-common/test/test_kit/StringConstants.ts @@ -186,6 +186,10 @@ export const TEST_CONFIG = { validAuthority: TEST_URIS.DEFAULT_INSTANCE + "common", validAuthorityHost: "login.microsoftonline.com", alternateValidAuthority: TEST_URIS.ALTERNATE_INSTANCE + "common", + tenantedValidAuthority: + TEST_URIS.DEFAULT_INSTANCE + "3338040d-6c67-4c5b-b112-36a304b66dad", + organizationsAuthority: TEST_URIS.DEFAULT_INSTANCE + "organizations", + consumersAuthority: TEST_URIS.DEFAULT_INSTANCE + "consumers", ADFS_VALID_AUTHORITY: "https://on.prem/adfs", DSTS_VALID_AUTHORITY: "https://domain.dsts.subdomain/dstsv2/tenant", b2cValidAuthority: From bf45c127fef658c03d4998f256036c718993830e Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Mon, 13 Nov 2023 18:00:47 -0800 Subject: [PATCH 57/91] Increase test coverage in CacheManager --- .../test/cache/CacheManager.spec.ts | 51 +++++++++++++++++-- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/lib/msal-common/test/cache/CacheManager.spec.ts b/lib/msal-common/test/cache/CacheManager.spec.ts index c3f7b05828..cdd1f16df3 100644 --- a/lib/msal-common/test/cache/CacheManager.spec.ts +++ b/lib/msal-common/test/cache/CacheManager.spec.ts @@ -305,6 +305,10 @@ describe("CacheManager.ts test cases", () => { buildAccountFromIdTokenClaims(ID_TOKEN_CLAIMS).getAccountInfo(); const account2 = buildAccountFromIdTokenClaims(ID_TOKEN_ALT_CLAIMS).getAccountInfo(); + it("getAllAccounts returns an empty array if there are no accounts in the cache", () => { + mockCache.clearCache(); + expect(mockCache.cacheManager.getAllAccounts()).toHaveLength(0); + }); it("getAllAccounts (gets all AccountInfo objects)", async () => { const accounts = mockCache.cacheManager.getAllAccounts(); @@ -592,14 +596,32 @@ describe("CacheManager.ts test cases", () => { ); const mainIdTokenKey = CacheHelpers.generateCredentialKey(mainIdTokenEntity); + + const filter = { + homeAccountId: multiTenantAccount.homeAccountId, + }; // Remove main ID token mockCache.cacheManager.removeIdToken(mainIdTokenKey); const resultAccount = - mockCache.cacheManager.getAccountInfoFilteredBy({ - homeAccountId: multiTenantAccount.homeAccountId, - }); + mockCache.cacheManager.getAccountInfoFilteredBy(filter); expect(resultAccount).not.toBeNull(); expect(resultAccount?.tenantId).toBe(GUEST_ID_TOKEN_CLAIMS.tid); + + const allAccountsReversed = mockCache.cacheManager + .getAllAccounts() + .reverse(); + + jest.spyOn( + CacheManager.prototype, + "getAllAccounts" + ).mockReturnValueOnce(allAccountsReversed); + + const reversedResultAccount = + mockCache.cacheManager.getAccountInfoFilteredBy(filter); + expect(reversedResultAccount).not.toBeNull(); + expect(reversedResultAccount?.tenantId).toBe( + GUEST_ID_TOKEN_CLAIMS.tid + ); }); it("returns account matching filter with isHomeTenant = true", () => { @@ -614,6 +636,29 @@ describe("CacheManager.ts test cases", () => { }); }); + describe("getBaseAccountInfo", () => { + it("returns base account regardless of tenantId", () => { + const multiTenantAccount = buildAccountFromIdTokenClaims( + ID_TOKEN_CLAIMS, + [GUEST_ID_TOKEN_CLAIMS] + ).getAccountInfo(); + const resultAccount = mockCache.cacheManager.getBaseAccountInfo({ + homeAccountId: multiTenantAccount.homeAccountId, + tenantId: GUEST_ID_TOKEN_CLAIMS.tid, + }); + + expect(resultAccount).toEqual(multiTenantAccount); + }); + + it("returns null if no account matches filter", () => { + expect( + mockCache.cacheManager.getBaseAccountInfo({ + homeAccountId: "inexistent-homeaccountid", + }) + ).toBeNull(); + }); + }); + it("getAccount (gets one AccountEntity object)", async () => { const ac = new AccountEntity(); ac.homeAccountId = "someUid.someUtid"; From 01d37936254c3040ab195621a276d3f935338656 Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Mon, 13 Nov 2023 18:07:53 -0800 Subject: [PATCH 58/91] Add TokenClaims utilties tests --- .../test/account/TokenClaims.spec.ts | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 lib/msal-common/test/account/TokenClaims.spec.ts diff --git a/lib/msal-common/test/account/TokenClaims.spec.ts b/lib/msal-common/test/account/TokenClaims.spec.ts new file mode 100644 index 0000000000..71f84aff3c --- /dev/null +++ b/lib/msal-common/test/account/TokenClaims.spec.ts @@ -0,0 +1,27 @@ +import { getTenantIdFromIdTokenClaims } from "../../src/account/TokenClaims"; + +describe("TokenClaims Utilities Unit Tests", () => { + it("returns null if no claims are passed", () => { + expect(getTenantIdFromIdTokenClaims()).toBeNull(); + }); + + it("returns null if tid, tfp, and acr claims are not present", () => { + expect(getTenantIdFromIdTokenClaims({})).toBeNull(); + }); + + it("returns tid claim if present", () => { + expect(getTenantIdFromIdTokenClaims({ tid: "tid" })).toBe("tid"); + }); + + it("returns tfp claim if present", () => { + expect(getTenantIdFromIdTokenClaims({ tfp: "tfp" })).toBe("tfp"); + }); + + it("returns acr claim if present", () => { + expect(getTenantIdFromIdTokenClaims({ acr: "acr" })).toBe("acr"); + }); + + it("correctly downcases the tenantId", () => { + expect(getTenantIdFromIdTokenClaims({ tid: "TID" })).toBe("tid"); + }); +}); From 3a62e4e1bdbd4cbaeb7f74ad8539998edd05d5cd Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Mon, 13 Nov 2023 18:22:57 -0800 Subject: [PATCH 59/91] Increase test coverage for AccountEntity --- .../test/cache/entities/AccountEntity.spec.ts | 47 ++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/lib/msal-common/test/cache/entities/AccountEntity.spec.ts b/lib/msal-common/test/cache/entities/AccountEntity.spec.ts index 04065552b0..cfb26004fb 100644 --- a/lib/msal-common/test/cache/entities/AccountEntity.spec.ts +++ b/lib/msal-common/test/cache/entities/AccountEntity.spec.ts @@ -16,16 +16,19 @@ import { TEST_POP_VALUES, PREFERRED_CACHE_ALIAS, TEST_CRYPTO_VALUES, + ID_TOKEN_CLAIMS, + GUEST_ID_TOKEN_CLAIMS, } from "../../test_kit/StringConstants"; import sinon from "sinon"; import { MockStorageClass, mockCrypto } from "../../client/ClientTestUtils"; -import { AccountInfo } from "../../../src/account/AccountInfo"; +import { AccountInfo, TenantProfile } from "../../../src/account/AccountInfo"; import { AuthorityOptions } from "../../../src/authority/AuthorityOptions"; import { ProtocolMode } from "../../../src/authority/ProtocolMode"; import { LogLevel, Logger } from "../../../src/logger/Logger"; import { Authority } from "../../../src/authority/Authority"; import { AuthorityType } from "../../../src/authority/AuthorityType"; import { TokenClaims } from "../../../src"; +import { buildAccountFromIdTokenClaims } from "../MockCache"; const cryptoInterface: ICrypto = { createNewGuid(): string { @@ -357,6 +360,48 @@ describe("AccountEntity.ts Unit Tests", () => { expect(AccountEntity.isAccountEntity(mockIdTokenEntity)).toEqual(false); }); + it("getAccountInfo correctly deserializes tenantProfiles in an account entity", () => { + const accountEntity = buildAccountFromIdTokenClaims(ID_TOKEN_CLAIMS, [ + GUEST_ID_TOKEN_CLAIMS, + ]); + + const tenantProfiles = new Map(); + + accountEntity.tenantProfiles?.forEach((tenantProfile) => { + tenantProfiles.set(tenantProfile.tenantId, tenantProfile); + }); + + const accountInfo = accountEntity.getAccountInfo(); + expect(accountInfo.tenantProfiles).toBeDefined(); + expect(accountInfo.tenantProfiles?.size).toBe(2); + expect(accountInfo.tenantProfiles).toMatchObject(tenantProfiles); + }); + + it("getAccountInfo creates a new tenantProfiles map if AccountEntity doesn't have a tenantProfiles array", () => { + const accountEntity = buildAccountFromIdTokenClaims(ID_TOKEN_CLAIMS); + accountEntity.tenantProfiles = undefined; + + const accountInfo = accountEntity.getAccountInfo(); + expect(accountInfo.tenantProfiles).toBeDefined(); + expect(accountInfo.tenantProfiles?.size).toBe(0); + expect(accountInfo.tenantProfiles).toMatchObject( + new Map() + ); + }); + + it("isSingleTenant returns true if AccountEntity doesn't have a tenantProfiles array", () => { + const accountEntity = buildAccountFromIdTokenClaims(ID_TOKEN_CLAIMS); + accountEntity.tenantProfiles = undefined; + + expect(accountEntity.isSingleTenant()).toBe(true); + }); + + it("isSingleTenant returns false if AccountEntity has a tenantProfiles array", () => { + const accountEntity = buildAccountFromIdTokenClaims(ID_TOKEN_CLAIMS); + + expect(accountEntity.isSingleTenant()).toBe(false); + }); + describe("accountInfoIsEqual()", () => { let acc: AccountEntity; let idTokenClaims: TokenClaims; From 932594c9f70f244772ff6b656314aa7b9cdd82ad Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Tue, 14 Nov 2023 11:01:04 -0800 Subject: [PATCH 60/91] Add multi-tenant docs first draft --- lib/msal-common/docs/multi-tenant-accounts.md | 173 ++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 lib/msal-common/docs/multi-tenant-accounts.md diff --git a/lib/msal-common/docs/multi-tenant-accounts.md b/lib/msal-common/docs/multi-tenant-accounts.md new file mode 100644 index 0000000000..fd5dc58cd4 --- /dev/null +++ b/lib/msal-common/docs/multi-tenant-accounts.md @@ -0,0 +1,173 @@ +# Multi-tenant Support in MSAL JS SDKs + +> This is an advanced document regarding the handling of accounts to acquire tokens across tenants in the `msal-browser` and `msal-node` SDKs. For basic account information, please review the [Accounts](./accounts.md) document. + +## Table of Contents + +- [Multi-tenant Accounts](#multi-tenant-accounts) +- [Tenant Profiles](#tenant-profiles) +- [Usage](#usage) + - [Authenticating with Multiple Tenants](#authenticating-with-multiple-tenants) + - [Acquiring Tokens Silently for Multiple Tenants](#acquiring-tokens-silently-for-multiple-tenants) + - [getAllAccounts with Multiple Tenants](#getallaccounts-with-multiple-tenants) + - [Multi-tenant Logout](#multi-tenant-logout) + +## Multi-tenant Accounts + +MSAL supports the acquisition and caching of access and ID tokens across multiple tenants. In order to facilitate this, MSAL utilizes multi-tenant accounts. Multi-tenant accounts are `AccountInfo` objects that are returned with the tenant-specific data matching the context of the `acquireToken` or `getAccount` API call. + +In addition to tenant-specific account data, multi-tenant accounts also contain a `Map` of the **tenant profiles** for the account corresponding to each tenant the user has authenticated with. + +## Tenant Profiles + +Conceptually, a tenant profile is the record of an account in a specific tenant. In MSAL JS SDKs, `TenantProfile` objects contain the subset of the `AccountInfo` properties that vary by tenant. They are created by using the claims from the ID token issued by each tenant the user authenticates with. + +`AccountInfo` objects returned from `acquireToken` and `getAccount` APIs contain a `Map` object called `tenantProfiles` where the key is the tenant ID and the value is the `TenantProfile` for that account in that tenant. + +MSAL uses these `TenantProfile` objects to match and build the tenant-specific `AccountInfo` objects required through all of MSAL's flows. They can also be used by client applications for different purposes such as facilitating account selection logic and displaying account data for the user across tenants. + +> Warning: While MSAL can return a tenant-specific `AccountInfo` object for each tenant profile, tenant profiles are not actually different accounts, they are only representations of the same account in the different tenants the account has authentiated with. **If a user uses different accounts to authenticate to each tenant, these will not be linked as tenant profiles of eachother and will be treated as completely different accounts that cannot be used to access eachother's tokens**. + +## Usage + +### Authenticating with Multiple Tenants + +In order to authenticate with multiple tenants, you can use either `login` or `acquireToken` APIs normally, only setting the authority to the particular tenant you are requesting tokens for. + +> Note: For `login`/`acquireToken` interactive APIs, the tenant context is set from the authority's `tenantId`. + +```javascript +const msalConfig = { + auth: { + clientId: "ENTER_CLIENT_ID", + authority: "https://login.microsoftonline.com/HOME_TENANT", // This is the authority that MSAL will default to for requests that don't specify their own authority. + }, +}; + +const myMSALObj = new PublicClientApplication(msalConfig); + +// handleRedirectPromise has to be called to resume redirect requests +myMSALObj + .handleRedirectPromise() + .then(handleResponse) + .catch((err) => { + console.error(err); + }); + +// Authority isn't specified because it is already the default set in the MSAL Config object +const homeTenantRequest = { + scopes: ["HOME_TENANT_SCOPE"], +}; + +// Guest tenant authority overrides the MSAL config authority, meaning the user can authenticate with the tenant they are a guest in and acquire tokens for it +const guestTenantRequest = { + scopes: ["GUEST_TENANT_SCOPE"], + authority: "https://login.microsoftonline.com/GUEST_TENANT", +}; + +const homeTenantAuthResponse = await myMSALObj.loginPopup(homeTenantRequest); +// Get the home tenant account from the AuthenticationResult +const homeTenantAccount = homeTenantAuthResponse.account; + +const guestTenantAuthResponse = await myMSALObj.loginPopup(homeTenantRequest); +// Get the guest tenant account from the AuthenticationResult +const guestTenantAccount = guestTenantAuthResponse.account; +``` + +Assuming the user logs into both tenants with the same account, once both login requests are successfully completed and the user has authenticated with each tenant, the cache will contain: + +- 2 ID tokens (one per tenant) +- 2 Access tokens (one per tenant) +- 1 Refresh token (one per account, shared across tenants) +- 1 AccountEntity containing both tenant profiles. The `homeTenantAccount` and `guestTenantAccount` objects shown above are both created from the same cached account entity augmented by the claims in their respective ID tokens. + +### Acquiring Tokens Silently for Multiple Tenants + +In order to acquire cached tokens for a particular tenant, the `AccountInfo` object for that particular tenant must be passed into `acquireTokenSilent`. In order to get the correct account to pass into acquire token silent, you can either: + +- Use the `AccountInfo` object returned from an interactive API call to that tenant: + +```javascript +// Original interactive call to home tenant +const homeTenantAuthResponse = await myMSALObj.loginPopup(homeTenantRequest); +// Get the home tenant account from the AuthenticationResult +const homeTenantAccount = homeTenantAuthResponse.account; +. +. +. +// Acquire a home tenant access token silently +const homeTenantSilentResponse = await myMSALObj.acquireTokenSilent({ + account: homeTenantAccount, + ...homeTenantRequest, +}); + + +// Original interactive call to guest tenant +const guestTenantAuthResponse = await myMSALObj.loginPopup(homeTenantRequest); +// Get the guest tenant account from the AuthenticationResult +const guestTenantAccount = guestTenantAuthResponse.account; + +// Acquire a guest tenant access token silently +const guestTenantSilentResponse = await myMSALObj.acquireTokenSilent({ + account: guestTenantAccount, + ...guestTenantRequest, +}); +``` + +- Use the `getAccount()` API with the desired `tenantId` as a filter and use the `AccountInfo` object returned: + +```javascript +const homeAccountId = "HOME_ACCOUNT_ID"; // Shared across tenant profiles +const homeTenantId = "HOME_TENANT_ID"; +const guestTenantId = "GUEST_TENANT_ID"; + +// Get the home tenant account +const homeTenantAccount = myMSALObj.getAccount({ + homeAccountId: homeAccountId, + tenantId: homeTenantId, +}); + +// Get home tenant token +const homeTenantAuthResponse = await myMSALObj.acquireTokenSilent({ + account: homeTenantAccount, + ...homeTenantRequest, +}); +``` + +### getAllAccounts with Multiple Tenants + +When any account in the MSAL cache contains multiple tenant profiles, `getAllAccounts()` is expected to return a full `AccountInfo` object for each tenant profile that satisfies the filter provided. If no filter is provided, every tenant profile for every account will be returned as a full `AccountInfo` object by default. This means that even if there is only one `AccountEntity` object in the cache, multiple `AccountInfo` objects may be returned from `getAllAccounts`. + +The results of `getAllAccounts` can be "flattened" to only return the home accounts which would each still have a map of their `tenantProfiles`. This can be achieved with the `isHomeTenant` filter. + +The sample code below shows how one can: + +- Filter the result of `getAllAccounts()` using the new optional `accountFilter` parameter to "flatten" the cached accounts into home accounts with a map of their tenant profiles (otherwise getAllAccounts will return the `AccountInfo` object for each tenant profile) +- Extract the desired `TenantProfile` object from the home `AccountInfo` object +- Use the `TenantProfile` object to get the `AccountInfo` object for that tenant profile +- Use the guest tenant account object to acquire cached tokens that belong to it + +```javascript +// When a filter is passed into getAllAccounts, it returns all cached accounts that match the filter. Use the special isHomeTenant filter to get the home accounts only. +const homeAccount = myMSALObj.getAllAccounts({ isHomeTenant: true })[0]; +const tenantId = "GUEST_TENANT_ID"; // This will be the tenant you want to retrieve a cached token for +const guestTenantProfile = homeAccount.tenantProfiles.get(tenantId); + +// TenantProfile is a subset of AccountInfo, so it can be passed whole as an `AccountFilter` +const guestTenantAccount = myMSALObj.getAccount({ ...tenantProfile }); + +const guestTenantAuthResponse = await myMSALObj + .acquireTokenSilent({ ...guestTenantRequest, account: guestTenantAccount }) + .catch(async (error) => { + if (error instanceof msal.InteractionRequiredAuthError) { + // fallback to interaction when silent call fails + myMSALObj.acquireTokenRedirect(request); + } else { + console.error(error); + } + }); +``` + +## Multi-tenant Logout + +Calling the `logout` API with an account object passed in will result in all tenant profiles corresponding to that account being logged out and all of their account information and auth artifacts being removed from the cache. From 17301aaf7337c2ca0f4551de01b5bb02fe8efedb Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Tue, 14 Nov 2023 11:09:27 -0800 Subject: [PATCH 61/91] Update multi-tenant docs --- lib/msal-common/docs/multi-tenant-accounts.md | 47 ++++++++++++------- 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/lib/msal-common/docs/multi-tenant-accounts.md b/lib/msal-common/docs/multi-tenant-accounts.md index fd5dc58cd4..55f337887f 100644 --- a/lib/msal-common/docs/multi-tenant-accounts.md +++ b/lib/msal-common/docs/multi-tenant-accounts.md @@ -14,13 +14,13 @@ ## Multi-tenant Accounts -MSAL supports the acquisition and caching of access and ID tokens across multiple tenants. In order to facilitate this, MSAL utilizes multi-tenant accounts. Multi-tenant accounts are `AccountInfo` objects that are returned with the tenant-specific data matching the context of the `acquireToken` or `getAccount` API call. +MSAL supports the acquisition and caching of access and ID tokens across multiple tenants. In order to facilitate this, MSAL utilizes multi-tenant accounts. Multi-tenant accounts are [AccountInfo](https://azuread.github.io/microsoft-authentication-library-for-js/ref/types/_azure_msal_common.AccountInfo.html) objects that are returned with the tenant-specific data matching the context of the `acquireToken` or `getAccount` API call. In addition to tenant-specific account data, multi-tenant accounts also contain a `Map` of the **tenant profiles** for the account corresponding to each tenant the user has authenticated with. ## Tenant Profiles -Conceptually, a tenant profile is the record of an account in a specific tenant. In MSAL JS SDKs, `TenantProfile` objects contain the subset of the `AccountInfo` properties that vary by tenant. They are created by using the claims from the ID token issued by each tenant the user authenticates with. +Conceptually, a tenant profile is the record of an account in a specific tenant. In MSAL JS SDKs, [TenantProfile](https://azuread.github.io/microsoft-authentication-library-for-js/ref/types/_azure_msal_common.TenantProfile.html) objects contain the subset of the `AccountInfo` properties that vary by tenant. They are created by using the claims from the ID token issued by each tenant the user authenticates with. `AccountInfo` objects returned from `acquireToken` and `getAccount` APIs contain a `Map` object called `tenantProfiles` where the key is the tenant ID and the value is the `TenantProfile` for that account in that tenant. @@ -136,13 +136,13 @@ const homeTenantAuthResponse = await myMSALObj.acquireTokenSilent({ ### getAllAccounts with Multiple Tenants -When any account in the MSAL cache contains multiple tenant profiles, `getAllAccounts()` is expected to return a full `AccountInfo` object for each tenant profile that satisfies the filter provided. If no filter is provided, every tenant profile for every account will be returned as a full `AccountInfo` object by default. This means that even if there is only one `AccountEntity` object in the cache, multiple `AccountInfo` objects may be returned from `getAllAccounts`. +When any account in the MSAL cache contains multiple tenant profiles, `getAllAccounts()` is expected to return a full `AccountInfo` object for each tenant profile that satisfies the filter provided. If no filter is provided, every tenant profile for every account will be returned as a full `AccountInfo` object by default. This means that even if there is only one account object in the cache, multiple `AccountInfo` objects may be returned from `getAllAccounts`. -The results of `getAllAccounts` can be "flattened" to only return the home accounts which would each still have a map of their `tenantProfiles`. This can be achieved with the `isHomeTenant` filter. +The results of `getAllAccounts` can be "flattened" to only return the home accounts which would each still have a map of their `tenantProfiles`. This can be achieved by setting the `isHomeTenant` filter to `true`. The opposite, getting only accounts built from guest tenant profiles, can be achieved by setting the `isHomeTenant` filter to `false.` The sample code below shows how one can: -- Filter the result of `getAllAccounts()` using the new optional `accountFilter` parameter to "flatten" the cached accounts into home accounts with a map of their tenant profiles (otherwise getAllAccounts will return the `AccountInfo` object for each tenant profile) +- Filter the result of `getAllAccounts()` using the optional `accountFilter` parameter to "flatten" the cached accounts into home accounts with a map of their tenant profiles (otherwise getAllAccounts will return the `AccountInfo` object for each tenant profile) - Extract the desired `TenantProfile` object from the home `AccountInfo` object - Use the `TenantProfile` object to get the `AccountInfo` object for that tenant profile - Use the guest tenant account object to acquire cached tokens that belong to it @@ -151,21 +151,32 @@ The sample code below shows how one can: // When a filter is passed into getAllAccounts, it returns all cached accounts that match the filter. Use the special isHomeTenant filter to get the home accounts only. const homeAccount = myMSALObj.getAllAccounts({ isHomeTenant: true })[0]; const tenantId = "GUEST_TENANT_ID"; // This will be the tenant you want to retrieve a cached token for + const guestTenantProfile = homeAccount.tenantProfiles.get(tenantId); -// TenantProfile is a subset of AccountInfo, so it can be passed whole as an `AccountFilter` -const guestTenantAccount = myMSALObj.getAccount({ ...tenantProfile }); - -const guestTenantAuthResponse = await myMSALObj - .acquireTokenSilent({ ...guestTenantRequest, account: guestTenantAccount }) - .catch(async (error) => { - if (error instanceof msal.InteractionRequiredAuthError) { - // fallback to interaction when silent call fails - myMSALObj.acquireTokenRedirect(request); - } else { - console.error(error); - } - }); +if (guestTenantProfile) { + // TenantProfile is a subset of AccountInfo, so it can be passed whole as an `AccountFilter` + const guestTenantAccount = myMSALObj.getAccount({ ...tenantProfile }); + + const guestTenantAuthResponse = await myMSALObj + .acquireTokenSilent({ + ...guestTenantRequest, + account: guestTenantAccount, + }) + .catch(async (error) => { + if (error instanceof msal.InteractionRequiredAuthError) { + // fallback to interaction when silent call fails + myMSALObj.acquireTokenRedirect(request); + } else { + console.error(error); + } + }); +} else { + // Authenticate with guest tenant first + const guestTenantAuthResponse = await myMSALObj.loginPopup( + homeTenantRequest + ); +} ``` ## Multi-tenant Logout From 662b20ab64e149e931fce5831ce431da44927024 Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Tue, 14 Nov 2023 11:12:34 -0800 Subject: [PATCH 62/91] Add multi-tenant reference to node and browser docs --- lib/msal-browser/docs/accounts.md | 2 +- lib/msal-node/docs/accounts.md | 16 +++++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/lib/msal-browser/docs/accounts.md b/lib/msal-browser/docs/accounts.md index d67e937fc4..bf0d81b1fc 100644 --- a/lib/msal-browser/docs/accounts.md +++ b/lib/msal-browser/docs/accounts.md @@ -1,6 +1,6 @@ # Accounts in MSAL Browser -> 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). +> 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). ## Usage diff --git a/lib/msal-node/docs/accounts.md b/lib/msal-node/docs/accounts.md index 3059b716c6..11db3faff3 100644 --- a/lib/msal-node/docs/accounts.md +++ b/lib/msal-node/docs/accounts.md @@ -1,14 +1,14 @@ # Accounts in MSAL Node -> This is the platform-specific Accounts documentation for `msal-node`. For the general documentation of the `AccountInfo` object structure, please visit the `msal-common` [Accounts document](../../msal-common/docs/Accounts.md). +> This is the platform-specific Accounts documentation for `msal-node`. For the general documentation of the `AccountInfo` object structure, please visit the `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). ## Usage The `msal-node` library provides the following different APIs to access cached accounts: -* `getAllAccounts()`: returns all the accounts currently in the cache. An application must choose an account to acquire tokens silently. -* `getAccountByHomeId()`: receives a `homeAccountId` string and returns the matching account from the cache. -* `getAccountByLocalId()`: receives a `localAccountId` string and returns the matching account from the cache. +- `getAllAccounts()`: returns all the accounts currently in the cache. An application must choose an account to acquire tokens silently. +- `getAccountByHomeId()`: receives a `homeAccountId` string and returns the matching account from the cache. +- `getAccountByLocalId()`: receives a `localAccountId` string and returns the matching account from the cache. The following are usage examples for each API: @@ -82,7 +82,9 @@ Once the account and tokens are cached and the application state holds the `home ```javascript async function getResource() { // Find account using homeAccountId or localAccountId built after receiving auth code token response - const account = await msalTokenCache.getAccountByHomeId(app.locals.homeAccountId); // alternativley: await msalTokenCache.getAccountByLocalId(localAccountId) if using localAccountId + const account = await msalTokenCache.getAccountByHomeId( + app.locals.homeAccountId + ); // alternativley: await msalTokenCache.getAccountByLocalId(localAccountId) if using localAccountId // Build silent request const silentRequest = { @@ -102,5 +104,5 @@ async function getResource() { ## Notes -* The current msal-node silent-flow [sample](../../../samples/msal-node-samples/silent-flow) has a working single account scenario that uses `getAccountByHomeId()`. -* If you have a multiple accounts scenario, please modify the [sample](../../../samples/msal-node-samples/silent-flow/index.js) (in `/graphCall` route) to list all cached accounts and choose a specific account. You may also need to customize the related view templates and `handlebars` template params. +- The current msal-node silent-flow [sample](../../../samples/msal-node-samples/silent-flow) has a working single account scenario that uses `getAccountByHomeId()`. +- If you have a multiple accounts scenario, please modify the [sample](../../../samples/msal-node-samples/silent-flow/index.js) (in `/graphCall` route) to list all cached accounts and choose a specific account. You may also need to customize the related view templates and `handlebars` template params. From 30b44887e622803d2374e73bb6191003187e591d Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Tue, 14 Nov 2023 17:38:31 -0800 Subject: [PATCH 63/91] Update lib/msal-browser/test/cache/TestStorageManager.ts Co-authored-by: Thomas Norling --- lib/msal-browser/test/cache/TestStorageManager.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/msal-browser/test/cache/TestStorageManager.ts b/lib/msal-browser/test/cache/TestStorageManager.ts index 3ef0969267..49ad387bbc 100644 --- a/lib/msal-browser/test/cache/TestStorageManager.ts +++ b/lib/msal-browser/test/cache/TestStorageManager.ts @@ -40,11 +40,7 @@ export class TestStorageManager extends CacheManager { removeAccountKeyFromMap(key: string): void { const currentAccounts = this.getAccountKeys(); - const removalIndex = currentAccounts.indexOf(key); - if (removalIndex > -1) { - currentAccounts.splice(removalIndex, 1); - this.store[ACCOUNT_KEYS] = currentAccounts; - } + this.store[ACCOUNT_KEYS] = currentAccounts.filter(entry => entry !== key); } getCachedAccountEntity(key: string): AccountEntity | null { From 82dc1e9563b241cab775cabb699018b1cce4bd4b Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Tue, 14 Nov 2023 17:41:39 -0800 Subject: [PATCH 64/91] Address PR feedback --- .../NativeInteractionClient.ts | 2 +- lib/msal-common/src/account/ClientInfo.ts | 5 ++-- .../src/cache/entities/AccountEntity.ts | 7 ++++-- .../src/client/AuthorizationCodeClient.ts | 4 ++-- .../src/response/ResponseHandler.ts | 2 +- .../test/account/ClientInfo.spec.ts | 24 ++++++++++++------- 6 files changed, 26 insertions(+), 18 deletions(-) diff --git a/lib/msal-browser/src/interaction_client/NativeInteractionClient.ts b/lib/msal-browser/src/interaction_client/NativeInteractionClient.ts index 30e05018bf..e585eec9f5 100644 --- a/lib/msal-browser/src/interaction_client/NativeInteractionClient.ts +++ b/lib/msal-browser/src/interaction_client/NativeInteractionClient.ts @@ -588,7 +588,7 @@ export class NativeInteractionClient extends BaseInteractionClient { const accountInfo: AccountInfo | null = updateAccountTenantProfileData( accountEntity.getAccountInfo(), - undefined, + undefined, // tenantProfile optional idTokenClaims ); diff --git a/lib/msal-common/src/account/ClientInfo.ts b/lib/msal-common/src/account/ClientInfo.ts index 5ab7f978d7..7697b4acb5 100644 --- a/lib/msal-common/src/account/ClientInfo.ts +++ b/lib/msal-common/src/account/ClientInfo.ts @@ -7,7 +7,6 @@ import { createClientAuthError, ClientAuthErrorCodes, } from "../error/ClientAuthError"; -import { ICrypto } from "../crypto/ICrypto"; import { Separators, Constants } from "../utils/Constants"; /** @@ -25,14 +24,14 @@ export type ClientInfo = { */ export function buildClientInfo( rawClientInfo: string, - crypto: ICrypto + base64Decode: (input: string) => string ): ClientInfo { if (!rawClientInfo) { throw createClientAuthError(ClientAuthErrorCodes.clientInfoEmptyError); } try { - const decodedClientInfo: string = crypto.base64Decode(rawClientInfo); + const decodedClientInfo: string = base64Decode(rawClientInfo); return JSON.parse(decodedClientInfo) as ClientInfo; } catch (e) { throw createClientAuthError( diff --git a/lib/msal-common/src/cache/entities/AccountEntity.ts b/lib/msal-common/src/cache/entities/AccountEntity.ts index 121d3e9da6..ffaffc2ac0 100644 --- a/lib/msal-common/src/cache/entities/AccountEntity.ts +++ b/lib/msal-common/src/cache/entities/AccountEntity.ts @@ -159,7 +159,10 @@ export class AccountEntity { let clientInfo: ClientInfo | undefined; if (accountDetails.clientInfo && cryptoObj) { - clientInfo = buildClientInfo(accountDetails.clientInfo, cryptoObj); + clientInfo = buildClientInfo( + accountDetails.clientInfo, + cryptoObj.base64Decode + ); } account.clientInfo = accountDetails.clientInfo; @@ -285,7 +288,7 @@ export class AccountEntity { try { const clientInfo = buildClientInfo( serverClientInfo, - cryptoObj + cryptoObj.base64Decode ); if (clientInfo.uid && clientInfo.utid) { return `${clientInfo.uid}.${clientInfo.utid}`; diff --git a/lib/msal-common/src/client/AuthorizationCodeClient.ts b/lib/msal-common/src/client/AuthorizationCodeClient.ts index 14777d3580..cbcf2e4547 100644 --- a/lib/msal-common/src/client/AuthorizationCodeClient.ts +++ b/lib/msal-common/src/client/AuthorizationCodeClient.ts @@ -254,7 +254,7 @@ export class AuthorizationCodeClient extends BaseClient { try { const clientInfo = buildClientInfo( request.clientInfo, - this.cryptoUtils + this.cryptoUtils.base64Decode ); ccsCredential = { credential: `${clientInfo.uid}${Separators.CLIENT_INFO_SEPARATOR}${clientInfo.utid}`, @@ -421,7 +421,7 @@ export class AuthorizationCodeClient extends BaseClient { try { const clientInfo = buildClientInfo( request.clientInfo, - this.cryptoUtils + this.cryptoUtils.base64Decode ); ccsCred = { credential: `${clientInfo.uid}${Separators.CLIENT_INFO_SEPARATOR}${clientInfo.utid}`, diff --git a/lib/msal-common/src/response/ResponseHandler.ts b/lib/msal-common/src/response/ResponseHandler.ts index 1e1eeadaab..234366add1 100644 --- a/lib/msal-common/src/response/ResponseHandler.ts +++ b/lib/msal-common/src/response/ResponseHandler.ts @@ -610,7 +610,7 @@ export class ResponseHandler { const accountInfo: AccountInfo | null = cacheRecord.account ? updateAccountTenantProfileData( cacheRecord.account.getAccountInfo(), - undefined, + undefined, // tenantProfile optional idTokenClaims ) : null; diff --git a/lib/msal-common/test/account/ClientInfo.spec.ts b/lib/msal-common/test/account/ClientInfo.spec.ts index daada80e54..67eee32423 100644 --- a/lib/msal-common/test/account/ClientInfo.spec.ts +++ b/lib/msal-common/test/account/ClientInfo.spec.ts @@ -80,27 +80,33 @@ describe("ClientInfo.ts Class Unit Tests", () => { ClientAuthError ); - expect(() => buildClientInfo("", cryptoInterface)).toThrowError( - ClientAuthErrorMessage.clientInfoEmptyError.desc - ); - expect(() => buildClientInfo("", cryptoInterface)).toThrowError( - ClientAuthError - ); + expect(() => + buildClientInfo("", cryptoInterface.base64Decode) + ).toThrowError(ClientAuthErrorMessage.clientInfoEmptyError.desc); + expect(() => + buildClientInfo("", cryptoInterface.base64Decode) + ).toThrowError(ClientAuthError); }); it("Throws error if function could not successfully decode ", () => { expect(() => - buildClientInfo("ThisCan'tbeParsed", cryptoInterface) + buildClientInfo( + "ThisCan'tbeParsed", + cryptoInterface.base64Decode + ) ).toThrowError(ClientAuthErrorMessage.clientInfoDecodingError.desc); expect(() => - buildClientInfo("ThisCan'tbeParsed", cryptoInterface) + buildClientInfo( + "ThisCan'tbeParsed", + cryptoInterface.base64Decode + ) ).toThrowError(ClientAuthError); }); it("Succesfully returns decoded client info", () => { const clientInfo = buildClientInfo( TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, - cryptoInterface + cryptoInterface.base64Decode ); expect(clientInfo.uid).toBe(TEST_DATA_CLIENT_INFO.TEST_UID); From d609c6f8799996acba0da0adf801b108248db10b Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Tue, 14 Nov 2023 18:07:59 -0800 Subject: [PATCH 65/91] Update base64Decode in buildClientInfo usages --- .../src/interaction_client/NativeInteractionClient.ts | 2 +- lib/msal-common/src/cache/entities/AccountEntity.ts | 6 +++--- lib/msal-common/src/response/ResponseHandler.ts | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/msal-browser/src/interaction_client/NativeInteractionClient.ts b/lib/msal-browser/src/interaction_client/NativeInteractionClient.ts index 4a778abe97..d00d53a0f8 100644 --- a/lib/msal-browser/src/interaction_client/NativeInteractionClient.ts +++ b/lib/msal-browser/src/interaction_client/NativeInteractionClient.ts @@ -428,7 +428,7 @@ export class NativeInteractionClient extends BaseInteractionClient { authority, homeAccountIdentifier, idTokenClaims, - this.browserCrypto, + this.browserCrypto.base64Decode, response.client_info, idTokenClaims.tid, undefined, diff --git a/lib/msal-common/src/cache/entities/AccountEntity.ts b/lib/msal-common/src/cache/entities/AccountEntity.ts index ffaffc2ac0..e1245186a2 100644 --- a/lib/msal-common/src/cache/entities/AccountEntity.ts +++ b/lib/msal-common/src/cache/entities/AccountEntity.ts @@ -144,7 +144,7 @@ export class AccountEntity { tenantProfiles?: Array; }, authority: Authority, - cryptoObj?: ICrypto + base64Decode?: (input: string) => string ): AccountEntity { const account: AccountEntity = new AccountEntity(); @@ -158,10 +158,10 @@ export class AccountEntity { let clientInfo: ClientInfo | undefined; - if (accountDetails.clientInfo && cryptoObj) { + if (accountDetails.clientInfo && base64Decode) { clientInfo = buildClientInfo( accountDetails.clientInfo, - cryptoObj.base64Decode + base64Decode ); } diff --git a/lib/msal-common/src/response/ResponseHandler.ts b/lib/msal-common/src/response/ResponseHandler.ts index 65ac1ce50d..92de68b6be 100644 --- a/lib/msal-common/src/response/ResponseHandler.ts +++ b/lib/msal-common/src/response/ResponseHandler.ts @@ -433,7 +433,7 @@ export class ResponseHandler { authority, this.homeAccountIdentifier, idTokenClaims, - this.cryptoObj, + this.cryptoObj.base64Decode, serverTokenResponse.client_info, claimsTenantId, authCodePayload, @@ -652,7 +652,7 @@ export function buildAccountToCache( authority: Authority, homeAccountId: string, idTokenClaims: TokenClaims, - cryptoObj: ICrypto, + base64Decode: (input: string) => string, clientInfo?: string, claimsTenantId?: string | null, authCodePayload?: AuthorizationCodePayload, @@ -684,7 +684,7 @@ export function buildAccountToCache( nativeAccountId: nativeAccountId, }, authority, - cryptoObj + base64Decode ); const tenantProfiles = baseAccount.tenantProfiles || []; From 2e09ae5c41112e4965f01db2262c6b02d070b163 Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Tue, 14 Nov 2023 18:20:08 -0800 Subject: [PATCH 66/91] Fix formatting --- lib/msal-browser/test/cache/TestStorageManager.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/msal-browser/test/cache/TestStorageManager.ts b/lib/msal-browser/test/cache/TestStorageManager.ts index 49ad387bbc..14b279be70 100644 --- a/lib/msal-browser/test/cache/TestStorageManager.ts +++ b/lib/msal-browser/test/cache/TestStorageManager.ts @@ -40,7 +40,9 @@ export class TestStorageManager extends CacheManager { removeAccountKeyFromMap(key: string): void { const currentAccounts = this.getAccountKeys(); - this.store[ACCOUNT_KEYS] = currentAccounts.filter(entry => entry !== key); + this.store[ACCOUNT_KEYS] = currentAccounts.filter( + (entry) => entry !== key + ); } getCachedAccountEntity(key: string): AccountEntity | null { From d17fc59d6e83252956d0a98352fbf2745864aafe Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Wed, 15 Nov 2023 10:41:10 -0800 Subject: [PATCH 67/91] Update docs --- lib/msal-common/docs/multi-tenant-accounts.md | 2 +- lib/msal-node/docs/accounts.md | 6 +++--- .../app/multi-tenant/auth.js | 21 ++++++++++++++----- .../app/multi-tenant/authConfig.js | 6 +++--- 4 files changed, 23 insertions(+), 12 deletions(-) diff --git a/lib/msal-common/docs/multi-tenant-accounts.md b/lib/msal-common/docs/multi-tenant-accounts.md index 55f337887f..398ee0ec4c 100644 --- a/lib/msal-common/docs/multi-tenant-accounts.md +++ b/lib/msal-common/docs/multi-tenant-accounts.md @@ -1,6 +1,6 @@ # Multi-tenant Support in MSAL JS SDKs -> This is an advanced document regarding the handling of accounts to acquire tokens across tenants in the `msal-browser` and `msal-node` SDKs. For basic account information, please review the [Accounts](./accounts.md) document. +> This document is about the handling of multi-tenant accounts to acquire tokens across tenants in the `msal-browser` and `msal-node` SDKs. For basic account information, please review the [Accounts](./accounts.md) document. ## Table of Contents diff --git a/lib/msal-node/docs/accounts.md b/lib/msal-node/docs/accounts.md index 11db3faff3..e75880a82f 100644 --- a/lib/msal-node/docs/accounts.md +++ b/lib/msal-node/docs/accounts.md @@ -1,6 +1,6 @@ # Accounts in MSAL Node -> This is the platform-specific Accounts documentation for `msal-node`. For the general documentation of the `AccountInfo` object structure, please visit the `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). +> This is the platform-specific Accounts documentation for `msal-node`. For the general documentation of the `AccountInfo` object structure, please visit the `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). ## Usage @@ -18,7 +18,7 @@ For a multiple accounts scenario: ```javascript // Initiates Acquire Token Silent flow -function callAcquireTokenSilent() +function callAcquireTokenSilent() { // Find all accounts const msalTokenCache = myMSALObj.getTokenCache(); const cachedAccounts = await msalTokenCache.getAllAccounts(); @@ -41,7 +41,7 @@ function callAcquireTokenSilent() .catch((error) => { // Error handling }); -}); +} ``` ### getAccountByHomeId and getAccountByLocalId diff --git a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/auth.js b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/auth.js index b412fd83b6..545e5d32e9 100644 --- a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/auth.js +++ b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/auth.js @@ -14,7 +14,7 @@ myMSALObj.initialize().then(() => { function handleResponse() { // when a filter is passed into getAllAccounts, it returns all cached accounts that match the filter. Use isHomeTenant filter to get the home accounts. - const allAccounts = myMSALObj.getAllAccounts({ tenantId: homeTenant, username: "hemoral@microsoft.com" }); + const allAccounts = myMSALObj.getAllAccounts(); console.log("Get all accounts: ", allAccounts); if (!allAccounts || allAccounts.length < 1) { return; @@ -77,9 +77,8 @@ function signOut(interactionType) { async function requestGuestToken() { const currentAcc = myMSALObj.getAccountByHomeId(accountId); if (currentAcc) { - const response = await getTokenRedirect({ ...guestTenantRequest, account: currentAcc }).catch(error => { - console.log(error); - }); + + const response = await sso(guestTenantRequest, currentAcc.idTokenClaims.login_hint); console.log(response); callMSGraph(graphConfig.graphMeEndpoint, response.accessToken, updateUI); guestProfileButton.style.display = 'none'; @@ -87,7 +86,6 @@ async function requestGuestToken() { } async function getTokenRedirect(request) { - console.log("Request: ", request); return await myMSALObj.acquireTokenSilent(request).catch(async (error) => { console.log("silent token acquisition fails."); if (error instanceof msal.InteractionRequiredAuthError) { @@ -98,4 +96,17 @@ async function getTokenRedirect(request) { console.error(error); } }); +} + +async function sso(request, loginHint) { + return await myMSALObj.ssoSilent(request, loginHint).catch(async (error) => { + console.log("sso failed"); + if (error instanceof msal.InteractionRequiredAuthError) { + // fallback to interaction when silent call fails + console.log("acquiring token using redirect"); + myMSALObj.acquireTokenRedirect(request); + } else { + console.error(error); + } + }) } \ No newline at end of file diff --git a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/authConfig.js b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/authConfig.js index 545004a514..88b3b5f870 100644 --- a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/authConfig.js +++ b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/authConfig.js @@ -1,11 +1,11 @@ -const homeTenant = "72f988bf-86f1-41af-91ab-2d7cd011db47"; -const guestTenant = "5d97b14d-c396-4aee-b524-c86d33e9b660" +const homeTenant = "HOME_TENANT_ID"; +const guestTenant = "GUEST_TENANT_ID" const baseAuthority = "https://login.microsoftonline.com" // Config object to be passed to Msal on creation const msalConfig = { auth: { - clientId: "bc77b0a7-16aa-4af4-884b-41b968c9c71a", + clientId: "ENTER_CLIENT_ID", authority: `${baseAuthority}/${homeTenant}` }, cache: { From e24ce716b7a28445906b03575dc5210406e2ce24 Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Wed, 15 Nov 2023 11:06:26 -0800 Subject: [PATCH 68/91] Update lib/msal-common/docs/multi-tenant-accounts.md Co-authored-by: Thomas Norling --- lib/msal-common/docs/multi-tenant-accounts.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/msal-common/docs/multi-tenant-accounts.md b/lib/msal-common/docs/multi-tenant-accounts.md index 398ee0ec4c..b1e7ae215e 100644 --- a/lib/msal-common/docs/multi-tenant-accounts.md +++ b/lib/msal-common/docs/multi-tenant-accounts.md @@ -26,7 +26,7 @@ Conceptually, a tenant profile is the record of an account in a specific tenant. MSAL uses these `TenantProfile` objects to match and build the tenant-specific `AccountInfo` objects required through all of MSAL's flows. They can also be used by client applications for different purposes such as facilitating account selection logic and displaying account data for the user across tenants. -> Warning: While MSAL can return a tenant-specific `AccountInfo` object for each tenant profile, tenant profiles are not actually different accounts, they are only representations of the same account in the different tenants the account has authentiated with. **If a user uses different accounts to authenticate to each tenant, these will not be linked as tenant profiles of eachother and will be treated as completely different accounts that cannot be used to access eachother's tokens**. +> Warning: While MSAL can return a tenant-specific `AccountInfo` object for each tenant profile, tenant profiles are not actually different accounts, they are only representations of the same account in the different tenants the account has authenticated with. **If a user uses different accounts to authenticate to each tenant, these will not be linked as tenant profiles of each other and will be treated as completely different accounts that cannot be used to access each other's tokens**. ## Usage From fda2f57163f2f51ea4a4259919d4c1c8453c6a09 Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Wed, 15 Nov 2023 17:12:01 -0800 Subject: [PATCH 69/91] Update multi-tenant docs from feedback --- lib/msal-common/docs/multi-tenant-accounts.md | 209 +++++++++++------- 1 file changed, 134 insertions(+), 75 deletions(-) diff --git a/lib/msal-common/docs/multi-tenant-accounts.md b/lib/msal-common/docs/multi-tenant-accounts.md index 398ee0ec4c..4fd08876c5 100644 --- a/lib/msal-common/docs/multi-tenant-accounts.md +++ b/lib/msal-common/docs/multi-tenant-accounts.md @@ -8,8 +8,9 @@ - [Tenant Profiles](#tenant-profiles) - [Usage](#usage) - [Authenticating with Multiple Tenants](#authenticating-with-multiple-tenants) - - [Acquiring Tokens Silently for Multiple Tenants](#acquiring-tokens-silently-for-multiple-tenants) - - [getAllAccounts with Multiple Tenants](#getallaccounts-with-multiple-tenants) + - [Filtering Multi-tenant Accounts](#filtering-multi-tenant-accounts) + - [Using getAccount to search for a specific tenanted account](#using-getaccount-to-search-for-a-specific-tenanted-account) + - [Using getAllAccounts with an account filter to narrow down the collection of accounts returned](#using-getallaccounts-with-an-account-filter-to-narrow-down-the-collection-of-accounts-returned) - [Multi-tenant Logout](#multi-tenant-logout) ## Multi-tenant Accounts @@ -18,6 +19,8 @@ MSAL supports the acquisition and caching of access and ID tokens across multipl In addition to tenant-specific account data, multi-tenant accounts also contain a `Map` of the **tenant profiles** for the account corresponding to each tenant the user has authenticated with. +> Note: Access and ID tokens are tenant-specific while Refresh Tokens are shared across tenants. + ## Tenant Profiles Conceptually, a tenant profile is the record of an account in a specific tenant. In MSAL JS SDKs, [TenantProfile](https://azuread.github.io/microsoft-authentication-library-for-js/ref/types/_azure_msal_common.TenantProfile.html) objects contain the subset of the `AccountInfo` properties that vary by tenant. They are created by using the claims from the ID token issued by each tenant the user authenticates with. @@ -26,7 +29,7 @@ Conceptually, a tenant profile is the record of an account in a specific tenant. MSAL uses these `TenantProfile` objects to match and build the tenant-specific `AccountInfo` objects required through all of MSAL's flows. They can also be used by client applications for different purposes such as facilitating account selection logic and displaying account data for the user across tenants. -> Warning: While MSAL can return a tenant-specific `AccountInfo` object for each tenant profile, tenant profiles are not actually different accounts, they are only representations of the same account in the different tenants the account has authentiated with. **If a user uses different accounts to authenticate to each tenant, these will not be linked as tenant profiles of eachother and will be treated as completely different accounts that cannot be used to access eachother's tokens**. +> Warning: While MSAL can return a tenant-specific `AccountInfo` object for each tenant profile, tenant profiles are not actually different accounts, they are only representations of the same account in the different tenants the account has authenticated with. **If a user uses different accounts to authenticate to each tenant, these will not be linked as tenant profiles of each other and will be treated as completely different accounts that cannot be used to access each other's tokens**. ## Usage @@ -34,113 +37,166 @@ MSAL uses these `TenantProfile` objects to match and build the tenant-specific ` In order to authenticate with multiple tenants, you can use either `login` or `acquireToken` APIs normally, only setting the authority to the particular tenant you are requesting tokens for. -> Note: For `login`/`acquireToken` interactive APIs, the tenant context is set from the authority's `tenantId`. +> Note: +> +> - For `login`/`acquireToken` interactive and `ssoSilent` APIs, the tenant context is set from the authority's `tenantId`. +> - For `acquireTokenSilent` the tenant context is set from the `AccountInfo` object passed in. + +MSAL Browser example: ```javascript +/** + * Custom function that first attempts to acquire tokens silently from a specific tenant + * and falls back to interaction if there is no cached token matching the request and tenant + */ +async function getTokenMultiTenant(request, tenantId) { + // If an account was added to the request, attempt silent token acquisition + if (request.account) { + let tenantedAccount = null; + if (tenantId) { + // Attempt to get tenant-specific account + tenantedAccount = myMSALObj.getAccount( + { + homeAccountId: account.homeAccountId, + tenantId: GUEST_TENANT_ID + }); + request.authority = BASE_AUTHORITY + tenantId + } + + if (tenantedAccount) { + // Use the cached tenant profile directly to find an access token in the cache + request.account = tenantedAccount; + } else { + // Force acquireTokenSilent to use the cached refresh token to acquire an access token from the tenant in the authority instead + request.cacheLookupPolicy = CacheLookupPolicy.RefreshToken // alternatively, you can set forceRefresh: true + } + + return await myMSALObj.acquireTokenSilent(request).catch((error) => { + if (error instanceof InteractionRequiredAuthError) { + // fallback to interaction when silent call fails. Possible reasons are expired tokens, MFA (multi-factor authentication) required, etc. + await myMSALObj.acquireTokenPopup(request); + } else { + console.error(error); + } + }); + } else { + // No account means user has yet to authenticate, interaction required + return await myMSALObj.loginPopup(request); + } +} + +. +. +. + +/** + * Main Script + */ + +import { PublicClientApplication, InteractionRequiredError, CacheLookupPolicy } from "@azure/msal-browser"; +/** + * Establish home and guest tenant IDs as well as base authority: + */ +const HOME_TENANT_ID = "HOME_TENANT_ID"; +const GUEST_TENANT_ID = "GUEST_TENANT_ID"; +const BASE_AUTHORITY = "https://login.microsoftonline.com/" + + +/** + * Configure PublicClientApplication + */ const msalConfig = { auth: { clientId: "ENTER_CLIENT_ID", - authority: "https://login.microsoftonline.com/HOME_TENANT", // This is the authority that MSAL will default to for requests that don't specify their own authority. + authority: BASE_AUTHORITY + HOME_TENANT_ID, // This is the authority that MSAL will default to for requests that don't specify their own authority. }, }; +/** + * Initialize PublicClientApplication + */ const myMSALObj = new PublicClientApplication(msalConfig); - -// handleRedirectPromise has to be called to resume redirect requests -myMSALObj +myMSALObj.initialize.then(() => { + // handleRedirectPromise has to be called to resume redirect requests .handleRedirectPromise() .then(handleResponse) .catch((err) => { console.error(err); }); +}) -// Authority isn't specified because it is already the default set in the MSAL Config object +/** + * Configure base requests + */ const homeTenantRequest = { scopes: ["HOME_TENANT_SCOPE"], }; - -// Guest tenant authority overrides the MSAL config authority, meaning the user can authenticate with the tenant they are a guest in and acquire tokens for it const guestTenantRequest = { - scopes: ["GUEST_TENANT_SCOPE"], - authority: "https://login.microsoftonline.com/GUEST_TENANT", + scopes: ["GUEST_TENANT_SCOPE"] }; -const homeTenantAuthResponse = await myMSALObj.loginPopup(homeTenantRequest); -// Get the home tenant account from the AuthenticationResult -const homeTenantAccount = homeTenantAuthResponse.account; +// There is no account at this point, user hasn't logged in +const homeTenantAuthResponse = await getTokenMultiTenant(homeTenantRequest); +// Get the home tenant/base account from the AuthenticationResult +const baseAccount = homeTenantAuthResponse.account; +// Get home tenant access token +const homeAccessToken = homeTenantAuthResponse.accessToken; + +// Acquire guest tenant tokens and tenant profile by leveraging the already authenticated account +const guestTenantAuthResponse = await getTokenMultiTenant( + { + ...guestTenantRequest, + account: baseAccount // At this point, this the base account with home account tenant profile information + }, + GUEST_TENANT_ID + ); -const guestTenantAuthResponse = await myMSALObj.loginPopup(homeTenantRequest); -// Get the guest tenant account from the AuthenticationResult const guestTenantAccount = guestTenantAuthResponse.account; +const guestTenantAccessToken = guestTenantAuthResponse.accessToken; ``` -Assuming the user logs into both tenants with the same account, once both login requests are successfully completed and the user has authenticated with each tenant, the cache will contain: +### Filtering Multi-tenant Accounts -- 2 ID tokens (one per tenant) -- 2 Access tokens (one per tenant) -- 1 Refresh token (one per account, shared across tenants) -- 1 AccountEntity containing both tenant profiles. The `homeTenantAccount` and `guestTenantAccount` objects shown above are both created from the same cached account entity augmented by the claims in their respective ID tokens. +With multi-tenant accounts, the [AccountFilter](https://azuread.github.io/microsoft-authentication-library-for-js/ref/types/_azure_msal_common.AccountFilter.html) type can be leveraged to search for a specific tenanted account object using `getAccount()` or narrow down the collection of accounts returned by `getAllAccounts()`. -### Acquiring Tokens Silently for Multiple Tenants +#### Using getAccount to search for a specific tenanted account -In order to acquire cached tokens for a particular tenant, the `AccountInfo` object for that particular tenant must be passed into `acquireTokenSilent`. In order to get the correct account to pass into acquire token silent, you can either: - -- Use the `AccountInfo` object returned from an interactive API call to that tenant: - -```javascript -// Original interactive call to home tenant -const homeTenantAuthResponse = await myMSALObj.loginPopup(homeTenantRequest); -// Get the home tenant account from the AuthenticationResult -const homeTenantAccount = homeTenantAuthResponse.account; -. -. -. -// Acquire a home tenant access token silently -const homeTenantSilentResponse = await myMSALObj.acquireTokenSilent({ - account: homeTenantAccount, - ...homeTenantRequest, -}); - - -// Original interactive call to guest tenant -const guestTenantAuthResponse = await myMSALObj.loginPopup(homeTenantRequest); -// Get the guest tenant account from the AuthenticationResult -const guestTenantAccount = guestTenantAuthResponse.account; - -// Acquire a guest tenant access token silently -const guestTenantSilentResponse = await myMSALObj.acquireTokenSilent({ - account: guestTenantAccount, - ...guestTenantRequest, -}); -``` - -- Use the `getAccount()` API with the desired `tenantId` as a filter and use the `AccountInfo` object returned: +This example uses the `getAccount()` API with the desired `tenantId` as a filter and then uses the `AccountInfo` object returned to acquire a previously cached token for that specific tenant. ```javascript const homeAccountId = "HOME_ACCOUNT_ID"; // Shared across tenant profiles -const homeTenantId = "HOME_TENANT_ID"; const guestTenantId = "GUEST_TENANT_ID"; -// Get the home tenant account -const homeTenantAccount = myMSALObj.getAccount({ +// Get the guest tenant account +const guestTenantAccount = myMSALObj.getAccount({ homeAccountId: homeAccountId, - tenantId: homeTenantId, + tenantId: guestTenantId, }); -// Get home tenant token -const homeTenantAuthResponse = await myMSALObj.acquireTokenSilent({ - account: homeTenantAccount, - ...homeTenantRequest, -}); +// Get guest tenant token +let guestTenantAuthResponse; +if (guestTenantAccount) { + guestTenantAuthResponse = await myMSALObj.acquireTokenSilent({ + account: guestTenantAccount, + ...guestTenantRequest, + }); +} else { + // authenticate with the guest tenant for the first time +} ``` -### getAllAccounts with Multiple Tenants +#### Using getAllAccounts with an account filter to narrow down the collection of accounts returned + +By default, `getAllAccounts` will return an account for every tenant profile that has been previously cached. However, the results of `getAllAccounts` can be filtered by any of the properties in the `AccountFilter` type. Additionally, multi-tenant accounts in the results can be "flattened" into their base/home accounts only by setting the `isHomeTenant` filter to true. -When any account in the MSAL cache contains multiple tenant profiles, `getAllAccounts()` is expected to return a full `AccountInfo` object for each tenant profile that satisfies the filter provided. If no filter is provided, every tenant profile for every account will be returned as a full `AccountInfo` object by default. This means that even if there is only one account object in the cache, multiple `AccountInfo` objects may be returned from `getAllAccounts`. +How flattening multit-tenant accounts works: -The results of `getAllAccounts` can be "flattened" to only return the home accounts which would each still have a map of their `tenantProfiles`. This can be achieved by setting the `isHomeTenant` filter to `true`. The opposite, getting only accounts built from guest tenant profiles, can be achieved by setting the `isHomeTenant` filter to `false.` +- To get base/home accounts only, set `isHomeTenant: true` in the filter object passed in. + - If `isHomeTenant` is set to `false`, instead of flattening it will filter our home accounts and return all guest tenant accounts that match the rest of the filter +- The "flattened" accounts returned will still have a map of all their `tenantProfiles` +- The `AccountInfo` object for each guest tenant profile would be ommitted from the `getAllAccounts` result array. -The sample code below shows how one can: +The sample code below shows how to: - Filter the result of `getAllAccounts()` using the optional `accountFilter` parameter to "flatten" the cached accounts into home accounts with a map of their tenant profiles (otherwise getAllAccounts will return the `AccountInfo` object for each tenant profile) - Extract the desired `TenantProfile` object from the home `AccountInfo` object @@ -149,13 +205,15 @@ The sample code below shows how one can: ```javascript // When a filter is passed into getAllAccounts, it returns all cached accounts that match the filter. Use the special isHomeTenant filter to get the home accounts only. -const homeAccount = myMSALObj.getAllAccounts({ isHomeTenant: true })[0]; +const allHomeAccounts = myMSALObj.getAllAccounts({ isHomeTenant: true }); +const homeAccount = allHomeAccounts[0]; // Assuming only one user is logged into multiple tenants const tenantId = "GUEST_TENANT_ID"; // This will be the tenant you want to retrieve a cached token for +// Get the `TenantProfile` account data subset for the desired tenant from the homeAccount object const guestTenantProfile = homeAccount.tenantProfiles.get(tenantId); if (guestTenantProfile) { - // TenantProfile is a subset of AccountInfo, so it can be passed whole as an `AccountFilter` + // TenantProfile is a subset of AccountInfo, so it can be passed whole as an AccountFilter const guestTenantAccount = myMSALObj.getAccount({ ...tenantProfile }); const guestTenantAuthResponse = await myMSALObj @@ -172,10 +230,11 @@ if (guestTenantProfile) { } }); } else { - // Authenticate with guest tenant first - const guestTenantAuthResponse = await myMSALObj.loginPopup( - homeTenantRequest - ); + // If the tenant profile isn't found in the account, that means the user hasn't authenticated with that tenant. This is the custom getToken function from the first example. + const guestTenantAuthResponse = await myMSALObj.getToken({ + ...guestTenantRequest, + account: homeAccount, + }); } ``` From f2a21de5e74675ea552590863a81e7400abbe822 Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Thu, 16 Nov 2023 11:59:55 -0800 Subject: [PATCH 70/91] Address PR feedback --- lib/msal-browser/src/cache/TokenCache.ts | 2 +- lib/msal-common/src/account/AccountInfo.ts | 16 +++++++++------ lib/msal-common/src/account/TokenClaims.ts | 2 +- lib/msal-common/src/cache/CacheManager.ts | 20 +++++-------------- .../src/cache/entities/AccountEntity.ts | 8 ++++---- .../test/account/AccountInfo.spec.ts | 12 ++++------- .../test/account/TokenClaims.spec.ts | 4 ---- lib/msal-node/src/cache/NodeStorage.ts | 5 ++++- 8 files changed, 29 insertions(+), 40 deletions(-) diff --git a/lib/msal-browser/src/cache/TokenCache.ts b/lib/msal-browser/src/cache/TokenCache.ts index 3fdeb8150a..117551803c 100644 --- a/lib/msal-browser/src/cache/TokenCache.ts +++ b/lib/msal-browser/src/cache/TokenCache.ts @@ -268,7 +268,7 @@ export class TokenCache implements ITokenCache { authority, homeAccountId, idTokenClaims, - this.cryptoObj, + this.cryptoObj.base64Decode, clientInfo, claimsTenantId, undefined, diff --git a/lib/msal-common/src/account/AccountInfo.ts b/lib/msal-common/src/account/AccountInfo.ts index f8aa19dee9..a0e11c68bc 100644 --- a/lib/msal-common/src/account/AccountInfo.ts +++ b/lib/msal-common/src/account/AccountInfo.ts @@ -41,16 +41,14 @@ export type AccountInfo = { /** * Account details that vary across tenants for the same user - * - * - tenantId - Full tenant or organizational id that this account belongs to - * - localAccountId - Local, tenant-specific account identifer for this account object, usually used in legacy cases - * - name - Full name for the account, including given name and family name - * - isHomeTenant - True if this is the home tenant profile of the account, false if it's a guest tenant profile */ export type TenantProfile = Pick< AccountInfo, "tenantId" | "localAccountId" | "name" > & { + /** + * - isHomeTenant - True if this is the home tenant profile of the account, false if it's a guest tenant profile + */ isHomeTenant?: boolean; }; @@ -83,10 +81,16 @@ export function buildTenantProfileFromIdTokenClaims( ): TenantProfile { const { oid, sub, tid, name, tfp, acr } = idTokenClaims; + /** + * Since there is no way to determine if the authority is AAD or B2C, we exhaust all the possible claims that can serve as tenant ID with the following precedence: + * tid - TenantID claim that identifies the tenant that issued the token in AAD. Expected in all AAD ID tokens, not present in B2C ID Tokens. + * tfp - Trust Framework Policy claim that identifies the policy that was used to authenticate the user. Functions as tenant for B2C scenarios. + * acr - Authentication Context Class Reference claim used only with older B2C policies. Fallback in case tfp is not present, but likely won't be present anyway. + */ const tenantId = tid || tfp || acr || ""; return { - tenantId: tenantId.toLowerCase(), + tenantId: tenantId, localAccountId: oid || sub || "", name: name, isHomeTenant: tenantIdMatchesHomeTenant(tenantId, homeAccountId), diff --git a/lib/msal-common/src/account/TokenClaims.ts b/lib/msal-common/src/account/TokenClaims.ts index 5d6a317fac..f8cc1bde29 100644 --- a/lib/msal-common/src/account/TokenClaims.ts +++ b/lib/msal-common/src/account/TokenClaims.ts @@ -92,7 +92,7 @@ export function getTenantIdFromIdTokenClaims( if (idTokenClaims) { const tenantId = idTokenClaims.tid || idTokenClaims.tfp || idTokenClaims.acr; - return tenantId?.toLowerCase() || null; + return tenantId || null; } return null; } diff --git a/lib/msal-common/src/cache/CacheManager.ts b/lib/msal-common/src/cache/CacheManager.ts index 496f26eaca..42f6e8d944 100644 --- a/lib/msal-common/src/cache/CacheManager.ts +++ b/lib/msal-common/src/cache/CacheManager.ts @@ -320,22 +320,10 @@ export abstract class CacheManager implements ICacheManager { accountEntity: AccountEntity, accountFilter?: AccountFilter ): AccountInfo[] { - const tenantProfileFilter: TenantProfileFilter | undefined = - accountFilter - ? { - localAccountId: accountFilter.localAccountId, - loginHint: accountFilter.loginHint, - name: accountFilter.name, - username: accountFilter.username, - sid: accountFilter.sid, - isHomeTenant: accountFilter.isHomeTenant, - } - : undefined; - return this.getTenantProfilesFromAccountEntity( accountEntity, accountFilter?.tenantId, - tenantProfileFilter + accountFilter ); } @@ -1068,7 +1056,7 @@ export abstract class CacheManager implements ICacheManager { } ); - const updatedAccount = Object.assign(new AccountEntity(), { + const updatedAccount = CacheManager.toObject(new AccountEntity(), { ...baseAccount, }); @@ -1846,7 +1834,9 @@ export abstract class CacheManager implements ICacheManager { entity: AccountEntity | CredentialEntity, realm: string ): boolean { - return !!(entity.realm && realm === entity.realm); + return !!( + entity.realm?.toLowerCase() && realm === entity.realm.toLowerCase() + ); } /** diff --git a/lib/msal-common/src/cache/entities/AccountEntity.ts b/lib/msal-common/src/cache/entities/AccountEntity.ts index e1245186a2..920cfc7ea3 100644 --- a/lib/msal-common/src/cache/entities/AccountEntity.ts +++ b/lib/msal-common/src/cache/entities/AccountEntity.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. */ -import { CacheAccountType } from "../../utils/Constants"; +import { CacheAccountType, Separators } from "../../utils/Constants"; import { Authority } from "../../authority/Authority"; import { ICrypto } from "../../crypto/ICrypto"; import { ClientInfo, buildClientInfo } from "../../account/ClientInfo"; @@ -68,7 +68,7 @@ export class AccountEntity { */ generateAccountId(): string { const accountId: Array = [this.homeAccountId, this.environment]; - return accountId.join("-").toLowerCase(); + return accountId.join(Separators.CACHE_KEY_SEPARATOR).toLowerCase(); } /** @@ -125,7 +125,7 @@ export class AccountEntity { homeTenantId || accountInterface.tenantId || "", ]; - return accountKey.join("-").toLowerCase(); + return accountKey.join(Separators.CACHE_KEY_SEPARATOR).toLowerCase(); } /** @@ -295,7 +295,7 @@ export class AccountEntity { } } catch (e) {} } - logger.verbose("No client info in response"); + logger.warning("No client info in response"); } // default to "sub" claim diff --git a/lib/msal-common/test/account/AccountInfo.spec.ts b/lib/msal-common/test/account/AccountInfo.spec.ts index 35975185e7..0ea8e562cc 100644 --- a/lib/msal-common/test/account/AccountInfo.spec.ts +++ b/lib/msal-common/test/account/AccountInfo.spec.ts @@ -63,7 +63,7 @@ describe("AccountInfo Unit Tests", () => { expect(tenantProfile.tenantId).not.toEqual(idTokenClaims.acr); expect(tenantProfile.tenantId).not.toEqual(""); }); - it("from the tfp claim (downcased) when present and tid not present", () => { + it("from the tfp claim when present and tid not present", () => { const idTokenClaims = { tfp: ID_TOKEN_EXTRA_CLAIMS.tfp, acr: ID_TOKEN_EXTRA_CLAIMS.acr, @@ -73,14 +73,12 @@ describe("AccountInfo Unit Tests", () => { TEST_ACCOUNT_INFO.homeAccountId, idTokenClaims ); - expect(tenantProfile.tenantId).toEqual( - idTokenClaims.tfp.toLowerCase() - ); + expect(tenantProfile.tenantId).toEqual(idTokenClaims.tfp); expect(tenantProfile.tenantId).not.toEqual(idTokenClaims.acr); expect(tenantProfile.tenantId).not.toEqual(""); }); - it("from the acr claim (downcased) when present but tid and tfp not present", () => { + it("from the acr claim when present but tid and tfp not present", () => { const idTokenClaims = { acr: ID_TOKEN_EXTRA_CLAIMS.acr, }; @@ -89,9 +87,7 @@ describe("AccountInfo Unit Tests", () => { TEST_ACCOUNT_INFO.homeAccountId, idTokenClaims ); - expect(tenantProfile.tenantId).toEqual( - idTokenClaims.acr.toLowerCase() - ); + expect(tenantProfile.tenantId).toEqual(idTokenClaims.acr); expect(tenantProfile.tenantId).not.toEqual(""); }); diff --git a/lib/msal-common/test/account/TokenClaims.spec.ts b/lib/msal-common/test/account/TokenClaims.spec.ts index 71f84aff3c..88b82abc2e 100644 --- a/lib/msal-common/test/account/TokenClaims.spec.ts +++ b/lib/msal-common/test/account/TokenClaims.spec.ts @@ -20,8 +20,4 @@ describe("TokenClaims Utilities Unit Tests", () => { it("returns acr claim if present", () => { expect(getTenantIdFromIdTokenClaims({ acr: "acr" })).toBe("acr"); }); - - it("correctly downcases the tenantId", () => { - expect(getTenantIdFromIdTokenClaims({ tid: "TID" })).toBe("tid"); - }); }); diff --git a/lib/msal-node/src/cache/NodeStorage.ts b/lib/msal-node/src/cache/NodeStorage.ts index f902690945..93ab126567 100644 --- a/lib/msal-node/src/cache/NodeStorage.ts +++ b/lib/msal-node/src/cache/NodeStorage.ts @@ -231,7 +231,10 @@ export class NodeStorage extends CacheManager { * @returns */ getCachedAccountEntity(accountKey: string): AccountEntity | null { - return Object.assign(new AccountEntity(), this.getItem(accountKey)); + const cachedAccount = this.getItem(accountKey); + return cachedAccount + ? Object.assign(new AccountEntity(), this.getItem(accountKey)) + : null; } /** From 4b1bf37e4ddedd70a1dab5f2b9a22400b298cafa Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Wed, 22 Nov 2023 11:57:56 -0800 Subject: [PATCH 71/91] Update multi-tenant account docs --- lib/msal-common/docs/multi-tenant-accounts.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/msal-common/docs/multi-tenant-accounts.md b/lib/msal-common/docs/multi-tenant-accounts.md index 4fd08876c5..8bb7b26d66 100644 --- a/lib/msal-common/docs/multi-tenant-accounts.md +++ b/lib/msal-common/docs/multi-tenant-accounts.md @@ -230,8 +230,8 @@ if (guestTenantProfile) { } }); } else { - // If the tenant profile isn't found in the account, that means the user hasn't authenticated with that tenant. This is the custom getToken function from the first example. - const guestTenantAuthResponse = await myMSALObj.getToken({ + // If the tenant profile isn't found in the account, that means the user hasn't authenticated with that tenant. This is the custom getTokenMultiTenant function from the first example. + const guestTenantAuthResponse = await myMSALObj.getTokenMultiTenant({ ...guestTenantRequest, account: homeAccount, }); From c3bb7350535e1fb921f70deb11c28d8a65db1829 Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Wed, 22 Nov 2023 12:01:49 -0800 Subject: [PATCH 72/91] Replace cryptoObj.base64Decode with original base64Decode function --- lib/msal-browser/src/cache/TokenCache.ts | 2 +- .../src/interaction_client/NativeInteractionClient.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/msal-browser/src/cache/TokenCache.ts b/lib/msal-browser/src/cache/TokenCache.ts index 117551803c..a51cdf1b25 100644 --- a/lib/msal-browser/src/cache/TokenCache.ts +++ b/lib/msal-browser/src/cache/TokenCache.ts @@ -268,7 +268,7 @@ export class TokenCache implements ITokenCache { authority, homeAccountId, idTokenClaims, - this.cryptoObj.base64Decode, + base64Decode, clientInfo, claimsTenantId, undefined, diff --git a/lib/msal-browser/src/interaction_client/NativeInteractionClient.ts b/lib/msal-browser/src/interaction_client/NativeInteractionClient.ts index d00d53a0f8..502fc37c60 100644 --- a/lib/msal-browser/src/interaction_client/NativeInteractionClient.ts +++ b/lib/msal-browser/src/interaction_client/NativeInteractionClient.ts @@ -428,7 +428,7 @@ export class NativeInteractionClient extends BaseInteractionClient { authority, homeAccountIdentifier, idTokenClaims, - this.browserCrypto.base64Decode, + base64Decode, response.client_info, idTokenClaims.tid, undefined, From 36c9f1ac840265c16052fe5de6f4110416eb360f Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Wed, 22 Nov 2023 17:08:53 -0800 Subject: [PATCH 73/91] Add multi-tenant e2e tests --- lib/msal-common/docs/multi-tenant-accounts.md | 2 +- lib/msal-common/src/cache/CacheManager.ts | 2 +- .../e2eTestUtils/src/BrowserCacheTestUtils.ts | 7 +- samples/e2eTestUtils/src/Constants.ts | 24 +- samples/e2eTestUtils/src/LabApiQueryParams.ts | 19 +- samples/e2eTestUtils/src/LabClient.ts | 66 +++- samples/e2eTestUtils/src/MsidUser.ts | 24 +- samples/e2eTestUtils/src/TestUtils.ts | 17 +- .../app/customizable-e2e-test/auth.js | 57 ++- .../authConfigs/aadMultiTenantAuthConfig.json | 28 ++ .../app/customizable-e2e-test/index.html | 4 + .../test/browserAADMultiTenant.spec.ts | 363 ++++++++++++++++++ 12 files changed, 544 insertions(+), 69 deletions(-) create mode 100644 samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/authConfigs/aadMultiTenantAuthConfig.json create mode 100644 samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/test/browserAADMultiTenant.spec.ts diff --git a/lib/msal-common/docs/multi-tenant-accounts.md b/lib/msal-common/docs/multi-tenant-accounts.md index 8bb7b26d66..33ed388a26 100644 --- a/lib/msal-common/docs/multi-tenant-accounts.md +++ b/lib/msal-common/docs/multi-tenant-accounts.md @@ -40,7 +40,7 @@ In order to authenticate with multiple tenants, you can use either `login` or `a > Note: > > - For `login`/`acquireToken` interactive and `ssoSilent` APIs, the tenant context is set from the authority's `tenantId`. -> - For `acquireTokenSilent` the tenant context is set from the `AccountInfo` object passed in. +> - For `acquireTokenSilent` the tenant context is set from the `AccountInfo` object passed in when searching the cache for matching tokens, regardless of the tenantId in the request's authority. MSAL Browser example: diff --git a/lib/msal-common/src/cache/CacheManager.ts b/lib/msal-common/src/cache/CacheManager.ts index 42f6e8d944..084ff1ab80 100644 --- a/lib/msal-common/src/cache/CacheManager.ts +++ b/lib/msal-common/src/cache/CacheManager.ts @@ -1145,7 +1145,7 @@ export abstract class CacheManager implements ICacheManager { ): CacheRecord { // Use authority tenantId for cache lookup filter if it's defined, otherwise use tenantId from account passed in const requestTenantId = - getTenantFromAuthorityString(request.authority) || account.tenantId; + account.tenantId || getTenantFromAuthorityString(request.authority); const tokenKeys = this.getTokenKeys(); const cachedAccount = this.readAccountFromCache(account); diff --git a/samples/e2eTestUtils/src/BrowserCacheTestUtils.ts b/samples/e2eTestUtils/src/BrowserCacheTestUtils.ts index 7d879ebad5..c8df77d1e1 100644 --- a/samples/e2eTestUtils/src/BrowserCacheTestUtils.ts +++ b/samples/e2eTestUtils/src/BrowserCacheTestUtils.ts @@ -112,11 +112,12 @@ export class BrowserCacheUtils { async accessTokenForScopesExists( accessTokenKeys: Array, - scopes: Array + scopes: Array, + targetTokenMatchesNumber: number = 1 ): Promise { const storage = await this.getWindowStorage(); - return accessTokenKeys.some((key) => { + const matches = accessTokenKeys.filter((key) => { const tokenVal = JSON.parse(storage[key]); const tokenScopes = tokenVal.target.toLowerCase().split(" "); @@ -124,6 +125,8 @@ export class BrowserCacheUtils { return tokenScopes.includes(scope.toLowerCase()); }); }); + + return matches.length === targetTokenMatchesNumber; } async popAccessTokenForScopesExists( diff --git a/samples/e2eTestUtils/src/Constants.ts b/samples/e2eTestUtils/src/Constants.ts index 7a0b71747e..52fc75717c 100644 --- a/samples/e2eTestUtils/src/Constants.ts +++ b/samples/e2eTestUtils/src/Constants.ts @@ -1,7 +1,7 @@ export const ENV_VARIABLES = { TENANT: "AZURE_TENANT_ID", CLIENT_ID: "AZURE_CLIENT_ID", - SECRET: "AZURE_CLIENT_SECRET" + SECRET: "AZURE_CLIENT_SECRET", }; export const LAB_API_ENDPOINT = "https://msidlab.com/api"; @@ -16,7 +16,8 @@ export const ParamKeys = { APP_TYPE: "apptype", SIGN_IN_AUDIENCE: "signInAudience", PUBLIC_CLIENT: "publicClient", - APP_PLATFORM: "appPlatform" + APP_PLATFORM: "appPlatform", + GUEST_HOMED_IN: "guesthomedin", }; // Lab API Query Param Values @@ -28,7 +29,7 @@ export const AzureEnvironments = { PPE: "azureppe", US_GOV: "azureusgovernment", US_GOV_JEDI_PROD: "usgovernmentjediprod", - US_GOV_MIGRATED: "azureusgovernmentmigrated" + US_GOV_MIGRATED: "azureusgovernmentmigrated", }; export const B2cProviders = { @@ -38,7 +39,7 @@ export const B2cProviders = { GOOGLE: "google", LOCAL: "local", MICROSOFT: "microsoft", - TWITTER: "twitter" + TWITTER: "twitter", }; export const FederationProviders = { @@ -49,7 +50,7 @@ export const FederationProviders = { ADFS2019: "adfsv2019", B2C: "b2c", PING: "ping", - SHIBBOLETH: "shibboleth" + SHIBBOLETH: "shibboleth", }; export const HomeDomains = { @@ -57,7 +58,7 @@ export const HomeDomains = { LAB2: "msidlab2.com", LAB3: "msidlab3.com", LAB4: "msidlab4.com", - LAB8: "msidlab8.com" + LAB8: "msidlab8.com", }; export const UserTypes = { @@ -66,15 +67,20 @@ export const UserTypes = { ONPREM: "onprem", GUEST: "guest", MSA: "msa", - B2C: "b2c" + B2C: "b2c", }; export const AppTypes = { CLOUD: "cloud", - ONPREM: "onprem" + ONPREM: "onprem", }; export const AppPlatforms = { SPA: "spa", - WEB: "web" + WEB: "web", +}; + +export const GuestHomedIn = { + HOSTAZUREAD: "hostazuread", + ONPREM: "onprem", }; diff --git a/samples/e2eTestUtils/src/LabApiQueryParams.ts b/samples/e2eTestUtils/src/LabApiQueryParams.ts index 693f155e47..de4213c655 100644 --- a/samples/e2eTestUtils/src/LabApiQueryParams.ts +++ b/samples/e2eTestUtils/src/LabApiQueryParams.ts @@ -3,13 +3,14 @@ * See: https://msidlab.com/swagger/v1/swagger.json */ export type LabApiQueryParams = { - userType?: string, - azureEnvironment?: string, - federationProvider?: string, - b2cProvider?: string, - homeDomain?: string, - appType?: string, - signInAudience?: string, - publicClient?: string, - appPlatform?: string, + userType?: string; + azureEnvironment?: string; + federationProvider?: string; + b2cProvider?: string; + homeDomain?: string; + appType?: string; + signInAudience?: string; + publicClient?: string; + appPlatform?: string; + guestHomedIn?: string; }; diff --git a/samples/e2eTestUtils/src/LabClient.ts b/samples/e2eTestUtils/src/LabClient.ts index 38234d56eb..3dbd479e45 100644 --- a/samples/e2eTestUtils/src/LabClient.ts +++ b/samples/e2eTestUtils/src/LabClient.ts @@ -1,13 +1,17 @@ import { ClientSecretCredential, AccessToken } from "@azure/identity"; import axios from "axios"; -import { ENV_VARIABLES, LAB_SCOPE, LAB_API_ENDPOINT, ParamKeys } from "./Constants"; +import { + ENV_VARIABLES, + LAB_SCOPE, + LAB_API_ENDPOINT, + ParamKeys, +} from "./Constants"; import { LabApiQueryParams } from "./LabApiQueryParams"; import * as dotenv from "dotenv"; dotenv.config({ path: __dirname + `/../../../.env` }); export class LabClient { - private credentials: ClientSecretCredential; private currentToken: AccessToken | null; constructor() { @@ -18,7 +22,11 @@ export class LabClient { if (!tenant || !clientId || !client_secret) { throw "Environment variables not set!"; } - this.credentials = new ClientSecretCredential(tenant, clientId, client_secret); + this.credentials = new ClientSecretCredential( + tenant, + clientId, + client_secret + ); } private async getCurrentToken(): Promise { @@ -34,12 +42,15 @@ export class LabClient { return this.currentToken.token; } - private async requestLabApi(endpoint: string, accessToken: string): Promise { + private async requestLabApi( + endpoint: string, + accessToken: string + ): Promise { try { const response = await axios(`${LAB_API_ENDPOINT}${endpoint}`, { headers: { - "Authorization": `Bearer ${accessToken}` - } + Authorization: `Bearer ${accessToken}`, + }, }); return response.data; } catch (e) { @@ -55,12 +66,16 @@ export class LabClient { * @param labApiParams * @returns */ - async getVarsByCloudEnvironment(labApiParams: LabApiQueryParams): Promise { + async getVarsByCloudEnvironment( + labApiParams: LabApiQueryParams + ): Promise { const accessToken = await this.getCurrentToken(); const apiParams: Array = []; if (labApiParams.azureEnvironment) { - apiParams.push(`${ParamKeys.AZURE_ENVIRONMENT}=${labApiParams.azureEnvironment}`); + apiParams.push( + `${ParamKeys.AZURE_ENVIRONMENT}=${labApiParams.azureEnvironment}` + ); } if (labApiParams.userType) { @@ -68,15 +83,21 @@ export class LabClient { } if (labApiParams.federationProvider) { - apiParams.push(`${ParamKeys.FEDERATION_PROVIDER}=${labApiParams.federationProvider}`); + apiParams.push( + `${ParamKeys.FEDERATION_PROVIDER}=${labApiParams.federationProvider}` + ); } if (labApiParams.b2cProvider) { - apiParams.push(`${ParamKeys.B2C_PROVIDER}=${labApiParams.b2cProvider}`); + apiParams.push( + `${ParamKeys.B2C_PROVIDER}=${labApiParams.b2cProvider}` + ); } if (labApiParams.homeDomain) { - apiParams.push(`${ParamKeys.HOME_DOMAIN}=${labApiParams.homeDomain}`); + apiParams.push( + `${ParamKeys.HOME_DOMAIN}=${labApiParams.homeDomain}` + ); } if (labApiParams.appType) { @@ -84,15 +105,27 @@ export class LabClient { } if (labApiParams.signInAudience) { - apiParams.push(`${ParamKeys.SIGN_IN_AUDIENCE}=${labApiParams.signInAudience}`); + apiParams.push( + `${ParamKeys.SIGN_IN_AUDIENCE}=${labApiParams.signInAudience}` + ); } if (labApiParams.publicClient) { - apiParams.push(`${ParamKeys.PUBLIC_CLIENT}=${labApiParams.publicClient}`); + apiParams.push( + `${ParamKeys.PUBLIC_CLIENT}=${labApiParams.publicClient}` + ); } if (labApiParams.appPlatform) { - apiParams.push(`${ParamKeys.APP_PLATFORM}=${labApiParams.appPlatform}`); + apiParams.push( + `${ParamKeys.APP_PLATFORM}=${labApiParams.appPlatform}` + ); + } + + if (labApiParams.guestHomedIn) { + apiParams.push( + `${ParamKeys.GUEST_HOMED_IN}=${labApiParams.guestHomedIn}` + ); } if (apiParams.length <= 0) { @@ -106,6 +139,9 @@ export class LabClient { async getSecret(secretName: string): Promise { const accessToken = await this.getCurrentToken(); - return await this.requestLabApi(`/LabSecret?&Secret=${secretName}`, accessToken); + return await this.requestLabApi( + `/LabSecret?&Secret=${secretName}`, + accessToken + ); } } diff --git a/samples/e2eTestUtils/src/MsidUser.ts b/samples/e2eTestUtils/src/MsidUser.ts index 4d2d61cde3..81bd5b1679 100644 --- a/samples/e2eTestUtils/src/MsidUser.ts +++ b/samples/e2eTestUtils/src/MsidUser.ts @@ -1,13 +1,15 @@ export type MsidUser = { - objectId?: string, - homeObjectId?: string, - displayName?: string, - givenName?: string, - surName?: string, - upn?: string, - mfa?: string, - homeDomain?: string, - tenantID?: string, - homeTenantID?: string, - b2cProvider?: string + objectId?: string; + homeObjectId?: string; + displayName?: string; + givenName?: string; + surName?: string; + upn?: string; + mfa?: string; + homeDomain?: string; + tenantID?: string; + homeTenantID?: string; + b2cProvider?: string; + userType?: string; + homeUPN?: string; }; diff --git a/samples/e2eTestUtils/src/TestUtils.ts b/samples/e2eTestUtils/src/TestUtils.ts index 9878c74209..c5bb962c8a 100644 --- a/samples/e2eTestUtils/src/TestUtils.ts +++ b/samples/e2eTestUtils/src/TestUtils.ts @@ -130,15 +130,22 @@ export async function retrieveAppConfiguration( export async function setupCredentials( labConfig: LabConfig, labClient: LabClient -): Promise<[string, string]> { +): Promise<[string, string, string]> { let username = ""; let accountPwd = ""; + let guestUsername = ""; + + const { user, lab } = labConfig; - if (labConfig.user.upn) { - username = labConfig.user.upn; + if (user.userType === "Guest") { + console.log(user); + username = user.homeUPN; + guestUsername = user.upn; + } else { + username = user.upn; } - if (!labConfig.lab.labName) { + if (!lab.labName) { throw Error("No Labname provided!"); } @@ -150,7 +157,7 @@ export async function setupCredentials( throw "Unable to get account password!"; } - return [username, accountPwd]; + return [username, accountPwd, guestUsername]; } export async function b2cLocalAccountEnterCredentials( diff --git a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/auth.js b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/auth.js index a1ee6b6583..7ef0021b05 100644 --- a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/auth.js +++ b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/auth.js @@ -1,18 +1,18 @@ -let signInType; let homeAccountId = ""; // Create the main myMSALObj instance // configuration parameters are located at authConfig.js -let myMSALObj; -let authConfig; +let myMSALObj, requestConfig, tenantConfig, signInType; + initializeMsal(); async function initializeMsal() { return fetch("testConfig.json").then(response => { return response.json(); - }).then(json => { - authConfig = json; - myMSALObj = new msal.PublicClientApplication(json.msalConfig); + }).then((authConfig) => { + myMSALObj = new msal.PublicClientApplication(authConfig.msalConfig); + requestConfig = authConfig.request; + tenantConfig = authConfig.tenants; myMSALObj.initialize().then(() => { setInitializedFlagTrue(); // Used as a flag in the test to ensure that MSAL has been initialized myMSALObj.handleRedirectPromise().then(handleResponse).catch(err => { @@ -33,9 +33,10 @@ function handleResponse(resp) { if (resp.accessToken) { updateUI(resp); } + return resp; } else { // need to call getAccount here? - const currentAccounts = myMSALObj.getAllAccounts(); + const currentAccounts = myMSALObj.getAllAccounts({ isHomeTenant: true }); if (currentAccounts === null) { return; } else if (currentAccounts.length > 1) { @@ -49,17 +50,17 @@ function handleResponse(resp) { async function signIn(signInType) { if (signInType === "popup") { - return myMSALObj.loginPopup(authConfig.request).then(handleResponse).catch(function (error) { + return myMSALObj.loginPopup(requestConfig).then(handleResponse).catch(function (error) { console.log(error); }); } else if (signInType === "redirect") { - return myMSALObj.loginRedirect(authConfig.request) + return myMSALObj.loginRedirect(requestConfig) } } function signOut(signOutType) { const logoutRequest = { - account: myMSALObj.getAccountByHomeId(homeAccountId) + account: myMSALObj.getAccount({ homeAccountId, isHomeTenant: true } ) }; if (signOutType === "popup") { @@ -70,8 +71,8 @@ function signOut(signOutType) { } async function getTokenPopup() { - const request = authConfig.request; - const currentAcc = myMSALObj.getAccountByHomeId(homeAccountId); + const request = requestConfig; + const currentAcc = myMSALObj.getAccount({ homeAccountId, isHomeTenant: true } ); if (currentAcc) { request.account = currentAcc; response = await myMSALObj.acquireTokenPopup(request).then(handleResponse).catch(error => { @@ -81,8 +82,8 @@ async function getTokenPopup() { } async function getTokenRedirect() { - const request = authConfig.request; - const currentAcc = myMSALObj.getAccountByHomeId(homeAccountId); + const request = requestConfig; + const currentAcc = myMSALObj.getAccount({ homeAccountId, isHomeTenant: true } ); if (currentAcc) { request.account = currentAcc; myMSALObj.acquireTokenRedirect(request); @@ -90,8 +91,8 @@ async function getTokenRedirect() { } async function getTokenSilently() { - const request = authConfig.request; - const currentAcc = myMSALObj.getAccountByHomeId(homeAccountId); + const request = requestConfig; + const currentAcc = myMSALObj.getAccount({ homeAccountId, isHomeTenant: true } ); if (currentAcc) { request.account = currentAcc; response = await myMSALObj.acquireTokenSilent(request).then(handleResponse).catch(error => { @@ -99,3 +100,27 @@ async function getTokenSilently() { }); } } + +async function getGuestTokenSilently() { + const request = requestConfig; + if (tenantConfig.guest) { + const guestAccount = myMSALObj.getAccount({ homeAccountId, tenantId: tenantConfig.guest.tenantId } ); + if (guestAccount) { + response = await myMSALObj.acquireTokenSilent({ ...request, account: guestAccount }).then(handleResponse).catch(error => { + console.error(error); + }); + } else { + const homeAccount = myMSALObj.getAccount({ homeAccountId, isHomeTenant: true } ); + response = await myMSALObj.acquireTokenSilent({ + ...request, + account: homeAccount, + authority: tenantConfig.guest.authority, + cacheLookupPolicy: msal.CacheLookupPolicy.RefreshToken + }).then(handleResponse).catch(error => { + console.error(error); + }); + } + } else { + console.error("No guest tenant in MSAL Config"); + } +} diff --git a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/authConfigs/aadMultiTenantAuthConfig.json b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/authConfigs/aadMultiTenantAuthConfig.json new file mode 100644 index 0000000000..3c9b81aeaa --- /dev/null +++ b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/authConfigs/aadMultiTenantAuthConfig.json @@ -0,0 +1,28 @@ +{ + "msalConfig": { + "auth": { + "clientId": "b5c2e510-4a17-4feb-b219-e55aa5b74144", + "authority": "https://login.microsoftonline.com/f645ad92-e38d-4d1a-b510-d1b09a74a8ca" + }, + "cache": { + "cacheLocation": "sessionStorage", + "storeAuthStateInCookie": false + }, + "system": { + "allowNativeBroker": false + } + }, + "request": { + "scopes": ["User.Read"] + }, + "tenants": { + "home": { + "tenantId": "f645ad92-e38d-4d1a-b510-d1b09a74a8ca", + "authority": "https://login.microsoftonline.com/f645ad92-e38d-4d1a-b510-d1b09a74a8ca" + }, + "guest": { + "tenantId": "8e44f19d-bbab-4a82-b76b-4cd0a6fbc97a", + "authority": "https://login.microsoftonline.com/8e44f19d-bbab-4a82-b76b-4cd0a6fbc97a" + } + } +} diff --git a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/index.html b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/index.html index a8f6f6e94d..8b100eb5ac 100644 --- a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/index.html +++ b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/index.html @@ -48,6 +48,10 @@
Please sign-in to see your profile an
+
+
+ diff --git a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/test/browserAADMultiTenant.spec.ts b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/test/browserAADMultiTenant.spec.ts new file mode 100644 index 0000000000..a6fa79a3a1 --- /dev/null +++ b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/test/browserAADMultiTenant.spec.ts @@ -0,0 +1,363 @@ +import * as puppeteer from "puppeteer"; +import { + Screenshot, + createFolder, + setupCredentials, + enterCredentials, + ONE_SECOND_IN_MS, + clickLoginPopup, + clickLogoutPopup, + clickLogoutRedirect, + waitForReturnToApp, + getBrowser, + getHomeUrl, + pcaInitializedPoller, + BrowserCacheUtils, + LabApiQueryParams, + AzureEnvironments, + AppTypes, + LabClient, + UserTypes, +} from "e2e-test-utils"; +import { + msalConfig as aadMsalConfig, + request as aadTokenRequest, + tenants as aadTenants, +} from "../authConfigs/aadMultiTenantAuthConfig.json"; +import fs from "fs"; +import { GuestHomedIn } from "e2e-test-utils/src/Constants"; + +const SCREENSHOT_BASE_FOLDER_NAME = `${__dirname}/screenshots/default tests`; +let sampleHomeUrl = ""; + +async function verifyTokenStore( + BrowserCache: BrowserCacheUtils, + scopes: string[], + numberOfTenants: number = 1 +): Promise { + const tokenStore = await BrowserCache.getTokens(); + expect(tokenStore.idTokens).toHaveLength(1 * numberOfTenants); + expect(tokenStore.accessTokens).toHaveLength(1 * numberOfTenants); + expect(tokenStore.refreshTokens).toHaveLength(1); + + const account = await BrowserCache.getAccountFromCache( + tokenStore.idTokens[0] + ); + expect(account).toBeDefined(); + + expect(account["tenantProfiles"]).toHaveLength(numberOfTenants); + expect( + await BrowserCache.accessTokenForScopesExists( + tokenStore.accessTokens, + scopes, + numberOfTenants + ) + ).toBeTruthy(); + const storage = await BrowserCache.getWindowStorage(); + const crossTenantArtifactCount = 4; // 1 account object + 1 refresh token + 1 account keys array + 1 token keys array = 4 + const perTenantArtifactCount = 2; // 1 id token + 1 access token per tenant + expect(Object.keys(storage).length).toEqual( + crossTenantArtifactCount + numberOfTenants * perTenantArtifactCount + ); +} + +describe("AAD-Prod Tests", () => { + let browser: puppeteer.Browser; + let context: puppeteer.BrowserContext; + let page: puppeteer.Page; + let BrowserCache: BrowserCacheUtils; + let username = ""; + let accountPwd = ""; + let guestUsername = ""; + + beforeAll(async () => { + createFolder(SCREENSHOT_BASE_FOLDER_NAME); + browser = await getBrowser(); + sampleHomeUrl = getHomeUrl(); + + const labApiParams: LabApiQueryParams = { + azureEnvironment: AzureEnvironments.CLOUD, + appType: AppTypes.CLOUD, + userType: UserTypes.GUEST, + guestHomedIn: GuestHomedIn.HOSTAZUREAD, + }; + + const labClient = new LabClient(); + const envResponse = await labClient.getVarsByCloudEnvironment( + labApiParams + ); + + [username, accountPwd, guestUsername] = await setupCredentials( + envResponse[0], + labClient + ); + + fs.writeFileSync( + "./app/customizable-e2e-test/testConfig.json", + JSON.stringify({ + msalConfig: aadMsalConfig, + request: aadTokenRequest, + tenants: aadTenants, + }) + ); + }); + + afterAll(async () => { + await context.close(); + await browser.close(); + }); + + describe("logout Tests", () => { + let testName: string; + let screenshot: Screenshot; + + beforeEach(async () => { + context = await browser.createIncognitoBrowserContext(); + page = await context.newPage(); + page.setDefaultTimeout(ONE_SECOND_IN_MS * 5); + BrowserCache = new BrowserCacheUtils( + page, + aadMsalConfig.cache.cacheLocation + ); + await page.goto(sampleHomeUrl); + await pcaInitializedPoller(page, 5000); + + testName = "logoutBaseCase"; + screenshot = new Screenshot( + `${SCREENSHOT_BASE_FOLDER_NAME}/${testName}` + ); + const [popupPage, popupWindowClosed] = await clickLoginPopup( + screenshot, + page + ); + await enterCredentials(popupPage, screenshot, username, accountPwd); + await waitForReturnToApp( + screenshot, + page, + popupPage, + popupWindowClosed + ); + await pcaInitializedPoller(page, 5000); + }); + + afterEach(async () => { + await page.evaluate(() => + Object.assign({}, window.sessionStorage.clear()) + ); + await page.evaluate(() => + Object.assign({}, window.localStorage.clear()) + ); + await page.close(); + }); + + it("logoutRedirect", async () => { + await clickLogoutRedirect(screenshot, page); + expect( + page + .url() + .startsWith( + "https://login.microsoftonline.com/f645ad92-e38d-4d1a-b510-d1b09a74a8ca/" + ) + ).toBeTruthy(); + expect(page.url()).toContain("logout"); + // Skip server sign-out + const tokenStore = await BrowserCache.getTokens(); + expect(tokenStore.idTokens.length).toEqual(0); + expect(tokenStore.accessTokens.length).toEqual(0); + expect(tokenStore.refreshTokens.length).toEqual(0); + }); + + it("logoutPopup", async () => { + const [popupWindow, popupWindowClosed] = await clickLogoutPopup( + screenshot, + page + ); + expect( + popupWindow + .url() + .startsWith( + "https://login.microsoftonline.com/f645ad92-e38d-4d1a-b510-d1b09a74a8ca/" + ) + ).toBeTruthy(); + expect(popupWindow.url()).toContain("logout"); + await popupWindow.waitForNavigation(); + const tokenStore = await BrowserCache.getTokens(); + + expect(tokenStore.idTokens.length).toEqual(0); + expect(tokenStore.accessTokens.length).toEqual(0); + expect(tokenStore.refreshTokens.length).toEqual(0); + }); + }); + + describe("acquireToken Tests", () => { + let testName: string; + let screenshot: Screenshot; + + beforeAll(async () => { + context = await browser.createIncognitoBrowserContext(); + page = await context.newPage(); + page.setDefaultTimeout(ONE_SECOND_IN_MS * 5); + BrowserCache = new BrowserCacheUtils( + page, + aadMsalConfig.cache.cacheLocation + ); + await page.goto(sampleHomeUrl); + + testName = "acquireTokenBaseCase"; + screenshot = new Screenshot( + `${SCREENSHOT_BASE_FOLDER_NAME}/${testName}` + ); + const [popupPage, popupWindowClosed] = await clickLoginPopup( + screenshot, + page + ); + await enterCredentials(popupPage, screenshot, username, accountPwd); + await waitForReturnToApp( + screenshot, + page, + popupPage, + popupWindowClosed + ); + }); + + beforeEach(async () => { + await page.reload(); + await page.waitForSelector("#WelcomeMessage"); + await pcaInitializedPoller(page, 5000); + }); + + afterAll(async () => { + await page.evaluate(() => + Object.assign({}, window.sessionStorage.clear()) + ); + await page.evaluate(() => + Object.assign({}, window.localStorage.clear()) + ); + await page.close(); + }); + + it("acquireTokenRedirect from home tenant", async () => { + testName = "acquireTokenRedirectFromHomeTenant"; + screenshot = new Screenshot( + `${SCREENSHOT_BASE_FOLDER_NAME}/${testName}` + ); + await page.waitForSelector("#acquireTokenRedirect"); + + // Remove access_tokens from cache so we can verify acquisition + const tokenStore = await BrowserCache.getTokens(); + await BrowserCache.removeTokens(tokenStore.refreshTokens); + await BrowserCache.removeTokens(tokenStore.accessTokens); + await page.click("#acquireTokenRedirect"); + await page.waitForSelector("#scopes-acquired"); + await screenshot.takeScreenshot( + page, + "acquireTokenRedirectGotTokens" + ); + + // Verify browser cache contains Account, idToken, AccessToken and RefreshToken + await verifyTokenStore(BrowserCache, aadTokenRequest.scopes); + }); + + it("acquireTokenSilent from cache (home tenant token)", async () => { + testName = "acquireTokenSilentCache"; + screenshot = new Screenshot( + `${SCREENSHOT_BASE_FOLDER_NAME}/${testName}` + ); + await page.waitForSelector("#acquireTokenSilent"); + await page.click("#acquireTokenSilent"); + await page.waitForSelector("#scopes-acquired"); + await screenshot.takeScreenshot( + page, + "acquireTokenSilent-fromCache-GotTokens" + ); + + const telemetryCacheEntry = + await BrowserCache.getTelemetryCacheEntry( + aadMsalConfig.auth.clientId + ); + expect(telemetryCacheEntry).toBeDefined(); + expect(telemetryCacheEntry["cacheHits"]).toEqual(1); + // Remove Telemetry Cache entry for next test + await BrowserCache.removeTokens([ + BrowserCacheUtils.getTelemetryKey(aadMsalConfig.auth.clientId), + ]); + + // Verify browser cache contains Account, idToken, AccessToken and RefreshToken + await verifyTokenStore(BrowserCache, aadTokenRequest.scopes); + }); + + it("acquireTokenSilent via RefreshToken (home tenant token)", async () => { + testName = "acquireTokenSilentRTHome"; + screenshot = new Screenshot( + `${SCREENSHOT_BASE_FOLDER_NAME}/${testName}` + ); + await page.waitForSelector("#acquireTokenSilent"); + + // Remove access_tokens from cache so we can verify acquisition + const tokenStore = await BrowserCache.getTokens(); + await BrowserCache.removeTokens(tokenStore.accessTokens); + + await page.click("#acquireTokenSilent"); + await page.waitForSelector("#scopes-acquired"); + await screenshot.takeScreenshot( + page, + "acquireTokenSilent-viaRefresh-home-GotTokens" + ); + + // Verify browser cache contains Account, idToken, AccessToken and RefreshToken + await verifyTokenStore(BrowserCache, aadTokenRequest.scopes); + }); + + it("acquireTokenSilent via RefreshToken (guest tenant token)", async () => { + testName = "acquireTokenSilentRTGuest"; + screenshot = new Screenshot( + `${SCREENSHOT_BASE_FOLDER_NAME}/${testName}` + ); + await page.waitForSelector("#acquireGuestToken"); + await page.click("#acquireGuestToken"); + await page.waitForSelector("#scopes-acquired"); + await screenshot.takeScreenshot( + page, + "acquireTokenSilent-viaRefresh-guest-GotTokens" + ); + + // Verify browser cache contains Account, idToken, AccessToken and RefreshToken + await verifyTokenStore( + BrowserCache, + aadTokenRequest.scopes, + 2 // two tenants authenticated with + ); + }); + + it("acquireTokenSilent from cache (guest tenant token)", async () => { + testName = "acquireTokenSilentCacheGuest"; + screenshot = new Screenshot( + `${SCREENSHOT_BASE_FOLDER_NAME}/${testName}` + ); + await page.click("#acquireGuestToken"); + await page.waitForSelector("#scopes-acquired"); + await screenshot.takeScreenshot( + page, + "acquireTokenSilent-fromCache-GotTokens" + ); + + const telemetryCacheEntry = + await BrowserCache.getTelemetryCacheEntry( + aadMsalConfig.auth.clientId + ); + expect(telemetryCacheEntry).toBeDefined(); + expect(telemetryCacheEntry["cacheHits"]).toEqual(1); + // Remove Telemetry Cache entry for next test + await BrowserCache.removeTokens([ + BrowserCacheUtils.getTelemetryKey(aadMsalConfig.auth.clientId), + ]); + + // Verify browser cache contains Account, idToken, AccessToken and RefreshToken + await verifyTokenStore( + BrowserCache, + aadTokenRequest.scopes, + 2 // two tenants authenticated with + ); + }); + }); +}); From 78b5d9a965dc493cb2c328e87f96697649ae8ef8 Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Wed, 22 Nov 2023 17:15:54 -0800 Subject: [PATCH 74/91] Update test utils --- samples/e2eTestUtils/src/TestUtils.ts | 39 ++++++------------- .../VanillaJSTestApp2.0/package.json | 1 + 2 files changed, 13 insertions(+), 27 deletions(-) diff --git a/samples/e2eTestUtils/src/TestUtils.ts b/samples/e2eTestUtils/src/TestUtils.ts index c5bb962c8a..d371e5dbce 100644 --- a/samples/e2eTestUtils/src/TestUtils.ts +++ b/samples/e2eTestUtils/src/TestUtils.ts @@ -1,11 +1,15 @@ import * as fs from "fs"; -import { Page, HTTPResponse, Browser } from "puppeteer"; +import { Page, HTTPResponse, Browser, WaitForOptions } from "puppeteer"; import { LabConfig } from "./LabConfig"; import { LabClient } from "./LabClient"; export const ONE_SECOND_IN_MS = 1000; export const RETRY_TIMES = 5; +const WAIT_FOR_NAVIGATION_CONFIG: WaitForOptions = { + waitUntil: ["load", "domcontentloaded", "networkidle0"], +}; + export class Screenshot { private folderName: string; private screenshotNum: number; @@ -138,7 +142,6 @@ export async function setupCredentials( const { user, lab } = labConfig; if (user.userType === "Guest") { - console.log(user); username = user.homeUPN; guestUsername = user.upn; } else { @@ -215,11 +218,7 @@ export async function enterCredentials( accountPwd: string ): Promise { await Promise.all([ - page - .waitForNavigation({ - waitUntil: ["load", "domcontentloaded", "networkidle0"], - }) - .catch(() => {}), // Wait for navigation but don't throw due to timeout + page.waitForNavigation(WAIT_FOR_NAVIGATION_CONFIG).catch(() => {}), // Wait for navigation but don't throw due to timeout page.waitForSelector("#i0116"), page.waitForSelector("#idSIButton9"), ]).catch(async (e) => { @@ -244,9 +243,7 @@ export async function enterCredentials( await page.waitForSelector("#aadTile", { timeout: 1000 }); await screenshot.takeScreenshot(page, "accountType"); await Promise.all([ - page.waitForNavigation({ - waitUntil: ["load", "domcontentloaded", "networkidle0"], - }), + page.waitForNavigation(WAIT_FOR_NAVIGATION_CONFIG), page.click("#aadTile"), ]).catch(async (e) => { await screenshot.takeScreenshot(page, "errorPage").catch(() => {}); @@ -267,9 +264,7 @@ export async function enterCredentials( // Wait either for another navigation to Keep me signed in page or back to redirectUri Promise.race([ - page.waitForNavigation({ - waitUntil: ["load", "domcontentloaded", "networkidle0"], - }), + page.waitForNavigation(WAIT_FOR_NAVIGATION_CONFIG), page.waitForResponse( (response: HTTPResponse) => response.url().startsWith(SAMPLE_HOME_URL), @@ -301,9 +296,7 @@ export async function enterCredentials( await page.waitForSelector("#idSIButton9", { timeout: 1000 }); await screenshot.takeScreenshot(page, "keepMeSignedInPage"); await Promise.all([ - page.waitForNavigation({ - waitUntil: ["load", "domcontentloaded", "networkidle0"], - }), + page.waitForNavigation(WAIT_FOR_NAVIGATION_CONFIG), page.click("#idSIButton9"), ]).catch(async (e) => { await screenshot.takeScreenshot(page, "errorPage").catch(() => {}); @@ -318,9 +311,7 @@ export async function enterCredentials( await page.waitForSelector("#idSIButton9", { timeout: 1000 }); await screenshot.takeScreenshot(page, "privateTenantSignInPage"); await Promise.all([ - page.waitForNavigation({ - waitUntil: ["load", "domcontentloaded", "networkidle0"], - }), + page.waitForNavigation(WAIT_FOR_NAVIGATION_CONFIG), page.click("#idSIButton9"), ]).catch(async (e) => { await screenshot.takeScreenshot(page, "errorPage").catch(() => {}); @@ -340,9 +331,7 @@ export async function approveRemoteConnect( await page.waitForSelector("#remoteConnectSubmit"); await screenshot.takeScreenshot(page, "remoteConnectPage"); await Promise.all([ - page.waitForNavigation({ - waitUntil: ["load", "domcontentloaded", "networkidle0"], - }), + page.waitForNavigation(WAIT_FOR_NAVIGATION_CONFIG), page.click("#remoteConnectSubmit"), ]).catch(async (e) => { await screenshot.takeScreenshot(page, "errorPage").catch(() => {}); @@ -405,11 +394,7 @@ export async function enterCredentialsADFS( accountPwd: string ): Promise { await Promise.all([ - page - .waitForNavigation({ - waitUntil: ["load", "domcontentloaded", "networkidle0"], - }) - .catch(() => {}), // Wait for navigation but don't throw due to timeout + page.waitForNavigation(WAIT_FOR_NAVIGATION_CONFIG).catch(() => {}), // Wait for navigation but don't throw due to timeout page.waitForSelector("#i0116"), page.waitForSelector("#idSIButton9"), ]).catch(async (e) => { diff --git a/samples/msal-browser-samples/VanillaJSTestApp2.0/package.json b/samples/msal-browser-samples/VanillaJSTestApp2.0/package.json index 7b1adb6c19..0ba05702d7 100644 --- a/samples/msal-browser-samples/VanillaJSTestApp2.0/package.json +++ b/samples/msal-browser-samples/VanillaJSTestApp2.0/package.json @@ -6,6 +6,7 @@ "main": "server.js", "scripts": { "start": "node server.js", + "e2e:sample": "node server.js -s customizable-e2e-test", "build:package": "cd ../../../lib/msal-browser && npm run build:all", "start:build": "npm run build:package && npm start", "test:e2e": "jest --runInBand", From 8a8623c99d1212b888ab5e706b5834f366579be2 Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Wed, 22 Nov 2023 17:17:06 -0800 Subject: [PATCH 75/91] Remove multi-tenant sample app --- .../app/multi-tenant/auth.js | 112 ------------------ .../app/multi-tenant/authConfig.js | 67 ----------- .../app/multi-tenant/graph.js | 32 ----- .../app/multi-tenant/index.html | 76 ------------ .../app/multi-tenant/redirect.html | 6 - .../app/multi-tenant/ui.js | 76 ------------ 6 files changed, 369 deletions(-) delete mode 100644 samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/auth.js delete mode 100644 samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/authConfig.js delete mode 100644 samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/graph.js delete mode 100644 samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/index.html delete mode 100644 samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/redirect.html delete mode 100644 samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/ui.js diff --git a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/auth.js b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/auth.js deleted file mode 100644 index 545e5d32e9..0000000000 --- a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/auth.js +++ /dev/null @@ -1,112 +0,0 @@ -let signInType; -let accountId = ""; - -// Create the main myMSALObj instance -// configuration parameters are located at authConfig.js -const myMSALObj = new msal.PublicClientApplication(msalConfig); - -// Redirect: once login is successful and redirects with tokens, call Graph API -myMSALObj.initialize().then(() => { - myMSALObj.handleRedirectPromise().then(handleResponse).catch(err => { - console.error(err); - }); -}); - -function handleResponse() { - // when a filter is passed into getAllAccounts, it returns all cached accounts that match the filter. Use isHomeTenant filter to get the home accounts. - const allAccounts = myMSALObj.getAllAccounts(); - console.log("Get all accounts: ", allAccounts); - if (!allAccounts || allAccounts.length < 1) { - return; - } else if (allAccounts.length >= 1) { - // Get all accounts returns the homeAccount with tenantProfiles when multiTenantAccountsEnabled is set to true - pickActiveAccountAndTenantProfile(allAccounts[0]); - } -} - -// Determines whether there is one or multiple tenant profiles to pick from and sets the active account based on the user selection if necessary. -async function pickActiveAccountAndTenantProfile(account) { - // Set home tenant profile as default active account - let activeAccount = myMSALObj.getActiveAccount(); - if (!activeAccount) { - activeAccount = account; - myMSALObj.setActiveAccount(activeAccount); - } - accountId = activeAccount.homeAccountId; - showWelcomeMessage(activeAccount); - showTenantProfilePicker(account.tenantProfiles || new Map(), activeAccount); -} - -async function setActiveAccount(tenantId) { - console.log("Set to: ", tenantId); - // Sets the active account to the cached account object matching the tenant profile selected by the user. - let activeAccount = myMSALObj.getActiveAccount(); - const newActiveAccount = myMSALObj.getAccount({ tenantId: tenantId }); - console.log(newActiveAccount); - if (newActiveAccount) { - myMSALObj.setActiveAccount(newActiveAccount); - accountId = activeAccount.homeAccountId; - } - handleResponse(); -} - -async function signIn(signInType) { - if (signInType === "popup") { - return myMSALObj.loginPopup(loginRequest).then(handleResponse).catch(function (error) { - console.log(error); - }); - } else if (signInType === "redirect") { - return myMSALObj.loginRedirect(loginRequest) - } -} - -function signOut(interactionType) { - const logoutRequest = { - account: myMSALObj.getActiveAccount() - }; - - if (interactionType === "popup") { - myMSALObj.logoutPopup(logoutRequest).then(() => { - window.location.reload(); - }); - } else { - myMSALObj.logoutRedirect(logoutRequest); - } -} - -async function requestGuestToken() { - const currentAcc = myMSALObj.getAccountByHomeId(accountId); - if (currentAcc) { - - const response = await sso(guestTenantRequest, currentAcc.idTokenClaims.login_hint); - console.log(response); - callMSGraph(graphConfig.graphMeEndpoint, response.accessToken, updateUI); - guestProfileButton.style.display = 'none'; - } -} - -async function getTokenRedirect(request) { - return await myMSALObj.acquireTokenSilent(request).catch(async (error) => { - console.log("silent token acquisition fails."); - if (error instanceof msal.InteractionRequiredAuthError) { - // fallback to interaction when silent call fails - console.log("acquiring token using redirect"); - myMSALObj.acquireTokenRedirect(request); - } else { - console.error(error); - } - }); -} - -async function sso(request, loginHint) { - return await myMSALObj.ssoSilent(request, loginHint).catch(async (error) => { - console.log("sso failed"); - if (error instanceof msal.InteractionRequiredAuthError) { - // fallback to interaction when silent call fails - console.log("acquiring token using redirect"); - myMSALObj.acquireTokenRedirect(request); - } else { - console.error(error); - } - }) -} \ No newline at end of file diff --git a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/authConfig.js b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/authConfig.js deleted file mode 100644 index 88b3b5f870..0000000000 --- a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/authConfig.js +++ /dev/null @@ -1,67 +0,0 @@ -const homeTenant = "HOME_TENANT_ID"; -const guestTenant = "GUEST_TENANT_ID" -const baseAuthority = "https://login.microsoftonline.com" - -// Config object to be passed to Msal on creation -const msalConfig = { - auth: { - clientId: "ENTER_CLIENT_ID", - authority: `${baseAuthority}/${homeTenant}` - }, - cache: { - cacheLocation: "localStorage", // This configures where your cache will be stored - storeAuthStateInCookie: false, // Set this to "true" if you are having issues on IE11 or Edge - }, - system: { - loggerOptions: { - logLevel: msal.LogLevel.Trace, - loggerCallback: (level, message, containsPii) => { - if (containsPii) { - return; - } - switch (level) { - case msal.LogLevel.Error: - console.error(message); - return; - case msal.LogLevel.Info: - console.info(message); - return; - case msal.LogLevel.Verbose: - console.debug(message); - return; - case msal.LogLevel.Warning: - console.warn(message); - return; - default: - console.log(message); - return; - } - } - } - }, - telemetry: { - application: { - appName: "MSAL Browser V2 Multi-tenant Sample", - appVersion: "1.0.0" - } - } -}; - -// Add here scopes for id token to be used at MS Identity Platform endpoints. -const loginRequest = { - scopes: ["User.Read"] -}; - -// Add here the endpoints for MS Graph API services you would like to use. -const graphConfig = { - graphMeEndpoint: "https://graph.microsoft.com/v1.0/me", -}; - -const silentRequest = { - scopes: ["openid", "profile", "User.Read"] -}; - -const guestTenantRequest = { - ...loginRequest, - authority: `${baseAuthority}/${guestTenant}` -} \ No newline at end of file diff --git a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/graph.js b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/graph.js deleted file mode 100644 index bf66627fa1..0000000000 --- a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/graph.js +++ /dev/null @@ -1,32 +0,0 @@ -// Helper function to call MS Graph API endpoint -// using authorization bearer token scheme -function callMSGraph(endpoint, accessToken, callback) { - const headers = new Headers(); - const bearer = `Bearer ${accessToken}`; - - headers.append("Authorization", bearer); - - const options = { - method: "GET", - headers: headers - }; - - console.log('request made to Graph API at: ' + new Date().toString()); - - fetch(endpoint, options) - .then(response => response.json()) - .then(response => callback(response, endpoint)) - .catch(error => console.log(error)); -} - -async function seeProfile() { - const currentAcc = myMSALObj.getActiveAccount(); - if (currentAcc) { - const request = {...loginRequest, authority: `${baseAuthority}/${currentAcc.tenantId}`}; - const response = await getTokenRedirect(request, currentAcc).catch(error => { - console.log(error); - }); - console.log(response); - callMSGraph(graphConfig.graphMeEndpoint, response.accessToken, updateUI); - } -} \ No newline at end of file diff --git a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/index.html b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/index.html deleted file mode 100644 index 68c5040ad1..0000000000 --- a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/index.html +++ /dev/null @@ -1,76 +0,0 @@ - - - - - - Quickstart | MSAL.JS Vanilla JavaScript SPA - - - - - - - - - -
-
Vanilla JavaScript SPA calling MS Graph API with MSAL.JS (w/ Multiple Tenants)
-
-
- -
-
- -
-
-
-
-
- -
-
-
-
- - - - - - - - - - - - - \ No newline at end of file diff --git a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/redirect.html b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/redirect.html deleted file mode 100644 index dcc8b0ed7d..0000000000 --- a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/redirect.html +++ /dev/null @@ -1,6 +0,0 @@ - \ No newline at end of file diff --git a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/ui.js b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/ui.js deleted file mode 100644 index f71d1e62db..0000000000 --- a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/multi-tenant/ui.js +++ /dev/null @@ -1,76 +0,0 @@ -// Select DOM elements to work with -const welcomeDiv = document.getElementById("WelcomeMessage"); -const pickProfileMessage = document.getElementById("PickProfileMessage"); -const signInDiv = document.getElementById("sign-in-div"); -const signOutDiv = document.getElementById("sign-out-div"); -const signInButton = document.getElementById("SignIn"); -const signOutButton = document.getElementById("SignOut"); -const popupButton = document.getElementById("popup"); -const redirectButton = document.getElementById("redirect"); -const cardDivTenantProfiles = document.getElementById("card-div-profiles"); -const cardDivGraphProfiles = document.getElementById("card-div-graph"); -const mailButton = document.getElementById("readMail"); -const profileButton = document.getElementById("seeProfile"); -const guestProfileButton = document.getElementById("seeProfileGuest"); -const tenantProfileDiv = document.getElementById("tenant-profile-div"); -const graphProfileDiv = document.getElementById("graph-profile-div"); - -function showWelcomeMessage(activeAccount) { - // Reconfiguring DOM elements - cardDivGraphProfiles.style.display = 'initial'; - welcomeDiv.innerHTML = `Welcome ${activeAccount.name}`; - signInButton.setAttribute('class', "btn btn-success dropdown-toggle"); - signInButton.innerHTML = "Sign Out"; - popupButton.setAttribute('onClick', "signOut(this.id)"); - popupButton.innerHTML = "Sign Out with Popup"; - redirectButton.setAttribute('onClick', "signOut(this.id)"); - redirectButton.innerHTML = "Sign Out with Redirect"; -} - -function createTenantProfileButton(tenantId, tenantProfile, activeTenantId) { - const tenantIdButton = document.createElement('button'); - const styleClass = (tenantId === activeTenantId) ? "btn btn-success" : "btn btn-primary"; - tenantIdButton.setAttribute('class', styleClass); - tenantIdButton.setAttribute('id', tenantId); - tenantIdButton.setAttribute('onClick', "setActiveAccount(this.id)"); - const label = `${tenantId}${ tenantProfile.isHomeTenant ? " (Home Tenant)" : "" }`; - tenantIdButton.innerHTML = label; - return tenantIdButton; -} - -function showTenantProfilePicker(tenantProfiles, activeAccount) { - // Reconfiguring DOM elements - cardDivTenantProfiles.style.display = 'initial'; - pickProfileMessage.innerHTML = "Select a tenant profile to set as the active account"; - tenantProfileDiv.innerHTML = ""; - tenantProfiles.forEach(function (tenantProfile, tenantId) { - tenantProfileDiv.appendChild(createTenantProfileButton(tenantId, tenantProfile, activeAccount.tenantId)); - tenantProfileDiv.appendChild(document.createElement('br')); - tenantProfileDiv.appendChild(document.createElement('br')); - }); - const newTenantProfileButton = document.createElement('button'); - newTenantProfileButton.setAttribute('class', "btn btn-primary"); - newTenantProfileButton.setAttribute('id', "new-tenant-button"); - newTenantProfileButton.setAttribute('onClick', "requestGuestToken()"); - newTenantProfileButton.innerHTML = "Add New Tenant Profile"; - tenantProfileDiv.appendChild(newTenantProfileButton); -} - -function updateUI(data, endpoint) { - console.log('Graph API responded at: ' + new Date().toString()); - if (endpoint === graphConfig.graphMeEndpoint) { - const title = document.createElement('p'); - title.innerHTML = "Title: " + data.jobTitle; - const email = document.createElement('p'); - email.innerHTML = "Mail: " + data.mail; - const phone = document.createElement('p'); - phone.innerHTML = "Phone: " + data.businessPhones[0]; - const address = document.createElement('p'); - address.innerHTML = "Location: " + data.officeLocation; - graphProfileDiv.innerHTML = ""; - graphProfileDiv.appendChild(title); - graphProfileDiv.appendChild(email); - graphProfileDiv.appendChild(phone); - graphProfileDiv.appendChild(address); - } -} From fe405c4cf2b7f387666a7c906be2bc9457788e1f Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Thu, 23 Nov 2023 13:53:43 -0800 Subject: [PATCH 76/91] Update browser cache test utils to ignore pop tokens in accessTokenForScopeExists --- .../e2eTestUtils/src/BrowserCacheTestUtils.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/samples/e2eTestUtils/src/BrowserCacheTestUtils.ts b/samples/e2eTestUtils/src/BrowserCacheTestUtils.ts index c8df77d1e1..727beec221 100644 --- a/samples/e2eTestUtils/src/BrowserCacheTestUtils.ts +++ b/samples/e2eTestUtils/src/BrowserCacheTestUtils.ts @@ -117,14 +117,19 @@ export class BrowserCacheUtils { ): Promise { const storage = await this.getWindowStorage(); - const matches = accessTokenKeys.filter((key) => { - const tokenVal = JSON.parse(storage[key]); - const tokenScopes = tokenVal.target.toLowerCase().split(" "); + const matches = accessTokenKeys + .filter((key) => { + // Ignore PoP tokens + return key.indexOf("accesstoken_with_authscheme") === -1; + }) + .filter((key) => { + const tokenVal = JSON.parse(storage[key]); + const tokenScopes = tokenVal.target.toLowerCase().split(" "); - return scopes.every((scope) => { - return tokenScopes.includes(scope.toLowerCase()); + return scopes.every((scope) => { + return tokenScopes.includes(scope.toLowerCase()); + }); }); - }); return matches.length === targetTokenMatchesNumber; } From 0ff394cf03c4a2e07ea3e8292dc04e52fbedbaf5 Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Thu, 23 Nov 2023 14:41:01 -0800 Subject: [PATCH 77/91] Hide http request log in VanillaJSTestApp when running customizable-e2e-test sample --- .../VanillaJSTestApp2.0/server.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/samples/msal-browser-samples/VanillaJSTestApp2.0/server.js b/samples/msal-browser-samples/VanillaJSTestApp2.0/server.js index 6b7ef6b885..32530e487f 100644 --- a/samples/msal-browser-samples/VanillaJSTestApp2.0/server.js +++ b/samples/msal-browser-samples/VanillaJSTestApp2.0/server.js @@ -37,8 +37,7 @@ if (argv.p) { port = argv.p; } -// Configure morgan module to log all requests. -app.use(morgan('dev')); +let logHttpRequests = true; // Set the front-end folder to serve public assets. app.use("/lib", express.static(path.join(__dirname, "../../../lib/msal-browser/lib"))); @@ -47,6 +46,9 @@ const sampleName = argv.sample; const isSample = sampleFolders.includes(sampleName); if (sampleName && isSample) { console.log(`Starting sample ${sampleName}`); + if (sampleName === "customizable-e2e-test") { + logHttpRequests = false; + } app.use(express.static('app/' + sampleName)); } else { if (sampleName && !isSample) { @@ -56,6 +58,12 @@ if (sampleName && isSample) { app.use(express.static('app/default')); } +if (logHttpRequests) { + // Configure morgan module to log all requests. + app.use(morgan('dev')); +} + + // set up a route for redirect.html. When using popup and silent APIs, // we recommend setting the redirectUri to a blank page or a page that does not implement MSAL. app.get("/redirect", function (req, res) { From d873cbf08235a6417619f3a827f8f8ca41ce0ce5 Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Thu, 23 Nov 2023 14:41:18 -0800 Subject: [PATCH 78/91] Fix realm matching logic in CacheManager --- lib/msal-common/src/cache/CacheManager.ts | 5 +---- .../app/customizable-e2e-test/test/browserB2C.spec.ts | 3 ++- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/msal-common/src/cache/CacheManager.ts b/lib/msal-common/src/cache/CacheManager.ts index 084ff1ab80..ca99254793 100644 --- a/lib/msal-common/src/cache/CacheManager.ts +++ b/lib/msal-common/src/cache/CacheManager.ts @@ -1146,7 +1146,6 @@ export abstract class CacheManager implements ICacheManager { // Use authority tenantId for cache lookup filter if it's defined, otherwise use tenantId from account passed in const requestTenantId = account.tenantId || getTenantFromAuthorityString(request.authority); - const tokenKeys = this.getTokenKeys(); const cachedAccount = this.readAccountFromCache(account); const cachedIdToken = this.getIdToken( @@ -1834,9 +1833,7 @@ export abstract class CacheManager implements ICacheManager { entity: AccountEntity | CredentialEntity, realm: string ): boolean { - return !!( - entity.realm?.toLowerCase() && realm === entity.realm.toLowerCase() - ); + return !!(entity.realm?.toLowerCase() === realm.toLowerCase()); } /** diff --git a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/test/browserB2C.spec.ts b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/test/browserB2C.spec.ts index b1470b1332..123dac0d82 100644 --- a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/test/browserB2C.spec.ts +++ b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/test/browserB2C.spec.ts @@ -47,7 +47,8 @@ async function verifyTokenStore( ) ).toBeTruthy(); const storage = await BrowserCache.getWindowStorage(); - expect(Object.keys(storage).length).toEqual(6); + // 1 AT + 1 IDT + 1 Account + 1 RT + 1 Account Keys + 1 Token Keys + 2 active account filters = 8 + expect(Object.keys(storage).length).toEqual(8); } describe("B2C Tests", () => { From 75c576a91d1e47b10914f0c1d1689d98319d2f1c Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Thu, 23 Nov 2023 14:41:49 -0800 Subject: [PATCH 79/91] Update customizable-e2e-sample to use activeAccount --- .../app/customizable-e2e-test/auth.js | 51 +++++++++++-------- 1 file changed, 29 insertions(+), 22 deletions(-) diff --git a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/auth.js b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/auth.js index 7ef0021b05..27dc31ee1d 100644 --- a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/auth.js +++ b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/auth.js @@ -1,5 +1,3 @@ -let homeAccountId = ""; - // Create the main myMSALObj instance // configuration parameters are located at authConfig.js let myMSALObj, requestConfig, tenantConfig, signInType; @@ -27,24 +25,32 @@ function setInitializedFlagTrue() { } function handleResponse(resp) { + let activeAccount; if (resp !== null) { - homeAccountId = resp.account.homeAccountId; - showWelcomeMessage(resp.account); + activeAccount = resp.account; + myMSALObj.setActiveAccount(activeAccount); + showWelcomeMessage(activeAccount); if (resp.accessToken) { updateUI(resp); } - return resp; } else { - // need to call getAccount here? - const currentAccounts = myMSALObj.getAllAccounts({ isHomeTenant: true }); - if (currentAccounts === null) { - return; - } else if (currentAccounts.length > 1) { - // Add choose account code here - } else if (currentAccounts.length === 1) { - homeAccountId = currentAccounts[0].homeAccountId; - showWelcomeMessage(currentAccounts[0]); + activeAccount = myMSALObj.getActiveAccount(); + if(!activeAccount) { + const currentAccounts = myMSALObj.getAllAccounts(); + if (currentAccounts.length === 0) { + return; + } else if (currentAccounts.length > 1) { + activeAccount = currentAccounts.sort((account) => { + return account.tenantId === account.homeAccountId.split(".")[1] ? -1 : 1; + })[0]; + } else if (currentAccounts.length === 1) { + activeAccount = currentAccounts[0]; + + } } + + myMSALObj.setActiveAccount(activeAccount); + showWelcomeMessage(activeAccount); } } @@ -60,7 +66,7 @@ async function signIn(signInType) { function signOut(signOutType) { const logoutRequest = { - account: myMSALObj.getAccount({ homeAccountId, isHomeTenant: true } ) + account: myMSALObj.getActiveAccount() }; if (signOutType === "popup") { @@ -72,7 +78,7 @@ function signOut(signOutType) { async function getTokenPopup() { const request = requestConfig; - const currentAcc = myMSALObj.getAccount({ homeAccountId, isHomeTenant: true } ); + const currentAcc = myMSALObj.getActiveAccount(); if (currentAcc) { request.account = currentAcc; response = await myMSALObj.acquireTokenPopup(request).then(handleResponse).catch(error => { @@ -83,7 +89,7 @@ async function getTokenPopup() { async function getTokenRedirect() { const request = requestConfig; - const currentAcc = myMSALObj.getAccount({ homeAccountId, isHomeTenant: true } ); + const currentAcc = myMSALObj.getActiveAccount(); if (currentAcc) { request.account = currentAcc; myMSALObj.acquireTokenRedirect(request); @@ -92,7 +98,7 @@ async function getTokenRedirect() { async function getTokenSilently() { const request = requestConfig; - const currentAcc = myMSALObj.getAccount({ homeAccountId, isHomeTenant: true } ); + const currentAcc = myMSALObj.getActiveAccount(); if (currentAcc) { request.account = currentAcc; response = await myMSALObj.acquireTokenSilent(request).then(handleResponse).catch(error => { @@ -103,14 +109,15 @@ async function getTokenSilently() { async function getGuestTokenSilently() { const request = requestConfig; - if (tenantConfig.guest) { - const guestAccount = myMSALObj.getAccount({ homeAccountId, tenantId: tenantConfig.guest.tenantId } ); + if (tenantConfig?.guest) { + const currentAcc = myMSALObj.getActiveAccount(); + const guestAccount = myMSALObj.getAccount({ homeAccountId: currentAcc?.homeAccountId, tenantId: tenantConfig.guest.tenantId } ); if (guestAccount) { response = await myMSALObj.acquireTokenSilent({ ...request, account: guestAccount }).then(handleResponse).catch(error => { console.error(error); }); } else { - const homeAccount = myMSALObj.getAccount({ homeAccountId, isHomeTenant: true } ); + const homeAccount = myMSALObj.getAccount({ homeAccountId } ); response = await myMSALObj.acquireTokenSilent({ ...request, account: homeAccount, @@ -121,6 +128,6 @@ async function getGuestTokenSilently() { }); } } else { - console.error("No guest tenant in MSAL Config"); + console.error("Sample Configuration Error: No guest tenant in MSAL Config"); } } From 9e476fb1d36453db44f188bc983e20b6f1880f86 Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Thu, 23 Nov 2023 16:55:23 -0800 Subject: [PATCH 80/91] Refactor verifyTokenStore browser cache util --- .../e2eTestUtils/src/BrowserCacheTestUtils.ts | 32 +++++++++ .../test/browserAAD.spec.ts | 61 ++++++++-------- .../test/browserAADMultiTenant.spec.ts | 61 +++++----------- .../test/browserAADTenanted.spec.ts | 61 ++++++++-------- .../test/browserB2C.spec.ts | 70 ++++++++++--------- .../test/localStorage.spec.ts | 29 ++------ 6 files changed, 151 insertions(+), 163 deletions(-) diff --git a/samples/e2eTestUtils/src/BrowserCacheTestUtils.ts b/samples/e2eTestUtils/src/BrowserCacheTestUtils.ts index 727beec221..e9c37fc996 100644 --- a/samples/e2eTestUtils/src/BrowserCacheTestUtils.ts +++ b/samples/e2eTestUtils/src/BrowserCacheTestUtils.ts @@ -206,4 +206,36 @@ export class BrowserCacheUtils { static getTelemetryKey(clientId: string): string { return "server-telemetry-" + clientId; } + + async verifyTokenStore(options: { + scopes: string[]; + idTokens?: number; + accessTokens?: number; + refreshTokens?: number; + numberOfTenants?: number; + }): Promise { + const tokenStore = await this.getTokens(); + const { scopes, idTokens, accessTokens, refreshTokens } = options; + const numberOfTenants = options.numberOfTenants || 1; + const totalIdTokens = (idTokens || 1) * numberOfTenants; + const totalAccessTokens = (accessTokens || 1) * numberOfTenants; + const totalRefreshTokens = refreshTokens || 1; + expect(tokenStore.idTokens).toHaveLength(totalIdTokens); + expect(tokenStore.accessTokens).toHaveLength(totalAccessTokens); + expect(tokenStore.refreshTokens).toHaveLength(refreshTokens || 1); + + const account = await this.getAccountFromCache(tokenStore.idTokens[0]); + expect(account).toBeDefined(); + expect(account["tenantProfiles"]).toHaveLength(numberOfTenants); + expect( + await this.accessTokenForScopesExists( + tokenStore.accessTokens, + scopes + ) + ).toBeTruthy(); + const storage = await this.getWindowStorage(); + expect(Object.keys(storage).length).toEqual( + totalIdTokens + totalAccessTokens + totalRefreshTokens + 5 // 1 Account + 1 Account Keys + 1 Token Keys + 2 active token filters = 5 + ); + } } diff --git a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/test/browserAAD.spec.ts b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/test/browserAAD.spec.ts index 2f5d9cdeb9..f634be4c4c 100644 --- a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/test/browserAAD.spec.ts +++ b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/test/browserAAD.spec.ts @@ -29,27 +29,6 @@ import { RedirectRequest } from "../../../../../../lib/msal-browser/src"; const SCREENSHOT_BASE_FOLDER_NAME = `${__dirname}/screenshots/default tests`; let sampleHomeUrl = ""; -async function verifyTokenStore( - BrowserCache: BrowserCacheUtils, - scopes: string[] -): Promise { - const tokenStore = await BrowserCache.getTokens(); - expect(tokenStore.idTokens).toHaveLength(1); - expect(tokenStore.accessTokens).toHaveLength(1); - expect(tokenStore.refreshTokens).toHaveLength(1); - expect( - await BrowserCache.getAccountFromCache(tokenStore.idTokens[0]) - ).toBeDefined(); - expect( - await BrowserCache.accessTokenForScopesExists( - tokenStore.accessTokens, - scopes - ) - ).toBeTruthy(); - const storage = await BrowserCache.getWindowStorage(); - expect(Object.keys(storage).length).toEqual(6); -} - describe("AAD-Prod Tests", () => { let browser: puppeteer.Browser; let context: puppeteer.BrowserContext; @@ -125,7 +104,9 @@ describe("AAD-Prod Tests", () => { await enterCredentials(page, screenshot, username, accountPwd); await waitForReturnToApp(screenshot, page); // Verify browser cache contains Account, idToken, AccessToken and RefreshToken - await verifyTokenStore(BrowserCache, aadTokenRequest.scopes); + await BrowserCache.verifyTokenStore({ + scopes: aadTokenRequest.scopes, + }); }); it("Performs loginRedirect from url with empty query string", async () => { @@ -139,7 +120,9 @@ describe("AAD-Prod Tests", () => { await enterCredentials(page, screenshot, username, accountPwd); await waitForReturnToApp(screenshot, page); // Verify browser cache contains Account, idToken, AccessToken and RefreshToken - await verifyTokenStore(BrowserCache, aadTokenRequest.scopes); + await BrowserCache.verifyTokenStore({ + scopes: aadTokenRequest.scopes, + }); expect(page.url()).toEqual(sampleHomeUrl); }); @@ -155,7 +138,9 @@ describe("AAD-Prod Tests", () => { await enterCredentials(page, screenshot, username, accountPwd); await waitForReturnToApp(screenshot, page); // Verify browser cache contains Account, idToken, AccessToken and RefreshToken - await verifyTokenStore(BrowserCache, aadTokenRequest.scopes); + await BrowserCache.verifyTokenStore({ + scopes: aadTokenRequest.scopes, + }); expect(page.url()).toEqual(testUrl); }); @@ -182,7 +167,9 @@ describe("AAD-Prod Tests", () => { await enterCredentials(page, screenshot, username, accountPwd); await waitForReturnToApp(screenshot, page); // Verify browser cache contains Account, idToken, AccessToken and RefreshToken - await verifyTokenStore(BrowserCache, aadTokenRequest.scopes); + await BrowserCache.verifyTokenStore({ + scopes: aadTokenRequest.scopes, + }); }); it("Performs loginRedirect with relative redirectStartPage", async () => { @@ -208,7 +195,9 @@ describe("AAD-Prod Tests", () => { await enterCredentials(page, screenshot, username, accountPwd); await waitForReturnToApp(screenshot, page); // Verify browser cache contains Account, idToken, AccessToken and RefreshToken - await verifyTokenStore(BrowserCache, aadTokenRequest.scopes); + await BrowserCache.verifyTokenStore({ + scopes: aadTokenRequest.scopes, + }); }); it("Performs loginPopup", async () => { @@ -230,7 +219,9 @@ describe("AAD-Prod Tests", () => { ); // Verify browser cache contains Account, idToken, AccessToken and RefreshToken - await verifyTokenStore(BrowserCache, aadTokenRequest.scopes); + await BrowserCache.verifyTokenStore({ + scopes: aadTokenRequest.scopes, + }); }); }); @@ -378,7 +369,9 @@ describe("AAD-Prod Tests", () => { ); // Verify browser cache contains Account, idToken, AccessToken and RefreshToken - await verifyTokenStore(BrowserCache, aadTokenRequest.scopes); + await BrowserCache.verifyTokenStore({ + scopes: aadTokenRequest.scopes, + }); }); it("acquireTokenPopup", async () => { @@ -397,7 +390,9 @@ describe("AAD-Prod Tests", () => { await screenshot.takeScreenshot(page, "acquireTokenPopupGotTokens"); // Verify browser cache contains Account, idToken, AccessToken and RefreshToken - await verifyTokenStore(BrowserCache, aadTokenRequest.scopes); + await BrowserCache.verifyTokenStore({ + scopes: aadTokenRequest.scopes, + }); }); it("acquireTokenSilent from Cache", async () => { @@ -425,7 +420,9 @@ describe("AAD-Prod Tests", () => { ]); // Verify browser cache contains Account, idToken, AccessToken and RefreshToken - await verifyTokenStore(BrowserCache, aadTokenRequest.scopes); + await BrowserCache.verifyTokenStore({ + scopes: aadTokenRequest.scopes, + }); }); it("acquireTokenSilent via RefreshToken", async () => { @@ -447,7 +444,9 @@ describe("AAD-Prod Tests", () => { ); // Verify browser cache contains Account, idToken, AccessToken and RefreshToken - await verifyTokenStore(BrowserCache, aadTokenRequest.scopes); + await BrowserCache.verifyTokenStore({ + scopes: aadTokenRequest.scopes, + }); }); }); }); diff --git a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/test/browserAADMultiTenant.spec.ts b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/test/browserAADMultiTenant.spec.ts index a6fa79a3a1..568e5333a2 100644 --- a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/test/browserAADMultiTenant.spec.ts +++ b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/test/browserAADMultiTenant.spec.ts @@ -30,37 +30,6 @@ import { GuestHomedIn } from "e2e-test-utils/src/Constants"; const SCREENSHOT_BASE_FOLDER_NAME = `${__dirname}/screenshots/default tests`; let sampleHomeUrl = ""; -async function verifyTokenStore( - BrowserCache: BrowserCacheUtils, - scopes: string[], - numberOfTenants: number = 1 -): Promise { - const tokenStore = await BrowserCache.getTokens(); - expect(tokenStore.idTokens).toHaveLength(1 * numberOfTenants); - expect(tokenStore.accessTokens).toHaveLength(1 * numberOfTenants); - expect(tokenStore.refreshTokens).toHaveLength(1); - - const account = await BrowserCache.getAccountFromCache( - tokenStore.idTokens[0] - ); - expect(account).toBeDefined(); - - expect(account["tenantProfiles"]).toHaveLength(numberOfTenants); - expect( - await BrowserCache.accessTokenForScopesExists( - tokenStore.accessTokens, - scopes, - numberOfTenants - ) - ).toBeTruthy(); - const storage = await BrowserCache.getWindowStorage(); - const crossTenantArtifactCount = 4; // 1 account object + 1 refresh token + 1 account keys array + 1 token keys array = 4 - const perTenantArtifactCount = 2; // 1 id token + 1 access token per tenant - expect(Object.keys(storage).length).toEqual( - crossTenantArtifactCount + numberOfTenants * perTenantArtifactCount - ); -} - describe("AAD-Prod Tests", () => { let browser: puppeteer.Browser; let context: puppeteer.BrowserContext; @@ -255,7 +224,9 @@ describe("AAD-Prod Tests", () => { ); // Verify browser cache contains Account, idToken, AccessToken and RefreshToken - await verifyTokenStore(BrowserCache, aadTokenRequest.scopes); + await BrowserCache.verifyTokenStore({ + scopes: aadTokenRequest.scopes, + }); }); it("acquireTokenSilent from cache (home tenant token)", async () => { @@ -283,7 +254,9 @@ describe("AAD-Prod Tests", () => { ]); // Verify browser cache contains Account, idToken, AccessToken and RefreshToken - await verifyTokenStore(BrowserCache, aadTokenRequest.scopes); + await BrowserCache.verifyTokenStore({ + scopes: aadTokenRequest.scopes, + }); }); it("acquireTokenSilent via RefreshToken (home tenant token)", async () => { @@ -305,7 +278,9 @@ describe("AAD-Prod Tests", () => { ); // Verify browser cache contains Account, idToken, AccessToken and RefreshToken - await verifyTokenStore(BrowserCache, aadTokenRequest.scopes); + await BrowserCache.verifyTokenStore({ + scopes: aadTokenRequest.scopes, + }); }); it("acquireTokenSilent via RefreshToken (guest tenant token)", async () => { @@ -322,11 +297,10 @@ describe("AAD-Prod Tests", () => { ); // Verify browser cache contains Account, idToken, AccessToken and RefreshToken - await verifyTokenStore( - BrowserCache, - aadTokenRequest.scopes, - 2 // two tenants authenticated with - ); + await BrowserCache.verifyTokenStore({ + scopes: aadTokenRequest.scopes, + numberOfTenants: 2, + }); }); it("acquireTokenSilent from cache (guest tenant token)", async () => { @@ -353,11 +327,10 @@ describe("AAD-Prod Tests", () => { ]); // Verify browser cache contains Account, idToken, AccessToken and RefreshToken - await verifyTokenStore( - BrowserCache, - aadTokenRequest.scopes, - 2 // two tenants authenticated with - ); + await BrowserCache.verifyTokenStore({ + scopes: aadTokenRequest.scopes, + numberOfTenants: 2, + }); }); }); }); diff --git a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/test/browserAADTenanted.spec.ts b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/test/browserAADTenanted.spec.ts index 1bd900ff81..5590b72c33 100644 --- a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/test/browserAADTenanted.spec.ts +++ b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/test/browserAADTenanted.spec.ts @@ -29,27 +29,6 @@ import { RedirectRequest } from "../../../../../../lib/msal-browser/src"; const SCREENSHOT_BASE_FOLDER_NAME = `${__dirname}/screenshots/default tests`; let sampleHomeUrl = ""; -async function verifyTokenStore( - BrowserCache: BrowserCacheUtils, - scopes: string[] -): Promise { - const tokenStore = await BrowserCache.getTokens(); - expect(tokenStore.idTokens).toHaveLength(1); - expect(tokenStore.accessTokens).toHaveLength(1); - expect(tokenStore.refreshTokens).toHaveLength(1); - expect( - await BrowserCache.getAccountFromCache(tokenStore.idTokens[0]) - ).toBeDefined(); - expect( - await BrowserCache.accessTokenForScopesExists( - tokenStore.accessTokens, - scopes - ) - ).toBeTruthy(); - const storage = await BrowserCache.getWindowStorage(); - expect(Object.keys(storage).length).toEqual(6); -} - describe("AAD-Prod Tests", () => { let browser: puppeteer.Browser; let context: puppeteer.BrowserContext; @@ -125,7 +104,9 @@ describe("AAD-Prod Tests", () => { await enterCredentials(page, screenshot, username, accountPwd); await waitForReturnToApp(screenshot, page); // Verify browser cache contains Account, idToken, AccessToken and RefreshToken - await verifyTokenStore(BrowserCache, aadTokenRequest.scopes); + await BrowserCache.verifyTokenStore({ + scopes: aadTokenRequest.scopes, + }); }); it("Performs loginRedirect from url with empty query string", async () => { @@ -139,7 +120,9 @@ describe("AAD-Prod Tests", () => { await enterCredentials(page, screenshot, username, accountPwd); await waitForReturnToApp(screenshot, page); // Verify browser cache contains Account, idToken, AccessToken and RefreshToken - await verifyTokenStore(BrowserCache, aadTokenRequest.scopes); + await BrowserCache.verifyTokenStore({ + scopes: aadTokenRequest.scopes, + }); expect(page.url()).toEqual(sampleHomeUrl); }); @@ -155,7 +138,9 @@ describe("AAD-Prod Tests", () => { await enterCredentials(page, screenshot, username, accountPwd); await waitForReturnToApp(screenshot, page); // Verify browser cache contains Account, idToken, AccessToken and RefreshToken - await verifyTokenStore(BrowserCache, aadTokenRequest.scopes); + await BrowserCache.verifyTokenStore({ + scopes: aadTokenRequest.scopes, + }); expect(page.url()).toEqual(testUrl); }); @@ -182,7 +167,9 @@ describe("AAD-Prod Tests", () => { await enterCredentials(page, screenshot, username, accountPwd); await waitForReturnToApp(screenshot, page); // Verify browser cache contains Account, idToken, AccessToken and RefreshToken - await verifyTokenStore(BrowserCache, aadTokenRequest.scopes); + await BrowserCache.verifyTokenStore({ + scopes: aadTokenRequest.scopes, + }); }); it("Performs loginRedirect with relative redirectStartPage", async () => { @@ -208,7 +195,9 @@ describe("AAD-Prod Tests", () => { await enterCredentials(page, screenshot, username, accountPwd); await waitForReturnToApp(screenshot, page); // Verify browser cache contains Account, idToken, AccessToken and RefreshToken - await verifyTokenStore(BrowserCache, aadTokenRequest.scopes); + await BrowserCache.verifyTokenStore({ + scopes: aadTokenRequest.scopes, + }); }); it("Performs loginPopup", async () => { @@ -230,7 +219,9 @@ describe("AAD-Prod Tests", () => { ); // Verify browser cache contains Account, idToken, AccessToken and RefreshToken - await verifyTokenStore(BrowserCache, aadTokenRequest.scopes); + await BrowserCache.verifyTokenStore({ + scopes: aadTokenRequest.scopes, + }); }); }); @@ -382,7 +373,9 @@ describe("AAD-Prod Tests", () => { ); // Verify browser cache contains Account, idToken, AccessToken and RefreshToken - await verifyTokenStore(BrowserCache, aadTokenRequest.scopes); + await BrowserCache.verifyTokenStore({ + scopes: aadTokenRequest.scopes, + }); }); it("acquireTokenPopup", async () => { @@ -401,7 +394,9 @@ describe("AAD-Prod Tests", () => { await screenshot.takeScreenshot(page, "acquireTokenPopupGotTokens"); // Verify browser cache contains Account, idToken, AccessToken and RefreshToken - await verifyTokenStore(BrowserCache, aadTokenRequest.scopes); + await BrowserCache.verifyTokenStore({ + scopes: aadTokenRequest.scopes, + }); }); it("acquireTokenSilent from Cache", async () => { @@ -429,7 +424,9 @@ describe("AAD-Prod Tests", () => { ]); // Verify browser cache contains Account, idToken, AccessToken and RefreshToken - await verifyTokenStore(BrowserCache, aadTokenRequest.scopes); + await BrowserCache.verifyTokenStore({ + scopes: aadTokenRequest.scopes, + }); }); it("acquireTokenSilent via RefreshToken", async () => { @@ -451,7 +448,9 @@ describe("AAD-Prod Tests", () => { ); // Verify browser cache contains Account, idToken, AccessToken and RefreshToken - await verifyTokenStore(BrowserCache, aadTokenRequest.scopes); + await BrowserCache.verifyTokenStore({ + scopes: aadTokenRequest.scopes, + }); }); }); }); diff --git a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/test/browserB2C.spec.ts b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/test/browserB2C.spec.ts index 123dac0d82..a18ebf6edf 100644 --- a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/test/browserB2C.spec.ts +++ b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/test/browserB2C.spec.ts @@ -29,28 +29,6 @@ import fs from "fs"; const SCREENSHOT_BASE_FOLDER_NAME = `${__dirname}/screenshots/default tests`; let sampleHomeUrl = ""; -async function verifyTokenStore( - BrowserCache: BrowserCacheUtils, - scopes: string[] -): Promise { - const tokenStore = await BrowserCache.getTokens(); - expect(tokenStore.idTokens).toHaveLength(1); - expect(tokenStore.accessTokens).toHaveLength(1); - expect(tokenStore.refreshTokens).toHaveLength(1); - expect( - await BrowserCache.getAccountFromCache(tokenStore.idTokens[0]) - ).toBeDefined(); - expect( - await BrowserCache.accessTokenForScopesExists( - tokenStore.accessTokens, - scopes - ) - ).toBeTruthy(); - const storage = await BrowserCache.getWindowStorage(); - // 1 AT + 1 IDT + 1 Account + 1 RT + 1 Account Keys + 1 Token Keys + 2 active account filters = 8 - expect(Object.keys(storage).length).toEqual(8); -} - describe("B2C Tests", () => { let browser: puppeteer.Browser; let context: puppeteer.BrowserContext; @@ -134,7 +112,9 @@ describe("B2C Tests", () => { ); await waitForReturnToApp(screenshot, page); - await verifyTokenStore(BrowserCache, b2cTokenRequest.scopes); + await BrowserCache.verifyTokenStore({ + scopes: b2cTokenRequest.scopes, + }); }); it("Performs loginPopup", async () => { @@ -160,7 +140,9 @@ describe("B2C Tests", () => { popupWindowClosed ); - await verifyTokenStore(BrowserCache, b2cTokenRequest.scopes); + await BrowserCache.verifyTokenStore({ + scopes: b2cTokenRequest.scopes, + }); }); }); @@ -232,7 +214,9 @@ describe("B2C Tests", () => { ); // Verify we now have an access_token - await verifyTokenStore(BrowserCache, b2cTokenRequest.scopes); + await BrowserCache.verifyTokenStore({ + scopes: b2cTokenRequest.scopes, + }); }); it("acquireTokenPopup", async () => { @@ -250,7 +234,9 @@ describe("B2C Tests", () => { ); // Verify we now have an access_token - await verifyTokenStore(BrowserCache, b2cTokenRequest.scopes); + await BrowserCache.verifyTokenStore({ + scopes: b2cTokenRequest.scopes, + }); }); it("acquireTokenSilent from Cache", async () => { @@ -277,7 +263,9 @@ describe("B2C Tests", () => { ]); // Verify we now have an access_token - await verifyTokenStore(BrowserCache, b2cTokenRequest.scopes); + await BrowserCache.verifyTokenStore({ + scopes: b2cTokenRequest.scopes, + }); }); it("acquireTokenSilent via RefreshToken", async () => { @@ -295,7 +283,9 @@ describe("B2C Tests", () => { ); // Verify we now have an access_token - await verifyTokenStore(BrowserCache, b2cTokenRequest.scopes); + await BrowserCache.verifyTokenStore({ + scopes: b2cTokenRequest.scopes, + }); }); }); }); @@ -356,7 +346,9 @@ describe("B2C Tests", () => { ); await waitForReturnToApp(screenshot, page); - await verifyTokenStore(BrowserCache, b2cTokenRequest.scopes); + await BrowserCache.verifyTokenStore({ + scopes: b2cTokenRequest.scopes, + }); }); it("Performs loginPopup", async () => { @@ -382,7 +374,9 @@ describe("B2C Tests", () => { popupWindowClosed ); - await verifyTokenStore(BrowserCache, b2cTokenRequest.scopes); + await BrowserCache.verifyTokenStore({ + scopes: b2cTokenRequest.scopes, + }); }); }); @@ -454,7 +448,9 @@ describe("B2C Tests", () => { ); // Verify we now have an access_token - await verifyTokenStore(BrowserCache, b2cTokenRequest.scopes); + await BrowserCache.verifyTokenStore({ + scopes: b2cTokenRequest.scopes, + }); }); it("acquireTokenPopup", async () => { @@ -472,7 +468,9 @@ describe("B2C Tests", () => { ); // Verify we now have an access_token - await verifyTokenStore(BrowserCache, b2cTokenRequest.scopes); + await BrowserCache.verifyTokenStore({ + scopes: b2cTokenRequest.scopes, + }); }); it("acquireTokenSilent from Cache", async () => { @@ -499,7 +497,9 @@ describe("B2C Tests", () => { ]); // Verify we now have an access_token - await verifyTokenStore(BrowserCache, b2cTokenRequest.scopes); + await BrowserCache.verifyTokenStore({ + scopes: b2cTokenRequest.scopes, + }); }); it("acquireTokenSilent via RefreshToken", async () => { @@ -517,7 +517,9 @@ describe("B2C Tests", () => { ); // Verify we now have an access_token - await verifyTokenStore(BrowserCache, b2cTokenRequest.scopes); + await BrowserCache.verifyTokenStore({ + scopes: b2cTokenRequest.scopes, + }); }); }); }); diff --git a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/test/localStorage.spec.ts b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/test/localStorage.spec.ts index 1c04d7fd3c..5215a8503f 100644 --- a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/test/localStorage.spec.ts +++ b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/test/localStorage.spec.ts @@ -26,27 +26,6 @@ import fs from "fs"; const SCREENSHOT_BASE_FOLDER_NAME = `${__dirname}/screenshots/localStorageTests`; -async function verifyTokenStore( - BrowserCache: BrowserCacheUtils, - scopes: string[] -): Promise { - const tokenStore = await BrowserCache.getTokens(); - expect(tokenStore.idTokens).toHaveLength(1); - expect(tokenStore.accessTokens).toHaveLength(1); - expect(tokenStore.refreshTokens).toHaveLength(1); - expect( - await BrowserCache.getAccountFromCache(tokenStore.idTokens[0]) - ).toBeDefined(); - expect( - await BrowserCache.accessTokenForScopesExists( - tokenStore.accessTokens, - scopes - ) - ).toBeTruthy(); - const storage = await BrowserCache.getWindowStorage(); - expect(Object.keys(storage).length).toEqual(6); -} - describe("LocalStorage Tests", function () { let username = ""; let accountPwd = ""; @@ -121,7 +100,9 @@ describe("LocalStorage Tests", function () { await enterCredentials(page, screenshot, username, accountPwd); await waitForReturnToApp(screenshot, page); // Verify browser cache contains Account, idToken, AccessToken and RefreshToken - await verifyTokenStore(BrowserCache, aadTokenRequest.scopes); + await BrowserCache.verifyTokenStore({ + scopes: aadTokenRequest.scopes, + }); }); it("Going back to app during redirect clears cache", async () => { @@ -167,7 +148,9 @@ describe("LocalStorage Tests", function () { ); // Verify browser cache contains Account, idToken, AccessToken and RefreshToken - await verifyTokenStore(BrowserCache, aadTokenRequest.scopes); + await BrowserCache.verifyTokenStore({ + scopes: aadTokenRequest.scopes, + }); }); it("Closing popup before login resolves clears cache", async () => { From 21ac601dde404d9fe1ff2d4a9cd052e4dd178368 Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Thu, 23 Nov 2023 17:16:34 -0800 Subject: [PATCH 81/91] Fix multi-tenant e2e tests --- samples/e2eTestUtils/src/BrowserCacheTestUtils.ts | 3 ++- .../VanillaJSTestApp2.0/app/customizable-e2e-test/auth.js | 4 ++-- .../customizable-e2e-test/test/browserAADMultiTenant.spec.ts | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/samples/e2eTestUtils/src/BrowserCacheTestUtils.ts b/samples/e2eTestUtils/src/BrowserCacheTestUtils.ts index e9c37fc996..40b4e05159 100644 --- a/samples/e2eTestUtils/src/BrowserCacheTestUtils.ts +++ b/samples/e2eTestUtils/src/BrowserCacheTestUtils.ts @@ -230,7 +230,8 @@ export class BrowserCacheUtils { expect( await this.accessTokenForScopesExists( tokenStore.accessTokens, - scopes + scopes, + totalAccessTokens ) ).toBeTruthy(); const storage = await this.getWindowStorage(); diff --git a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/auth.js b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/auth.js index 27dc31ee1d..31d080ed0b 100644 --- a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/auth.js +++ b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/auth.js @@ -117,10 +117,10 @@ async function getGuestTokenSilently() { console.error(error); }); } else { - const homeAccount = myMSALObj.getAccount({ homeAccountId } ); + const currentAcc = myMSALObj.getActiveAccount(); response = await myMSALObj.acquireTokenSilent({ ...request, - account: homeAccount, + account: currentAcc, authority: tenantConfig.guest.authority, cacheLookupPolicy: msal.CacheLookupPolicy.RefreshToken }).then(handleResponse).catch(error => { diff --git a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/test/browserAADMultiTenant.spec.ts b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/test/browserAADMultiTenant.spec.ts index 568e5333a2..85e11dbca1 100644 --- a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/test/browserAADMultiTenant.spec.ts +++ b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/test/browserAADMultiTenant.spec.ts @@ -27,7 +27,7 @@ import { import fs from "fs"; import { GuestHomedIn } from "e2e-test-utils/src/Constants"; -const SCREENSHOT_BASE_FOLDER_NAME = `${__dirname}/screenshots/default tests`; +const SCREENSHOT_BASE_FOLDER_NAME = `${__dirname}/screenshots/multiTenantTests`; let sampleHomeUrl = ""; describe("AAD-Prod Tests", () => { From cbc445433670bf66658739513355d0cbb146c384 Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Thu, 23 Nov 2023 17:35:52 -0800 Subject: [PATCH 82/91] Fix BrowserCacheTestUtils typing --- samples/e2eTestUtils/src/BrowserCacheTestUtils.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/samples/e2eTestUtils/src/BrowserCacheTestUtils.ts b/samples/e2eTestUtils/src/BrowserCacheTestUtils.ts index 40b4e05159..504596ef75 100644 --- a/samples/e2eTestUtils/src/BrowserCacheTestUtils.ts +++ b/samples/e2eTestUtils/src/BrowserCacheTestUtils.ts @@ -1,4 +1,5 @@ import * as puppeteer from "puppeteer"; +import { AccountEntity } from "../../../lib/msal-browser/src"; export interface ServerTelemetryEntity { failedRequests: Array; @@ -224,7 +225,9 @@ export class BrowserCacheUtils { expect(tokenStore.accessTokens).toHaveLength(totalAccessTokens); expect(tokenStore.refreshTokens).toHaveLength(refreshTokens || 1); - const account = await this.getAccountFromCache(tokenStore.idTokens[0]); + const account = (await this.getAccountFromCache( + tokenStore.idTokens[0] + )) as AccountEntity; expect(account).toBeDefined(); expect(account["tenantProfiles"]).toHaveLength(numberOfTenants); expect( From 3465668f53387289cdfa3cc566a3141f71475911 Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Thu, 23 Nov 2023 18:08:08 -0800 Subject: [PATCH 83/91] Update test utils to avoid undefined assignments --- samples/e2eTestUtils/src/TestUtils.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/samples/e2eTestUtils/src/TestUtils.ts b/samples/e2eTestUtils/src/TestUtils.ts index d371e5dbce..cd24278b2c 100644 --- a/samples/e2eTestUtils/src/TestUtils.ts +++ b/samples/e2eTestUtils/src/TestUtils.ts @@ -142,10 +142,10 @@ export async function setupCredentials( const { user, lab } = labConfig; if (user.userType === "Guest") { - username = user.homeUPN; - guestUsername = user.upn; + username = user.homeUPN || ""; + guestUsername = user.upn || ""; } else { - username = user.upn; + username = user.upn || ""; } if (!lab.labName) { From a9afc3832d5e90aec084995e5f48ce4f34671bbf Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Thu, 23 Nov 2023 18:20:15 -0800 Subject: [PATCH 84/91] Update test utils --- samples/e2eTestUtils/src/TestUtils.ts | 18 +++++++++++------- .../test/browserAADMultiTenant.spec.ts | 2 +- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/samples/e2eTestUtils/src/TestUtils.ts b/samples/e2eTestUtils/src/TestUtils.ts index cd24278b2c..bf9c351259 100644 --- a/samples/e2eTestUtils/src/TestUtils.ts +++ b/samples/e2eTestUtils/src/TestUtils.ts @@ -134,17 +134,21 @@ export async function retrieveAppConfiguration( export async function setupCredentials( labConfig: LabConfig, labClient: LabClient -): Promise<[string, string, string]> { +): Promise<[string, string]> { let username = ""; let accountPwd = ""; - let guestUsername = ""; const { user, lab } = labConfig; if (user.userType === "Guest") { - username = user.homeUPN || ""; - guestUsername = user.upn || ""; + if (!user.homeUPN) { + throw Error("Guest user does not have a homeUPN"); + } + username = user.homeUPN; } else { + if (!user.upn) { + throw Error("User does not have a upn"); + } username = user.upn || ""; } @@ -152,15 +156,15 @@ export async function setupCredentials( throw Error("No Labname provided!"); } - const testPwdSecret = await labClient.getSecret(labConfig.lab.labName); + const testPwdSecret = await labClient.getSecret(lab.labName); accountPwd = testPwdSecret.value; if (!accountPwd) { - throw "Unable to get account password!"; + throw Error("Unable to get account password!"); } - return [username, accountPwd, guestUsername]; + return [username, accountPwd]; } export async function b2cLocalAccountEnterCredentials( diff --git a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/test/browserAADMultiTenant.spec.ts b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/test/browserAADMultiTenant.spec.ts index 85e11dbca1..4cc4e01394 100644 --- a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/test/browserAADMultiTenant.spec.ts +++ b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/test/browserAADMultiTenant.spec.ts @@ -56,7 +56,7 @@ describe("AAD-Prod Tests", () => { labApiParams ); - [username, accountPwd, guestUsername] = await setupCredentials( + [username, accountPwd] = await setupCredentials( envResponse[0], labClient ); From 6ad097ffff41115f34b778d40dcabb68b5589c30 Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Thu, 23 Nov 2023 19:52:54 -0800 Subject: [PATCH 85/91] Fix ignorable ts error in BrowserCacheTestUtils --- samples/e2eTestUtils/src/BrowserCacheTestUtils.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/samples/e2eTestUtils/src/BrowserCacheTestUtils.ts b/samples/e2eTestUtils/src/BrowserCacheTestUtils.ts index 504596ef75..7e09a076d5 100644 --- a/samples/e2eTestUtils/src/BrowserCacheTestUtils.ts +++ b/samples/e2eTestUtils/src/BrowserCacheTestUtils.ts @@ -1,5 +1,4 @@ import * as puppeteer from "puppeteer"; -import { AccountEntity } from "../../../lib/msal-browser/src"; export interface ServerTelemetryEntity { failedRequests: Array; @@ -225,11 +224,14 @@ export class BrowserCacheUtils { expect(tokenStore.accessTokens).toHaveLength(totalAccessTokens); expect(tokenStore.refreshTokens).toHaveLength(refreshTokens || 1); - const account = (await this.getAccountFromCache( - tokenStore.idTokens[0] - )) as AccountEntity; + const account = await this.getAccountFromCache(tokenStore.idTokens[0]); expect(account).toBeDefined(); - expect(account["tenantProfiles"]).toHaveLength(numberOfTenants); + if (account.hasOwnProperty("tenantProfiles")) { + // @ts-ignore + expect(account["tenantProfiles"]).toHaveLength(numberOfTenants); + } else { + throw new Error("Account does not have a tenantProfiles property"); + } expect( await this.accessTokenForScopesExists( tokenStore.accessTokens, From d65cbeb6ac2d7b6333c487919582fea0cbd1a45e Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Thu, 23 Nov 2023 20:10:22 -0800 Subject: [PATCH 86/91] Fix browser cache test utils for angular e2e --- samples/e2eTestUtils/src/BrowserCacheTestUtils.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/samples/e2eTestUtils/src/BrowserCacheTestUtils.ts b/samples/e2eTestUtils/src/BrowserCacheTestUtils.ts index 7e09a076d5..910c623fcc 100644 --- a/samples/e2eTestUtils/src/BrowserCacheTestUtils.ts +++ b/samples/e2eTestUtils/src/BrowserCacheTestUtils.ts @@ -226,11 +226,17 @@ export class BrowserCacheUtils { const account = await this.getAccountFromCache(tokenStore.idTokens[0]); expect(account).toBeDefined(); - if (account.hasOwnProperty("tenantProfiles")) { - // @ts-ignore - expect(account["tenantProfiles"]).toHaveLength(numberOfTenants); + if (account) { + if (account.hasOwnProperty("tenantProfiles")) { + // @ts-ignore + expect(account["tenantProfiles"]).toHaveLength(numberOfTenants); + } else { + throw new Error( + "Account does not have a tenantProfiles property" + ); + } } else { - throw new Error("Account does not have a tenantProfiles property"); + throw new Error("Account is null"); } expect( await this.accessTokenForScopesExists( From 662cd52da3c57dca65ca0261046885103cb6e6e8 Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Mon, 27 Nov 2023 10:45:35 -0800 Subject: [PATCH 87/91] Use check-latest on setup node --- .github/workflows/ci-template.yml | 121 +++++++++++++++--------------- 1 file changed, 61 insertions(+), 60 deletions(-) diff --git a/.github/workflows/ci-template.yml b/.github/workflows/ci-template.yml index 38ca232b0e..b4e9db8614 100644 --- a/.github/workflows/ci-template.yml +++ b/.github/workflows/ci-template.yml @@ -3,76 +3,77 @@ name: CI template workflow on: - workflow_call: - inputs: - path: - required: true - type: string - lib-name: - required: true - type: string - os: - default: 'ubuntu-latest' - type: string - should-run: # This ensures check status is reported regardless of whether or not the job was run - default: true - type: boolean - + workflow_call: + inputs: + path: + required: true + type: string + lib-name: + required: true + type: string + os: + default: "ubuntu-latest" + type: string + should-run: # This ensures check status is reported regardless of whether or not the job was run + default: true + type: boolean + concurrency: - group: ${{github.workflow}}-${{inputs.lib-name}}-${{github.ref}}-${{inputs.os}} - cancel-in-progress: true + group: ${{github.workflow}}-${{inputs.lib-name}}-${{github.ref}}-${{inputs.os}} + cancel-in-progress: true jobs: - build-test: - if: inputs.should-run - runs-on: ${{ inputs.os }} + build-test: + if: inputs.should-run + runs-on: ${{ inputs.os }} - steps: - - uses: actions/checkout@v4 + steps: + - uses: actions/checkout@v4 - - name: Use Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: 'npm' + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: "npm" + check-latest: true - - name: Install Dependencies - run: npm ci --workspace=${{ inputs.path }}/${{ inputs.lib-name }} --include-workspace-root + - name: Install Dependencies + run: npm ci --workspace=${{ inputs.path }}/${{ inputs.lib-name }} --include-workspace-root - - name: Upload npm logs - uses: actions/upload-artifact@v3 - if: failure() - with: - name: npm_logs_${{ inputs.lib-name }} - path: ~/.npm/_logs/**/* - retention-days: 2 + - name: Upload npm logs + uses: actions/upload-artifact@v3 + if: failure() + with: + name: npm_logs_${{ inputs.lib-name }} + path: ~/.npm/_logs/**/* + retention-days: 2 - - name: Build packages - working-directory: ${{ inputs.path }}/${{ inputs.lib-name }} - run: npm run build:all + - name: Build packages + working-directory: ${{ inputs.path }}/${{ inputs.lib-name }} + run: npm run build:all - - name: Lint - working-directory: ${{ inputs.path }}/${{ inputs.lib-name }} - run: npm run lint + - name: Lint + working-directory: ${{ inputs.path }}/${{ inputs.lib-name }} + run: npm run lint - - name: Check Formatting - working-directory: ${{ inputs.path }}/${{ inputs.lib-name }} - run: npm run format:check + - name: Check Formatting + working-directory: ${{ inputs.path }}/${{ inputs.lib-name }} + run: npm run format:check - - name: Install Keyring on Linux - if: inputs.os == 'ubuntu-latest' && inputs.path == 'extensions' - run: | - sudo apt-get update - sudo apt-get install -y gnome-keyring - echo 'tests' | gnome-keyring-daemon --unlock + - name: Install Keyring on Linux + if: inputs.os == 'ubuntu-latest' && inputs.path == 'extensions' + run: | + sudo apt-get update + sudo apt-get install -y gnome-keyring + echo 'tests' | gnome-keyring-daemon --unlock - - name: Unit Tests with coverage - working-directory: ${{ inputs.path }}/${{ inputs.lib-name }} - run: npm run test:coverage + - name: Unit Tests with coverage + working-directory: ${{ inputs.path }}/${{ inputs.lib-name }} + run: npm run test:coverage - - name: Upload Test Coverage to CodeCov - if: success() - uses: codecov/codecov-action@v3 - with: - files: ${{ inputs.path }}/${{ inputs.lib-name }}/coverage/lcov.info - flags: ${{ inputs.lib-name }} + - name: Upload Test Coverage to CodeCov + if: success() + uses: codecov/codecov-action@v3 + with: + files: ${{ inputs.path }}/${{ inputs.lib-name }}/coverage/lcov.info + flags: ${{ inputs.lib-name }} From f2f3519d878395da367da80aa854895f7afef804 Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Mon, 27 Nov 2023 16:55:21 -0800 Subject: [PATCH 88/91] Move account and ID token creation test utils to a shared-test-utils dir --- lib/msal-browser/src/index.ts | 1 + .../test/app/PublicClientApplication.spec.ts | 2 +- .../test/cache/TestStorageManager.ts | 61 ---------------- .../test/cache/TokenCache.spec.ts | 2 +- .../test/event/EventHandler.spec.ts | 2 +- .../NativeInteractionClient.spec.ts | 2 +- .../interaction_client/RedirectClient.spec.ts | 2 +- .../SilentCacheClient.spec.ts | 2 +- .../test/account/AccountInfo.spec.ts | 2 +- .../test/cache/CacheManager.spec.ts | 4 +- lib/msal-common/test/cache/MockCache.ts | 72 ++----------------- .../test/cache/entities/AccountEntity.spec.ts | 2 +- .../test/client/RefreshTokenClient.spec.ts | 2 +- .../test/client/SilentFlowClient.spec.ts | 2 +- shared-test-utils/CredentialGenerators.ts | 70 ++++++++++++++++++ 15 files changed, 87 insertions(+), 141 deletions(-) create mode 100644 shared-test-utils/CredentialGenerators.ts diff --git a/lib/msal-browser/src/index.ts b/lib/msal-browser/src/index.ts index 1f934a81ed..e7b6b6b295 100644 --- a/lib/msal-browser/src/index.ts +++ b/lib/msal-browser/src/index.ts @@ -146,6 +146,7 @@ export { PerformanceEvents, // Telemetry InProgressPerformanceEvent, + TenantProfile, } from "@azure/msal-common"; export { version } from "./packageMetadata"; diff --git a/lib/msal-browser/test/app/PublicClientApplication.spec.ts b/lib/msal-browser/test/app/PublicClientApplication.spec.ts index 8ee54e5c76..27e25623cb 100644 --- a/lib/msal-browser/test/app/PublicClientApplication.spec.ts +++ b/lib/msal-browser/test/app/PublicClientApplication.spec.ts @@ -116,7 +116,7 @@ import { import { buildAccountFromIdTokenClaims, buildIdToken, -} from "../cache/TestStorageManager"; +} from "../../../../shared-test-utils/CredentialGenerators"; const cacheConfig = { temporaryCacheLocation: BrowserCacheLocation.SessionStorage, diff --git a/lib/msal-browser/test/cache/TestStorageManager.ts b/lib/msal-browser/test/cache/TestStorageManager.ts index 14b279be70..a0a1888ea9 100644 --- a/lib/msal-browser/test/cache/TestStorageManager.ts +++ b/lib/msal-browser/test/cache/TestStorageManager.ts @@ -208,64 +208,3 @@ export class TestStorageManager extends CacheManager { return currentCacheKey; } } - -export function buildAccountFromIdTokenClaims( - idTokenClaims: TokenClaims, - guestIdTokenClaimsList?: TokenClaims[], - options?: Partial -): AccountEntity { - const { oid, tid, preferred_username, emails, name } = idTokenClaims; - const tenantId = tid || ""; - const email = emails ? emails[0] : null; - - const homeAccountId = `${oid}.${tid}`; - - const accountInfo: AccountInfo = { - homeAccountId: homeAccountId || "", - username: preferred_username || email || "", - localAccountId: oid || "", - tenantId: tenantId, - environment: "login.windows.net", - authorityType: "MSSTS", - name: name, - tenantProfiles: new Map([ - [ - tenantId, - buildTenantProfileFromIdTokenClaims( - homeAccountId, - idTokenClaims - ), - ], - ]), - }; - guestIdTokenClaimsList?.forEach((guestIdTokenClaims: TokenClaims) => { - const guestTenantId = guestIdTokenClaims.tid || ""; - accountInfo.tenantProfiles?.set( - guestTenantId, - buildTenantProfileFromIdTokenClaims( - accountInfo.homeAccountId, - guestIdTokenClaims - ) - ); - }); - return AccountEntity.createFromAccountInfo({ ...accountInfo, ...options }); -} - -export function buildIdToken( - idTokenClaims: TokenClaims, - idTokenSecret: string, - options?: Partial -): IdTokenEntity { - const { oid, tid } = idTokenClaims; - const homeAccountId = `${oid}.${tid}`; - const idToken = { - realm: tid || "", - environment: "login.microsoftonline.com", - credentialType: CredentialType.ID_TOKEN, - secret: idTokenSecret, - clientId: "mock_client_id", - homeAccountId: homeAccountId, - }; - - return { ...idToken, ...options }; -} diff --git a/lib/msal-browser/test/cache/TokenCache.spec.ts b/lib/msal-browser/test/cache/TokenCache.spec.ts index 5c0e7bf8b9..16681ded49 100644 --- a/lib/msal-browser/test/cache/TokenCache.spec.ts +++ b/lib/msal-browser/test/cache/TokenCache.spec.ts @@ -38,7 +38,7 @@ import { } from "../utils/StringConstants"; import { BrowserAuthErrorMessage, SilentRequest } from "../../src"; import { base64Decode } from "../../src/encode/Base64Decode"; -import { buildAccountFromIdTokenClaims } from "./TestStorageManager"; +import { buildAccountFromIdTokenClaims } from "../../../../shared-test-utils/CredentialGenerators"; describe("TokenCache tests", () => { let configuration: BrowserConfiguration; diff --git a/lib/msal-browser/test/event/EventHandler.spec.ts b/lib/msal-browser/test/event/EventHandler.spec.ts index f6c41abba5..10e873ccd6 100644 --- a/lib/msal-browser/test/event/EventHandler.spec.ts +++ b/lib/msal-browser/test/event/EventHandler.spec.ts @@ -5,7 +5,7 @@ import sinon from "sinon"; import { EventHandler } from "../../src/event/EventHandler"; import { Logger, LogLevel, AccountInfo, AccountEntity } from "../../src"; import { CryptoOps } from "../../src/crypto/CryptoOps"; -import { buildAccountFromIdTokenClaims } from "../cache/TestStorageManager"; +import { buildAccountFromIdTokenClaims } from "../../../../shared-test-utils/CredentialGenerators"; import { ID_TOKEN_CLAIMS } from "../utils/StringConstants"; describe("Event API tests", () => { diff --git a/lib/msal-browser/test/interaction_client/NativeInteractionClient.spec.ts b/lib/msal-browser/test/interaction_client/NativeInteractionClient.spec.ts index 68e2e5f99c..9d6f6a4f43 100644 --- a/lib/msal-browser/test/interaction_client/NativeInteractionClient.spec.ts +++ b/lib/msal-browser/test/interaction_client/NativeInteractionClient.spec.ts @@ -45,7 +45,7 @@ import { IPublicClientApplication } from "../../src"; import { buildAccountFromIdTokenClaims, buildIdToken, -} from "../cache/TestStorageManager"; +} from "../../../../shared-test-utils/CredentialGenerators"; const networkInterface = { sendGetRequestAsync(): T { diff --git a/lib/msal-browser/test/interaction_client/RedirectClient.spec.ts b/lib/msal-browser/test/interaction_client/RedirectClient.spec.ts index 5c3228a6b7..c4a5cc67ee 100644 --- a/lib/msal-browser/test/interaction_client/RedirectClient.spec.ts +++ b/lib/msal-browser/test/interaction_client/RedirectClient.spec.ts @@ -83,7 +83,7 @@ import { AuthenticationResult } from "../../src/response/AuthenticationResult"; import { buildAccountFromIdTokenClaims, buildIdToken, -} from "../cache/TestStorageManager"; +} from "../../../../shared-test-utils/CredentialGenerators"; const cacheConfig = { cacheLocation: BrowserCacheLocation.SessionStorage, diff --git a/lib/msal-browser/test/interaction_client/SilentCacheClient.spec.ts b/lib/msal-browser/test/interaction_client/SilentCacheClient.spec.ts index 2219f1cef7..3d6546656d 100644 --- a/lib/msal-browser/test/interaction_client/SilentCacheClient.spec.ts +++ b/lib/msal-browser/test/interaction_client/SilentCacheClient.spec.ts @@ -27,7 +27,7 @@ import { import { buildAccountFromIdTokenClaims, buildIdToken, -} from "../cache/TestStorageManager"; +} from "../../../../shared-test-utils/CredentialGenerators"; const testAccountEntity: AccountEntity = buildAccountFromIdTokenClaims( ID_TOKEN_CLAIMS, diff --git a/lib/msal-common/test/account/AccountInfo.spec.ts b/lib/msal-common/test/account/AccountInfo.spec.ts index 0ea8e562cc..bf331af811 100644 --- a/lib/msal-common/test/account/AccountInfo.spec.ts +++ b/lib/msal-common/test/account/AccountInfo.spec.ts @@ -1,6 +1,6 @@ import { AccountEntity } from "../../src"; import * as AccountInfo from "../../src/account/AccountInfo"; -import { buildAccountFromIdTokenClaims } from "../cache/MockCache"; +import { buildAccountFromIdTokenClaims } from "../../../../shared-test-utils/CredentialGenerators"; import { ID_TOKEN_ALT_CLAIMS, ID_TOKEN_CLAIMS, diff --git a/lib/msal-common/test/cache/CacheManager.spec.ts b/lib/msal-common/test/cache/CacheManager.spec.ts index cdd1f16df3..b3770f83ba 100644 --- a/lib/msal-common/test/cache/CacheManager.spec.ts +++ b/lib/msal-common/test/cache/CacheManager.spec.ts @@ -30,11 +30,11 @@ import { createClientAuthError, } from "../../src/error/ClientAuthError"; import { AccountInfo } from "../../src/account/AccountInfo"; +import { MockCache } from "./MockCache"; import { - MockCache, buildAccountFromIdTokenClaims, buildIdToken, -} from "./MockCache"; +} from "../../../../shared-test-utils/CredentialGenerators"; import { mockCrypto } from "../client/ClientTestUtils"; import { TestError } from "../test_kit/TestErrors"; import { CacheManager } from "../../src/cache/CacheManager"; diff --git a/lib/msal-common/test/cache/MockCache.ts b/lib/msal-common/test/cache/MockCache.ts index 642ca116f0..ca2e2f9bc6 100644 --- a/lib/msal-common/test/cache/MockCache.ts +++ b/lib/msal-common/test/cache/MockCache.ts @@ -4,7 +4,6 @@ */ import { - AccountEntity, AppMetadataEntity, AuthorityMetadataEntity, CacheManager, @@ -14,14 +13,7 @@ import { StaticAuthorityOptions, CredentialType, AuthenticationScheme, - TokenClaims, - IdTokenEntity, } from "../../src"; -import { - AccountInfo, - TenantProfile, - buildTenantProfileFromIdTokenClaims, -} from "../../src/account/AccountInfo"; import { MockStorageClass } from "../client/ClientTestUtils"; import { TEST_TOKENS, @@ -30,6 +22,10 @@ import { ID_TOKEN_ALT_CLAIMS, GUEST_ID_TOKEN_CLAIMS, } from "../test_kit/StringConstants"; +import { + buildAccountFromIdTokenClaims, + buildIdToken, +} from "../../../../shared-test-utils/CredentialGenerators"; export class MockCache { cacheManager: MockStorageClass; @@ -289,63 +285,3 @@ export class MockCache { this.cacheManager.setAuthorityMetadata(cacheKey, authorityMetadata); } } - -export function buildAccountFromIdTokenClaims( - idTokenClaims: TokenClaims, - guestIdTokenClaimsList?: TokenClaims[] -): AccountEntity { - const { oid, tid, preferred_username, emails, name } = idTokenClaims; - const tenantId = tid || ""; - const email = emails ? emails[0] : null; - - const homeAccountId = `${oid}.${tid}`; - - const accountInfo: AccountInfo = { - homeAccountId: homeAccountId || "", - username: preferred_username || email || "", - localAccountId: oid || "", - tenantId: tenantId, - environment: "login.windows.net", - authorityType: "MSSTS", - name: name, - tenantProfiles: new Map([ - [ - tenantId, - buildTenantProfileFromIdTokenClaims( - homeAccountId, - idTokenClaims - ), - ], - ]), - }; - guestIdTokenClaimsList?.forEach((guestIdTokenClaims: TokenClaims) => { - const guestTenantId = guestIdTokenClaims.tid || ""; - accountInfo.tenantProfiles?.set( - guestTenantId, - buildTenantProfileFromIdTokenClaims( - accountInfo.homeAccountId, - guestIdTokenClaims - ) - ); - }); - return AccountEntity.createFromAccountInfo(accountInfo); -} - -export function buildIdToken( - idTokenClaims: TokenClaims, - idTokenSecret: string, - options?: Partial -): IdTokenEntity { - const { oid, tid } = idTokenClaims; - const homeAccountId = `${oid}.${tid}`; - const idToken = { - realm: tid || "", - environment: "login.microsoftonline.com", - credentialType: CredentialType.ID_TOKEN, - secret: idTokenSecret, - clientId: "mock_client_id", - homeAccountId: homeAccountId, - }; - - return { ...idToken, ...options }; -} diff --git a/lib/msal-common/test/cache/entities/AccountEntity.spec.ts b/lib/msal-common/test/cache/entities/AccountEntity.spec.ts index cfb26004fb..8cdd3a12ff 100644 --- a/lib/msal-common/test/cache/entities/AccountEntity.spec.ts +++ b/lib/msal-common/test/cache/entities/AccountEntity.spec.ts @@ -28,7 +28,7 @@ import { LogLevel, Logger } from "../../../src/logger/Logger"; import { Authority } from "../../../src/authority/Authority"; import { AuthorityType } from "../../../src/authority/AuthorityType"; import { TokenClaims } from "../../../src"; -import { buildAccountFromIdTokenClaims } from "../MockCache"; +import { buildAccountFromIdTokenClaims } from "../../../../../shared-test-utils/CredentialGenerators"; const cryptoInterface: ICrypto = { createNewGuid(): string { diff --git a/lib/msal-common/test/client/RefreshTokenClient.spec.ts b/lib/msal-common/test/client/RefreshTokenClient.spec.ts index bc088841ff..b5e869e5fd 100644 --- a/lib/msal-common/test/client/RefreshTokenClient.spec.ts +++ b/lib/msal-common/test/client/RefreshTokenClient.spec.ts @@ -57,7 +57,7 @@ import { } from "../../src/error/InteractionRequiredAuthError"; import { StubPerformanceClient } from "../../src/telemetry/performance/StubPerformanceClient"; import { ProtocolMode } from "../../src/authority/ProtocolMode"; -import { buildAccountFromIdTokenClaims } from "../cache/MockCache"; +import { buildAccountFromIdTokenClaims } from "../../../../shared-test-utils/CredentialGenerators"; const testAccountEntity: AccountEntity = new AccountEntity(); testAccountEntity.homeAccountId = `${TEST_DATA_CLIENT_INFO.TEST_UID}.${TEST_DATA_CLIENT_INFO.TEST_UTID}`; diff --git a/lib/msal-common/test/client/SilentFlowClient.spec.ts b/lib/msal-common/test/client/SilentFlowClient.spec.ts index 557be40e49..bc357fb1f3 100644 --- a/lib/msal-common/test/client/SilentFlowClient.spec.ts +++ b/lib/msal-common/test/client/SilentFlowClient.spec.ts @@ -55,7 +55,7 @@ import { } from "../../src/error/InteractionRequiredAuthError"; import { StubPerformanceClient } from "../../src/telemetry/performance/StubPerformanceClient"; import { Logger } from "../../src/logger/Logger"; -import { buildAccountFromIdTokenClaims } from "../cache/MockCache"; +import { buildAccountFromIdTokenClaims } from "../../../../shared-test-utils/CredentialGenerators"; const testAccountEntity: AccountEntity = buildAccountFromIdTokenClaims(ID_TOKEN_CLAIMS); diff --git a/shared-test-utils/CredentialGenerators.ts b/shared-test-utils/CredentialGenerators.ts new file mode 100644 index 0000000000..7f2d2a7bc2 --- /dev/null +++ b/shared-test-utils/CredentialGenerators.ts @@ -0,0 +1,70 @@ +import { + AccountEntity, + AccountInfo, + CredentialType, + IdTokenEntity, + TenantProfile, + TokenClaims, + buildTenantProfileFromIdTokenClaims, +} from "@azure/msal-common"; + +export function buildAccountFromIdTokenClaims( + idTokenClaims: TokenClaims, + guestIdTokenClaimsList?: TokenClaims[], + options?: Partial +): AccountEntity { + const { oid, tid, preferred_username, emails, name } = idTokenClaims; + const tenantId = tid || ""; + const email = emails ? emails[0] : null; + + const homeAccountId = `${oid}.${tid}`; + + const accountInfo: AccountInfo = { + homeAccountId: homeAccountId || "", + username: preferred_username || email || "", + localAccountId: oid || "", + tenantId: tenantId, + environment: "login.windows.net", + authorityType: "MSSTS", + name: name, + tenantProfiles: new Map([ + [ + tenantId, + buildTenantProfileFromIdTokenClaims( + homeAccountId, + idTokenClaims + ), + ], + ]), + }; + guestIdTokenClaimsList?.forEach((guestIdTokenClaims: TokenClaims) => { + const guestTenantId = guestIdTokenClaims.tid || ""; + accountInfo.tenantProfiles?.set( + guestTenantId, + buildTenantProfileFromIdTokenClaims( + accountInfo.homeAccountId, + guestIdTokenClaims + ) + ); + }); + return AccountEntity.createFromAccountInfo({ ...accountInfo, ...options }); +} + +export function buildIdToken( + idTokenClaims: TokenClaims, + idTokenSecret: string, + options?: Partial +): IdTokenEntity { + const { oid, tid } = idTokenClaims; + const homeAccountId = `${oid}.${tid}`; + const idToken = { + realm: tid || "", + environment: "login.microsoftonline.com", + credentialType: CredentialType.ID_TOKEN, + secret: idTokenSecret, + clientId: "mock_client_id", + homeAccountId: homeAccountId, + }; + + return { ...idToken, ...options }; +} From 900155a929b7459bec5b296fd7471abba599dda7 Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Tue, 28 Nov 2023 13:35:19 -0800 Subject: [PATCH 89/91] Add shared msal config package --- lib/msal-browser/package.json | 1 + .../test/app/PublicClientApplication.spec.ts | 5 +-- .../test/cache/TokenCache.spec.ts | 2 +- .../test/event/EventHandler.spec.ts | 2 +- .../NativeInteractionClient.spec.ts | 5 +-- .../interaction_client/RedirectClient.spec.ts | 5 +-- .../SilentCacheClient.spec.ts | 5 +-- lib/msal-common/package.json | 1 + .../test/account/AccountInfo.spec.ts | 2 +- .../test/cache/CacheManager.spec.ts | 5 +-- lib/msal-common/test/cache/MockCache.ts | 5 +-- .../test/cache/entities/AccountEntity.spec.ts | 2 +- .../test/client/RefreshTokenClient.spec.ts | 2 +- .../test/client/SilentFlowClient.spec.ts | 2 +- package-lock.json | 16 ++++++++ shared-test-utils/package-lock.json | 39 +++++++++++++++++++ shared-test-utils/package.json | 11 ++++++ .../{ => src}/CredentialGenerators.ts | 0 shared-test-utils/src/index.ts | 4 ++ 19 files changed, 84 insertions(+), 30 deletions(-) create mode 100644 shared-test-utils/package-lock.json create mode 100644 shared-test-utils/package.json rename shared-test-utils/{ => src}/CredentialGenerators.ts (100%) create mode 100644 shared-test-utils/src/index.ts diff --git a/lib/msal-browser/package.json b/lib/msal-browser/package.json index 21222ec9ec..1c8665799d 100644 --- a/lib/msal-browser/package.json +++ b/lib/msal-browser/package.json @@ -86,6 +86,7 @@ "@types/sinon": "^7.5.0", "dotenv": "^8.2.0", "eslint-config-msal": "^0.0.0", + "msal-test-utils": "^0.0.1", "fake-indexeddb": "^3.1.3", "jest": "^29.5.0", "jest-environment-jsdom": "^29.5.0", diff --git a/lib/msal-browser/test/app/PublicClientApplication.spec.ts b/lib/msal-browser/test/app/PublicClientApplication.spec.ts index 27e25623cb..5007b6397e 100644 --- a/lib/msal-browser/test/app/PublicClientApplication.spec.ts +++ b/lib/msal-browser/test/app/PublicClientApplication.spec.ts @@ -113,10 +113,7 @@ import { Configuration, buildConfiguration, } from "../../src/config/Configuration"; -import { - buildAccountFromIdTokenClaims, - buildIdToken, -} from "../../../../shared-test-utils/CredentialGenerators"; +import { buildAccountFromIdTokenClaims, buildIdToken } from "msal-test-utils"; const cacheConfig = { temporaryCacheLocation: BrowserCacheLocation.SessionStorage, diff --git a/lib/msal-browser/test/cache/TokenCache.spec.ts b/lib/msal-browser/test/cache/TokenCache.spec.ts index 16681ded49..4e7a41f7fd 100644 --- a/lib/msal-browser/test/cache/TokenCache.spec.ts +++ b/lib/msal-browser/test/cache/TokenCache.spec.ts @@ -38,7 +38,7 @@ import { } from "../utils/StringConstants"; import { BrowserAuthErrorMessage, SilentRequest } from "../../src"; import { base64Decode } from "../../src/encode/Base64Decode"; -import { buildAccountFromIdTokenClaims } from "../../../../shared-test-utils/CredentialGenerators"; +import { buildAccountFromIdTokenClaims } from "msal-test-utils"; describe("TokenCache tests", () => { let configuration: BrowserConfiguration; diff --git a/lib/msal-browser/test/event/EventHandler.spec.ts b/lib/msal-browser/test/event/EventHandler.spec.ts index 10e873ccd6..9f1aeda536 100644 --- a/lib/msal-browser/test/event/EventHandler.spec.ts +++ b/lib/msal-browser/test/event/EventHandler.spec.ts @@ -5,7 +5,7 @@ import sinon from "sinon"; import { EventHandler } from "../../src/event/EventHandler"; import { Logger, LogLevel, AccountInfo, AccountEntity } from "../../src"; import { CryptoOps } from "../../src/crypto/CryptoOps"; -import { buildAccountFromIdTokenClaims } from "../../../../shared-test-utils/CredentialGenerators"; +import { buildAccountFromIdTokenClaims } from "msal-test-utils"; import { ID_TOKEN_CLAIMS } from "../utils/StringConstants"; describe("Event API tests", () => { diff --git a/lib/msal-browser/test/interaction_client/NativeInteractionClient.spec.ts b/lib/msal-browser/test/interaction_client/NativeInteractionClient.spec.ts index 9d6f6a4f43..bb026b9806 100644 --- a/lib/msal-browser/test/interaction_client/NativeInteractionClient.spec.ts +++ b/lib/msal-browser/test/interaction_client/NativeInteractionClient.spec.ts @@ -42,10 +42,7 @@ import { getDefaultPerformanceClient } from "../utils/TelemetryUtils"; import { CryptoOps } from "../../src/crypto/CryptoOps"; import { BrowserCacheManager } from "../../src/cache/BrowserCacheManager"; import { IPublicClientApplication } from "../../src"; -import { - buildAccountFromIdTokenClaims, - buildIdToken, -} from "../../../../shared-test-utils/CredentialGenerators"; +import { buildAccountFromIdTokenClaims, buildIdToken } from "msal-test-utils"; const networkInterface = { sendGetRequestAsync(): T { diff --git a/lib/msal-browser/test/interaction_client/RedirectClient.spec.ts b/lib/msal-browser/test/interaction_client/RedirectClient.spec.ts index c4a5cc67ee..490463da8f 100644 --- a/lib/msal-browser/test/interaction_client/RedirectClient.spec.ts +++ b/lib/msal-browser/test/interaction_client/RedirectClient.spec.ts @@ -80,10 +80,7 @@ import { NativeInteractionClient } from "../../src/interaction_client/NativeInte import { NativeMessageHandler } from "../../src/broker/nativeBroker/NativeMessageHandler"; import { getDefaultPerformanceClient } from "../utils/TelemetryUtils"; import { AuthenticationResult } from "../../src/response/AuthenticationResult"; -import { - buildAccountFromIdTokenClaims, - buildIdToken, -} from "../../../../shared-test-utils/CredentialGenerators"; +import { buildAccountFromIdTokenClaims, buildIdToken } from "msal-test-utils"; const cacheConfig = { cacheLocation: BrowserCacheLocation.SessionStorage, diff --git a/lib/msal-browser/test/interaction_client/SilentCacheClient.spec.ts b/lib/msal-browser/test/interaction_client/SilentCacheClient.spec.ts index 3d6546656d..b60afc2cd0 100644 --- a/lib/msal-browser/test/interaction_client/SilentCacheClient.spec.ts +++ b/lib/msal-browser/test/interaction_client/SilentCacheClient.spec.ts @@ -24,10 +24,7 @@ import { AuthenticationResult, AccountInfo, } from "@azure/msal-common"; -import { - buildAccountFromIdTokenClaims, - buildIdToken, -} from "../../../../shared-test-utils/CredentialGenerators"; +import { buildAccountFromIdTokenClaims, buildIdToken } from "msal-test-utils"; const testAccountEntity: AccountEntity = buildAccountFromIdTokenClaims( ID_TOKEN_CLAIMS, diff --git a/lib/msal-common/package.json b/lib/msal-common/package.json index 56217cb9e8..ad10b1269a 100644 --- a/lib/msal-common/package.json +++ b/lib/msal-common/package.json @@ -79,6 +79,7 @@ "@types/node": "^20.3.1", "@types/sinon": "^7.5.0", "eslint-config-msal": "^0.0.0", + "msal-test-utils": "^0.0.1", "jest": "^29.5.0", "lodash": "^4.17.21", "prettier": "2.8.7", diff --git a/lib/msal-common/test/account/AccountInfo.spec.ts b/lib/msal-common/test/account/AccountInfo.spec.ts index bf331af811..51d853d07a 100644 --- a/lib/msal-common/test/account/AccountInfo.spec.ts +++ b/lib/msal-common/test/account/AccountInfo.spec.ts @@ -1,6 +1,6 @@ import { AccountEntity } from "../../src"; import * as AccountInfo from "../../src/account/AccountInfo"; -import { buildAccountFromIdTokenClaims } from "../../../../shared-test-utils/CredentialGenerators"; +import { buildAccountFromIdTokenClaims } from "msal-test-utils"; import { ID_TOKEN_ALT_CLAIMS, ID_TOKEN_CLAIMS, diff --git a/lib/msal-common/test/cache/CacheManager.spec.ts b/lib/msal-common/test/cache/CacheManager.spec.ts index b3770f83ba..1874e6dec5 100644 --- a/lib/msal-common/test/cache/CacheManager.spec.ts +++ b/lib/msal-common/test/cache/CacheManager.spec.ts @@ -31,10 +31,7 @@ import { } from "../../src/error/ClientAuthError"; import { AccountInfo } from "../../src/account/AccountInfo"; import { MockCache } from "./MockCache"; -import { - buildAccountFromIdTokenClaims, - buildIdToken, -} from "../../../../shared-test-utils/CredentialGenerators"; +import { buildAccountFromIdTokenClaims, buildIdToken } from "msal-test-utils"; import { mockCrypto } from "../client/ClientTestUtils"; import { TestError } from "../test_kit/TestErrors"; import { CacheManager } from "../../src/cache/CacheManager"; diff --git a/lib/msal-common/test/cache/MockCache.ts b/lib/msal-common/test/cache/MockCache.ts index ca2e2f9bc6..0e44b2de36 100644 --- a/lib/msal-common/test/cache/MockCache.ts +++ b/lib/msal-common/test/cache/MockCache.ts @@ -22,10 +22,7 @@ import { ID_TOKEN_ALT_CLAIMS, GUEST_ID_TOKEN_CLAIMS, } from "../test_kit/StringConstants"; -import { - buildAccountFromIdTokenClaims, - buildIdToken, -} from "../../../../shared-test-utils/CredentialGenerators"; +import { buildAccountFromIdTokenClaims, buildIdToken } from "msal-test-utils"; export class MockCache { cacheManager: MockStorageClass; diff --git a/lib/msal-common/test/cache/entities/AccountEntity.spec.ts b/lib/msal-common/test/cache/entities/AccountEntity.spec.ts index 8cdd3a12ff..ec87226ad8 100644 --- a/lib/msal-common/test/cache/entities/AccountEntity.spec.ts +++ b/lib/msal-common/test/cache/entities/AccountEntity.spec.ts @@ -28,7 +28,7 @@ import { LogLevel, Logger } from "../../../src/logger/Logger"; import { Authority } from "../../../src/authority/Authority"; import { AuthorityType } from "../../../src/authority/AuthorityType"; import { TokenClaims } from "../../../src"; -import { buildAccountFromIdTokenClaims } from "../../../../../shared-test-utils/CredentialGenerators"; +import { buildAccountFromIdTokenClaims } from "msal-test-utils"; const cryptoInterface: ICrypto = { createNewGuid(): string { diff --git a/lib/msal-common/test/client/RefreshTokenClient.spec.ts b/lib/msal-common/test/client/RefreshTokenClient.spec.ts index b5e869e5fd..bbb5ae6e92 100644 --- a/lib/msal-common/test/client/RefreshTokenClient.spec.ts +++ b/lib/msal-common/test/client/RefreshTokenClient.spec.ts @@ -57,7 +57,7 @@ import { } from "../../src/error/InteractionRequiredAuthError"; import { StubPerformanceClient } from "../../src/telemetry/performance/StubPerformanceClient"; import { ProtocolMode } from "../../src/authority/ProtocolMode"; -import { buildAccountFromIdTokenClaims } from "../../../../shared-test-utils/CredentialGenerators"; +import { buildAccountFromIdTokenClaims } from "msal-test-utils"; const testAccountEntity: AccountEntity = new AccountEntity(); testAccountEntity.homeAccountId = `${TEST_DATA_CLIENT_INFO.TEST_UID}.${TEST_DATA_CLIENT_INFO.TEST_UTID}`; diff --git a/lib/msal-common/test/client/SilentFlowClient.spec.ts b/lib/msal-common/test/client/SilentFlowClient.spec.ts index bc357fb1f3..d2c8827783 100644 --- a/lib/msal-common/test/client/SilentFlowClient.spec.ts +++ b/lib/msal-common/test/client/SilentFlowClient.spec.ts @@ -55,7 +55,7 @@ import { } from "../../src/error/InteractionRequiredAuthError"; import { StubPerformanceClient } from "../../src/telemetry/performance/StubPerformanceClient"; import { Logger } from "../../src/logger/Logger"; -import { buildAccountFromIdTokenClaims } from "../../../../shared-test-utils/CredentialGenerators"; +import { buildAccountFromIdTokenClaims } from "msal-test-utils"; const testAccountEntity: AccountEntity = buildAccountFromIdTokenClaims(ID_TOKEN_CLAIMS); diff --git a/package-lock.json b/package-lock.json index 697204a38c..182198a583 100644 --- a/package-lock.json +++ b/package-lock.json @@ -193,6 +193,7 @@ "fake-indexeddb": "^3.1.3", "jest": "^29.5.0", "jest-environment-jsdom": "^29.5.0", + "msal-test-utils": "^0.0.1", "prettier": "2.8.7", "rimraf": "^3.0.0", "rollup": "^3.14.0", @@ -238,6 +239,7 @@ "eslint-config-msal": "^0.0.0", "jest": "^29.5.0", "lodash": "^4.17.21", + "msal-test-utils": "^0.0.1", "prettier": "2.8.7", "rimraf": "^3.0.2", "rollup": "^3.14.0", @@ -38601,6 +38603,10 @@ "resolved": "samples/msal-react-samples/gatsby-sample", "link": true }, + "node_modules/msal-test-utils": { + "resolved": "shared-test-utils", + "link": true + }, "node_modules/msal-vue-sample": { "resolved": "samples/msal-browser-samples/vue3-sample-app", "link": true @@ -74467,6 +74473,16 @@ "eslint-plugin-react-hooks": "^4.1.2", "eslint-plugin-security": "^1.4.0" } + }, + "shared-test-utils": { + "name": "msal-test-utils", + "version": "0.0.1", + "dev": true, + "license": "MIT", + "devDependencies": { + "@azure/msal-common": "^14.0.0", + "typescript": "^4.9.5" + } } } } diff --git a/shared-test-utils/package-lock.json b/shared-test-utils/package-lock.json new file mode 100644 index 0000000000..a537191963 --- /dev/null +++ b/shared-test-utils/package-lock.json @@ -0,0 +1,39 @@ +{ + "name": "msal-test-utils", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "msal-test-utils", + "version": "0.0.1", + "license": "MIT", + "devDependencies": { + "@azure/msal-common": "^14.0.0", + "typescript": "^4.9.5" + } + }, + "node_modules/@azure/msal-common": { + "version": "14.4.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.4.0.tgz", + "integrity": "sha512-ffCymScQuMKVj+YVfwNI52A5Tu+uiZO2eTf+c+3TXxdAssks4nokJhtr+uOOMxH0zDi6d1OjFKFKeXODK0YLSg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + } + } +} diff --git a/shared-test-utils/package.json b/shared-test-utils/package.json new file mode 100644 index 0000000000..7f440ba024 --- /dev/null +++ b/shared-test-utils/package.json @@ -0,0 +1,11 @@ +{ + "name": "msal-test-utils", + "version": "0.0.1", + "license": "MIT", + "private": true, + "main": "src/index.ts", + "devDependencies": { + "typescript": "^4.9.5", + "@azure/msal-common": "^14.0.0" + } +} diff --git a/shared-test-utils/CredentialGenerators.ts b/shared-test-utils/src/CredentialGenerators.ts similarity index 100% rename from shared-test-utils/CredentialGenerators.ts rename to shared-test-utils/src/CredentialGenerators.ts diff --git a/shared-test-utils/src/index.ts b/shared-test-utils/src/index.ts new file mode 100644 index 0000000000..1db0a9e266 --- /dev/null +++ b/shared-test-utils/src/index.ts @@ -0,0 +1,4 @@ +export { + buildAccountFromIdTokenClaims, + buildIdToken, +} from "./CredentialGenerators"; From e27fff7003b3a3dd45126f46e2edc8048181e78a Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Tue, 28 Nov 2023 14:56:20 -0800 Subject: [PATCH 90/91] Add shared-utils to workspaces --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index a45a7cfacd..bae545a5b3 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "lib/msal-angular", "lib/msal-react", "extensions/msal-node-extensions", + "shared-test-utils", "samples/e2eTestUtils", "samples/msal-browser-samples/*", "samples/msal-angular-v3-samples/*", From 8363d228a8f0996df056196b8f9f34880355fb50 Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Tue, 28 Nov 2023 14:59:14 -0800 Subject: [PATCH 91/91] Update package-lock --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index f78a8db98f..10620cf2cb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "lib/msal-angular", "lib/msal-react", "extensions/msal-node-extensions", + "shared-test-utils", "samples/e2eTestUtils", "samples/msal-browser-samples/*", "samples/msal-angular-v3-samples/*", @@ -84,7 +85,7 @@ "typescript": "^4.9.5" }, "engines": { - "node": "18 || 20" + "node": "16 || 18 || 20" } }, "extensions/msal-node-extensions/node_modules/@types/node": { @@ -74663,7 +74664,6 @@ "shared-test-utils": { "name": "msal-test-utils", "version": "0.0.1", - "dev": true, "license": "MIT", "devDependencies": { "@azure/msal-common": "^14.0.0",