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" +} 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-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/src/cache/BrowserCacheManager.ts b/lib/msal-browser/src/cache/BrowserCacheManager.ts index 3277f78248..a6c6c75440 100644 --- a/lib/msal-browser/src/cache/BrowserCacheManager.ts +++ b/lib/msal-browser/src/cache/BrowserCacheManager.ts @@ -394,15 +394,31 @@ 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; @@ -505,6 +521,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 @@ -1034,6 +1059,7 @@ export class BrowserCacheManager extends CacheManager { return this.getAccountInfoFilteredBy({ homeAccountId: activeAccountValueObj.homeAccountId, localAccountId: activeAccountValueObj.localAccountId, + tenantId: activeAccountValueObj.tenantId, }); } this.logger.trace( @@ -1058,6 +1084,7 @@ export class BrowserCacheManager extends CacheManager { const activeAccountValue: ActiveAccountFilters = { homeAccountId: account.homeAccountId, localAccountId: account.localAccountId, + tenantId: account.tenantId, }; this.browserStorage.setItem( activeAccountKey, diff --git a/lib/msal-browser/src/cache/TokenCache.ts b/lib/msal-browser/src/cache/TokenCache.ts index cad72715a9..a51cdf1b25 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, + base64Decode, 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/controllers/StandardController.ts b/lib/msal-browser/src/controllers/StandardController.ts index 55038dbdd2..dd60452515 100644 --- a/lib/msal-browser/src/controllers/StandardController.ts +++ b/lib/msal-browser/src/controllers/StandardController.ts @@ -1549,7 +1549,7 @@ export class StandardController implements IController { ): string { const account = request.account || - this.browserStorage.getAccountInfoFilteredBy({ + this.getAccount({ loginHint: request.loginHint, sid: request.sid, }) || 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/src/interaction_client/NativeInteractionClient.ts b/lib/msal-browser/src/interaction_client/NativeInteractionClient.ts index 3e0742c3d9..502fc37c60 100644 --- a/lib/msal-browser/src/interaction_client/NativeInteractionClient.ts +++ b/lib/msal-browser/src/interaction_client/NativeInteractionClient.ts @@ -33,7 +33,9 @@ import { invokeAsync, createAuthError, AuthErrorCodes, + updateAccountTenantProfileData, CacheHelpers, + buildAccountToCache, } from "@azure/msal-common"; import { BaseInteractionClient } from "./BaseInteractionClient"; import { BrowserConfiguration } from "../config/Configuration"; @@ -420,14 +422,18 @@ export class NativeInteractionClient extends BaseInteractionClient { response, idTokenClaims ); - const accountEntity = AccountEntity.createAccount( - { - homeAccountId: homeAccountIdentifier, - idTokenClaims: idTokenClaims, - clientInfo: response.client_info, - nativeAccountId: response.account.id, - }, - authority + + const baseAccount = buildAccountToCache( + this.browserStorage, + authority, + homeAccountIdentifier, + idTokenClaims, + base64Decode, + response.client_info, + idTokenClaims.tid, + undefined, + response.account.id, + this.logger ); // generate authenticationResult @@ -435,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, @@ -580,14 +586,11 @@ 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 = updateAccountTenantProfileData( + accountEntity.getAccountInfo(), + undefined, // tenantProfile optional + 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 dc478b660c..5007b6397e 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, @@ -43,7 +44,6 @@ import { IdTokenEntity, CacheManager, PersistentCacheKeys, - Authority, AuthError, ProtocolMode, ServerResponseType, @@ -113,6 +113,7 @@ import { Configuration, buildConfiguration, } from "../../src/config/Configuration"; +import { buildAccountFromIdTokenClaims, buildIdToken } from "msal-test-utils"; const cacheConfig = { temporaryCacheLocation: BrowserCacheLocation.SessionStorage, @@ -772,16 +773,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, @@ -5073,39 +5065,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: TEST_CONFIG.OID, - idToken: TEST_TOKENS.IDTOKEN_V2, - idTokenClaims: ID_TOKEN_CLAIMS, - nativeAccountId: undefined, - }; - - 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"; - 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(); @@ -5126,9 +5099,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(() => { @@ -5139,22 +5112,20 @@ 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?.idToken).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?.idToken).not.toBeUndefined(); - expect(activeAccount).toEqual(testAccountInfo1); + expect(activeAccount).toEqual(matchAccount); pca.clearCache({ - account: testAccountInfo1, + account: matchAccount, correlationId: "test123", }); expect(pca.getActiveAccount()).toEqual(null); @@ -5163,142 +5134,35 @@ describe("PublicClientApplication.ts Class Unit 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: 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, - }; + const testAccount1: AccountEntity = + buildAccountFromIdTokenClaims(ID_TOKEN_CLAIMS); + const testAccountInfo1: AccountInfo = testAccount1.getAccountInfo(); + testAccountInfo1.idTokenClaims = ID_TOKEN_CLAIMS; - 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"; 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: 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, - nativeAccountId: undefined, - }; - 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, - }; - - // 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, - idTokenClaims: { ...ID_TOKEN_CLAIMS, login_hint: "testLoginHint" }, - nativeAccountId: undefined, - }; + const testAccount2: AccountEntity = + buildAccountFromIdTokenClaims(ID_TOKEN_ALT_CLAIMS); + const testAccountInfo2: AccountInfo = testAccount2.getAccountInfo(); + testAccountInfo2.idTokenClaims = ID_TOKEN_ALT_CLAIMS; - 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"; - - 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, - idTokenClaims: { ...ID_TOKEN_CLAIMS, upn: "testUpn" }, - 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"; - - 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; @@ -5322,21 +5186,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(() => { @@ -5346,18 +5201,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[0].idToken).not.toBeUndefined(); - expect(accounts[1].idToken).not.toBeUndefined(); - expect(accounts[2].idToken).not.toBeUndefined(); - expect(accounts[3].idToken).not.toBeUndefined(); + expect(accounts).toHaveLength(2); + expect(accounts[0].idTokenClaims).not.toBeUndefined(); + expect(accounts[1].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", () => { @@ -5367,20 +5221,24 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { }); it("getAccountByUsername returns account specified", () => { - const account = pca.getAccountByUsername("example@microsoft.com"); - expect(account?.idToken).not.toBeUndefined(); + 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("Example@Microsoft.com"); - expect(account?.idToken).not.toBeUndefined(); + 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?.idToken).not.toBeUndefined(); + expect(account2?.idTokenClaims).not.toBeUndefined(); expect(account2).toEqual(testAccountInfo2); }); @@ -5399,9 +5257,9 @@ 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?.idToken).not.toBeUndefined(); + expect(account?.idTokenClaims).not.toBeUndefined(); expect(account).toEqual(testAccountInfo1); }); @@ -5417,8 +5275,8 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { }); it("getAccountByLocalId returns account specified", () => { - const account = pca.getAccountByLocalId(TEST_CONFIG.OID); - expect(account?.idToken).not.toBeUndefined(); + const account = pca.getAccountByLocalId(ID_TOKEN_CLAIMS.oid); + expect(account?.idTokenClaims).not.toBeUndefined(); expect(account).toEqual(testAccountInfo1); }); @@ -5442,72 +5300,72 @@ 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?.idToken).not.toBeUndefined(); + expect(account?.idTokenClaims).not.toBeUndefined(); expect(account?.homeAccountId).toEqual( - testAccountInfo3.homeAccountId + testAccountInfo1.homeAccountId ); }); 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?.idTokenClaims).not.toBeUndefined(); expect(account?.homeAccountId).toEqual( - testAccountInfo3.homeAccountId + testAccountInfo2.homeAccountId ); }); it("getAccount returns account specified using upn", () => { const account = pca.getAccount({ - loginHint: "testUpn", + loginHint: ID_TOKEN_CLAIMS.upn, }); - expect(account?.idToken).not.toBeUndefined(); + 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?.idToken).not.toBeUndefined(); + 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?.idToken).not.toBeUndefined(); + expect(account?.idTokenClaims).not.toBeUndefined(); expect(account).toEqual(testAccountInfo1); }); 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?.idTokenClaims).not.toBeUndefined(); expect(account).toEqual(testAccountInfo1); }); 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?.idTokenClaims).not.toBeUndefined(); expect(account).toEqual(testAccountInfo1); }); 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?.idTokenClaims).not.toBeUndefined(); expect(account).toEqual(testAccountInfo1); }); }); @@ -5515,74 +5373,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; - testAccount2.idTokenClaims = ID_TOKEN_CLAIMS; - - 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; @@ -5628,38 +5445,10 @@ 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); }); - 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: { @@ -5703,7 +5492,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 ); @@ -5717,13 +5506,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 ); @@ -5735,7 +5524,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); @@ -5944,15 +5733,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 a0d5f27cfb..a0a1888ea9 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,23 @@ export class TestStorageManager extends CacheManager { return null; } + removeAccountKeyFromMap(key: string): void { + const currentAccounts = this.getAccountKeys(); + this.store[ACCOUNT_KEYS] = currentAccounts.filter( + (entry) => entry !== key + ); + } + + 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 +68,11 @@ 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 { + this.removeAccount(accountKey); } getAccountKeys(): string[] { diff --git a/lib/msal-browser/test/cache/TokenCache.spec.ts b/lib/msal-browser/test/cache/TokenCache.spec.ts index 951886331a..4e7a41f7fd 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 "msal-test-utils"; 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,16 +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, - }; + 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 5c9cd773fc..9f1aeda536 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 "msal-test-utils"; +import { ID_TOKEN_CLAIMS } from "../utils/StringConstants"; describe("Event API tests", () => { const loggerOptions = { @@ -117,7 +119,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,28 +128,10 @@ describe("Event API tests", () => { const eventHandler = new EventHandler(logger, browserCrypto); eventHandler.addEventCallback(subscriber); - const accountEntity = { - 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 accountEntity: AccountEntity = + buildAccountFromIdTokenClaims(ID_TOKEN_CLAIMS); - 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, - }; + const account: AccountInfo = accountEntity.getAccountInfo(); const cacheKey1 = AccountEntity.generateAccountCacheKey(account); @@ -172,28 +156,10 @@ describe("Event API tests", () => { const eventHandler = new EventHandler(logger, browserCrypto); eventHandler.addEventCallback(subscriber); - const accountEntity = { - 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 accountEntity: AccountEntity = + buildAccountFromIdTokenClaims(ID_TOKEN_CLAIMS); - 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, - }; + const account: AccountInfo = accountEntity.getAccountInfo(); const cacheKey1 = AccountEntity.generateAccountCacheKey(account); diff --git a/lib/msal-browser/test/interaction_client/BaseInteractionClient.spec.ts b/lib/msal-browser/test/interaction_client/BaseInteractionClient.spec.ts index 6466e1e03e..628efcefcc 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"; @@ -75,20 +77,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 || "", @@ -114,19 +107,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 idToken2: IdTokenEntity = { 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 6a2f40e524..bb026b9806 100644 --- a/lib/msal-browser/test/interaction_client/NativeInteractionClient.spec.ts +++ b/lib/msal-browser/test/interaction_client/NativeInteractionClient.spec.ts @@ -42,6 +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 "msal-test-utils"; const networkInterface = { sendGetRequestAsync(): T { @@ -52,29 +53,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"; - -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}`, @@ -91,7 +98,7 @@ const testAccessTokenEntity: AccessTokenEntity = { const testCacheRecord: CacheRecord = { account: testAccountEntity, - idToken: testIdToken, + idToken: TEST_ID_TOKEN, accessToken: testAccessTokenEntity, refreshToken: null, appMetadata: null, @@ -211,10 +218,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, @@ -226,7 +233,7 @@ describe("NativeInteractionClient Tests", () => { sinon .stub(CacheManager.prototype, "getBaseAccountInfo") - .returns(testAccountInfo); + .returns(TEST_ACCOUNT_INFO); sinon .stub(CacheManager.prototype, "readCacheRecord") @@ -237,60 +244,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, - }; 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); }); @@ -329,137 +315,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, - }; 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, - }; 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, - }; 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); }); @@ -495,36 +418,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, - }; 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( @@ -553,49 +453,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, - }; 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( @@ -624,36 +503,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 () => { @@ -664,9 +535,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(); @@ -689,9 +560,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(); @@ -714,9 +585,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(); @@ -735,17 +606,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") @@ -757,7 +618,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"], @@ -786,30 +647,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, - }; - sinon .stub(NavigationClient.prototype, "navigateExternal") .callsFake((url: string) => { @@ -819,7 +656,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); @@ -832,17 +669,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, }; @@ -851,30 +688,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, - }; - sinon .stub(NavigationClient.prototype, "navigateExternal") .callsFake((url: string) => { @@ -890,7 +703,7 @@ describe("NativeInteractionClient Tests", () => { expect( messageBody.request && messageBody.request.prompt ).toBe(undefined); - return Promise.resolve(mockWamResponse); + return Promise.resolve(MOCK_WAM_RESPONSE); } ); // @ts-ignore @@ -905,17 +718,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, }; @@ -923,17 +736,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") @@ -947,7 +750,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( @@ -979,17 +782,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") @@ -1000,7 +793,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/PopupClient.spec.ts b/lib/msal-browser/test/interaction_client/PopupClient.spec.ts index 3995d365cb..df8f318165 100644 --- a/lib/msal-browser/test/interaction_client/PopupClient.spec.ts +++ b/lib/msal-browser/test/interaction_client/PopupClient.spec.ts @@ -1169,7 +1169,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); @@ -1257,7 +1256,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 fca9f2eddf..490463da8f 100644 --- a/lib/msal-browser/test/interaction_client/RedirectClient.spec.ts +++ b/lib/msal-browser/test/interaction_client/RedirectClient.spec.ts @@ -80,6 +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 "msal-test-utils"; const cacheConfig = { cacheLocation: BrowserCacheLocation.SessionStorage, @@ -478,30 +479,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, @@ -511,6 +499,7 @@ describe("RedirectClient", () => { account: testAccount, tokenType: AuthenticationScheme.BEARER, }; + sinon .stub(FetchClient.prototype, "sendGetRequestAsync") .callsFake((url): any => { @@ -646,31 +635,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, @@ -680,6 +658,7 @@ describe("RedirectClient", () => { account: testAccount, tokenType: AuthenticationScheme.BEARER, }; + sinon .stub(FetchClient.prototype, "sendGetRequestAsync") .callsFake((url): any => { @@ -966,32 +945,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, @@ -1129,32 +1092,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, @@ -1307,32 +1254,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, @@ -2085,18 +2016,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 = { @@ -2109,7 +2030,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({ @@ -2169,23 +2090,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, @@ -3468,7 +3374,6 @@ describe("RedirectClient", () => { testAccount.authorityType = "MSSTS"; testAccount.clientInfo = TEST_DATA_CLIENT_INFO.TEST_CLIENT_INFO_B64ENCODED; - testAccount.idTokenClaims = testIdTokenClaims; browserStorage.setAccount(testAccount); @@ -3523,7 +3428,6 @@ describe("RedirectClient", () => { testAccount.authorityType = "MSSTS"; testAccount.clientInfo = TEST_DATA_CLIENT_INFO.TEST_CLIENT_INFO_B64ENCODED; - testAccount.idTokenClaims = testIdTokenClaims; browserStorage.setAccount(testAccount); @@ -3802,38 +3706,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: TEST_DATA_CLIENT_INFO.TEST_UID, - environment: "login.windows.net", - tenantId: ID_TOKEN_CLAIMS.tid, - username: ID_TOKEN_CLAIMS.preferred_username, - idToken: TEST_TOKENS.IDTOKEN_V2, + ...testAccountEntity.getAccountInfo(), idTokenClaims: ID_TOKEN_CLAIMS, - name: ID_TOKEN_CLAIMS.name, - nativeAccountId: undefined, }; - const testAccount: AccountEntity = new AccountEntity(); - testAccount.homeAccountId = testAccountInfo.homeAccountId; - testAccount.localAccountId = testAccountInfo.localAccountId; - testAccount.environment = testAccountInfo.environment; - testAccount.realm = testAccountInfo.tenantId; - testAccount.username = testAccountInfo.username; - testAccount.name = testAccountInfo.name; - testAccount.authorityType = "MSSTS"; - 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, @@ -3852,7 +3736,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 708185fb7a..b60afc2cd0 100644 --- a/lib/msal-browser/test/interaction_client/SilentCacheClient.spec.ts +++ b/lib/msal-browser/test/interaction_client/SilentCacheClient.spec.ts @@ -24,25 +24,27 @@ import { AuthenticationResult, AccountInfo, } from "@azure/msal-common"; +import { buildAccountFromIdTokenClaims, buildIdToken } from "msal-test-utils"; -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 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 testAccountEntity: AccountEntity = buildAccountFromIdTokenClaims( + ID_TOKEN_CLAIMS, + undefined, + { environment: "login.microsoftonline.com" } +); +const testAccount: AccountInfo = { + ...testAccountEntity.getAccountInfo(), + idTokenClaims: ID_TOKEN_CLAIMS, }; +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}`, clientId: TEST_CONFIG.MSAL_CLIENT_ID, @@ -65,18 +67,6 @@ const testRefreshTokenEntity: RefreshTokenEntity = { 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; @@ -166,10 +156,7 @@ describe("SilentCacheClient", () => { pca.browserStorage.setIdTokenCredential(testIdToken); pca.setActiveAccount(testAccount); - expect(pca.getActiveAccount()).toEqual({ - ...testAccount, - idToken: TEST_TOKENS.IDTOKEN_V2, - }); + 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 f571fa18f3..7f8e38c455 100644 --- a/lib/msal-browser/test/utils/StringConstants.ts +++ b/lib/msal-browser/test/utils/StringConstants.ts @@ -77,7 +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", + "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: + "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: @@ -86,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, @@ -104,12 +102,40 @@ 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", +}; + // Test Expiration Vals export const TEST_TOKEN_LIFETIMES = { DEFAULT_EXPIRES_IN: 3599, @@ -119,18 +145,20 @@ 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_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: - '{"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/docs/multi-tenant-accounts.md b/lib/msal-common/docs/multi-tenant-accounts.md new file mode 100644 index 0000000000..33ed388a26 --- /dev/null +++ b/lib/msal-common/docs/multi-tenant-accounts.md @@ -0,0 +1,243 @@ +# Multi-tenant Support in MSAL JS SDKs + +> 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 + +- [Multi-tenant Accounts](#multi-tenant-accounts) +- [Tenant Profiles](#tenant-profiles) +- [Usage](#usage) + - [Authenticating with Multiple Tenants](#authenticating-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 + +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. + +> 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. + +`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 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 + +### 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 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 when searching the cache for matching tokens, regardless of the tenantId in the request's authority. + +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: 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); +myMSALObj.initialize.then(() => { + // handleRedirectPromise has to be called to resume redirect requests + .handleRedirectPromise() + .then(handleResponse) + .catch((err) => { + console.error(err); + }); +}) + +/** + * Configure base requests + */ +const homeTenantRequest = { + scopes: ["HOME_TENANT_SCOPE"], +}; +const guestTenantRequest = { + scopes: ["GUEST_TENANT_SCOPE"] +}; + +// 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 guestTenantAccount = guestTenantAuthResponse.account; +const guestTenantAccessToken = guestTenantAuthResponse.accessToken; +``` + +### Filtering Multi-tenant Accounts + +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()`. + +#### Using getAccount to search for a specific tenanted account + +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 guestTenantId = "GUEST_TENANT_ID"; + +// Get the guest tenant account +const guestTenantAccount = myMSALObj.getAccount({ + homeAccountId: homeAccountId, + tenantId: guestTenantId, +}); + +// 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 +} +``` + +#### 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. + +How flattening multit-tenant accounts works: + +- 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 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 +- 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 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 + 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 { + // 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, + }); +} +``` + +## 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. 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/src/account/AccountInfo.ts b/lib/msal-common/src/account/AccountInfo.ts index 3471373ae9..a0e11c68bc 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 + * - tenantProfiles - Map of tenant profile objects for each tenant that the account has authenticated with in the browser */ export type AccountInfo = { homeAccountId: string; @@ -35,9 +36,104 @@ export type AccountInfo = { }; nativeAccountId?: string; authorityType?: string; + tenantProfiles?: Map; +}; + +/** + * Account details that vary across tenants for the same user + */ +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; }; export type ActiveAccountFilters = { homeAccountId: string; localAccountId: string; + 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; + + /** + * 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, + 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 baseAccountInfo + * @param idTokenClaims + * @returns + */ +export function updateAccountTenantProfileData( + baseAccountInfo: AccountInfo, + tenantProfile?: TenantProfile, + idTokenClaims?: TokenClaims +): AccountInfo { + let updatedAccountInfo = baseAccountInfo; + // Tenant Profile overrides passed in account info + if (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, ...claimsSourcedTenantProfile } = + buildTenantProfileFromIdTokenClaims( + baseAccountInfo.homeAccountId, + idTokenClaims + ); + + updatedAccountInfo = { + ...updatedAccountInfo, + ...claimsSourcedTenantProfile, + idTokenClaims: idTokenClaims, + }; + + return updatedAccountInfo; + } + + return updatedAccountInfo; +} 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/account/TokenClaims.ts b/lib/msal-common/src/account/TokenClaims.ts index c747c6663e..f8cc1bde29 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 || null; + } + return null; +} diff --git a/lib/msal-common/src/authority/Authority.ts b/lib/msal-common/src/authority/Authority.ts index 6a2f7672da..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; } @@ -1253,6 +1253,33 @@ export class Authority { } } +/** + * Extract tenantId from authority + */ +export function getTenantFromAuthorityString( + authority: string +): string | undefined { + const authorityUrl = new UrlString(authority); + const authorityUrlComponents = authorityUrl.getUrlComponents(); + /** + * 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 + */ + 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 { return authorityUri.endsWith(Constants.FORWARD_SLASH) ? authorityUri diff --git a/lib/msal-common/src/cache/CacheManager.ts b/lib/msal-common/src/cache/CacheManager.ts index b05c7844db..ca99254793 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 { @@ -32,7 +33,12 @@ import { createClientAuthError, ClientAuthErrorCodes, } from "../error/ClientAuthError"; -import { AccountInfo } from "../account/AccountInfo"; +import { + AccountInfo, + TenantProfile, + tenantIdMatchesHomeTenant, + updateAccountTenantProfileData, +} from "../account/AccountInfo"; import { AppMetadataEntity } from "./entities/AppMetadataEntity"; import { ServerTelemetryEntity } from "./entities/ServerTelemetryEntity"; import { ThrottlingEntity } from "./entities/ThrottlingEntity"; @@ -43,6 +49,7 @@ import { BaseAuthRequest } from "../request/BaseAuthRequest"; import { Logger } from "../logger/Logger"; import { name, version } from "../packageMetadata"; import { StoreInCache } from "../request/StoreInCache"; +import { getTenantFromAuthorityString } from "../authority/Authority"; import { getAliasesFromStaticSources } from "../authority/AuthorityMetadata"; import { StaticAuthorityOptions } from "../authority/AuthorityOptions"; import { TokenClaims } from "../account/TokenClaims"; @@ -75,7 +82,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 @@ -83,6 +98,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 @@ -238,31 +258,23 @@ export abstract class CacheManager implements ICacheManager { * @returns Array of AccountInfo objects in cache */ getAllAccounts(accountFilter?: AccountFilter): AccountInfo[] { - const validAccounts: AccountInfo[] = []; - this.getAccountsFilteredBy(accountFilter || {}).forEach( - (accountEntity: AccountEntity) => { - const accountInfo = this.getAccountInfoFromEntity( - accountEntity, - accountFilter - ); - if (accountInfo) { - validAccounts.push(accountInfo); - } - } + return this.buildTenantProfiles( + this.getAccountsFilteredBy(accountFilter || {}), + accountFilter ); - return validAccounts; } /** - * 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]; @@ -285,52 +297,214 @@ export abstract class CacheManager implements ICacheManager { } } - private getAccountInfoFromEntity( + /** + * 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.flatMap((accountEntity) => { + return this.getAccountInfoForTenantProfiles( + accountEntity, + accountFilter + ); + }); + } + + private getAccountInfoForTenantProfiles( accountEntity: AccountEntity, accountFilter?: AccountFilter + ): AccountInfo[] { + return this.getTenantProfilesFromAccountEntity( + accountEntity, + accountFilter?.tenantId, + accountFilter + ); + } + + private getTenantedAccountInfoByFilter( + accountInfo: AccountInfo, + tokenKeys: TokenKeys, + tenantProfile: TenantProfile, + tenantProfileFilter?: TenantProfileFilter ): AccountInfo | null { - const accountInfo = accountEntity.getAccountInfo(); - const idToken = this.getIdToken(accountInfo); + 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 + ); + if (idToken) { - const idTokenClaims = extractTokenClaims( + idTokenClaims = extractTokenClaims( idToken.secret, this.cryptoImpl.base64Decode ); if ( - this.idTokenClaimsMatchAccountFilter( + !this.idTokenClaimsMatchTenantProfileFilter( idTokenClaims, - accountFilter + tenantProfileFilter ) ) { - accountInfo.idToken = idToken.secret; - accountInfo.idTokenClaims = idTokenClaims; - return accountInfo; + // ID token sourced claims don't match so this tenant profile is not a match + return null; + } + } + + // 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 idTokenClaimsMatchAccountFilter( + private idTokenClaimsMatchTenantProfileFilter( idTokenClaims: TokenClaims, - accountFilter?: AccountFilter + tenantProfileFilter?: TenantProfileFilter ): boolean { - if (accountFilter) { + // Tenant Profile filtering + if (tenantProfileFilter) { + if ( + !!tenantProfileFilter.localAccountId && + !this.matchLocalAccountIdFromTokenClaims( + idTokenClaims, + tenantProfileFilter.localAccountId + ) + ) { + return false; + } + + if ( + !!tenantProfileFilter.loginHint && + !this.matchLoginHintFromTokenClaims( + idTokenClaims, + tenantProfileFilter.loginHint + ) + ) { + return false; + } + + if ( + !!tenantProfileFilter.username && + !this.matchUsername( + idTokenClaims.preferred_username, + tenantProfileFilter.username + ) + ) { + return false; + } + if ( - !!accountFilter.loginHint && - !this.matchLoginHint(idTokenClaims, accountFilter.loginHint) + !!tenantProfileFilter.name && + !this.matchName(idTokenClaims, tenantProfileFilter.name) ) { return false; } + if ( - !!accountFilter.sid && - !this.matchSid(idTokenClaims, accountFilter.sid) + !!tenantProfileFilter.sid && + !this.matchSid(idTokenClaims, tenantProfileFilter.sid) ) { return false; } } + return true; } + /** * saves a cache record * @param cacheRecord @@ -414,7 +588,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 */ @@ -422,18 +596,17 @@ export abstract class CacheManager implements ICacheManager { const allAccountKeys = this.getAccountKeys(); 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; } - const entity: AccountEntity | null = this.getAccount(cacheKey); + const entity: AccountEntity | null = this.getAccount( + cacheKey, + this.commonLogger + ); + + // Match base account fields if (!entity) { return; @@ -446,16 +619,9 @@ export abstract class CacheManager implements ICacheManager { return; } - if ( - !!accountFilter.localAccountId && - !this.matchLocalAccountId(entity, accountFilter.localAccountId) - ) { - return; - } - if ( !!accountFilter.username && - !this.matchUsername(entity, accountFilter.username) + !this.matchUsername(entity.username, accountFilter.username) ) { return; } @@ -474,14 +640,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( @@ -499,10 +657,23 @@ export abstract class CacheManager implements ICacheManager { return; } - if ( - !!accountFilter.name && - !this.matchName(entity, accountFilter.name) - ) { + // 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; } @@ -788,7 +959,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; } @@ -826,6 +997,88 @@ export abstract class CacheManager implements ICacheManager { await Promise.all(removedCredentials); } + /** + * 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 | 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 = CacheManager.toObject(new AccountEntity(), { + ...baseAccount, + }); + + 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); + logger?.verbose("Updated an outdated account entity in the cache"); + return updatedAccount; + } + + // No update is necessary + return accountEntity; + } + /** * returns a boolean if the given credential is removed * @param credential @@ -890,11 +1143,15 @@ export abstract class CacheManager implements ICacheManager { performanceClient?: IPerformanceClient, correlationId?: string ): CacheRecord { + // 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( account, tokenKeys, + requestTenantId, performanceClient, correlationId ); @@ -902,6 +1159,7 @@ export abstract class CacheManager implements ICacheManager { account, request, tokenKeys, + requestTenantId, performanceClient, correlationId ); @@ -914,13 +1172,6 @@ export abstract class CacheManager implements ICacheManager { ); const cachedAppMetadata = this.readAppMetadataFromCache(environment); - if (cachedAccount && cachedIdToken) { - cachedAccount.idTokenClaims = extractTokenClaims( - cachedIdToken.secret, - this.cryptoImpl.base64Decode - ); - } - return { account: cachedAccount, idToken: cachedIdToken, @@ -937,19 +1188,21 @@ 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); } /** * Retrieve IdTokenEntity from cache * @param account {AccountInfo} * @param tokenKeys {?TokenKeys} + * @param targetRealm {?string} * @param performanceClient {?IPerformanceClient} * @param correlationId {?string} */ getIdToken( account: AccountInfo, tokenKeys?: TokenKeys, + targetRealm?: string, performanceClient?: IPerformanceClient, correlationId?: string ): IdTokenEntity | null { @@ -959,36 +1212,66 @@ export abstract class CacheManager implements ICacheManager { environment: account.environment, credentialType: CredentialType.ID_TOKEN, clientId: this.clientId, - realm: account.tenantId, + 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 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 for account but none match account entity tenant id, returning first result" + ); + return idTokenMap.values().next().value; + } else if (numHomeIdTokens === 1) { + this.commonLogger.info( + "CacheManager:getIdToken - Multiple ID tokens found for account, defaulting to home tenant profile" + ); + 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(generateCredentialKey(idToken)); + tokensToBeRemoved.forEach((idToken, key) => { + this.removeIdToken(key); }); if (performanceClient && correlationId) { performanceClient.addFields( - { multiMatchedID: idTokens.length }, + { multiMatchedID: idTokenMap.size }, correlationId ); } 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; } /** @@ -999,11 +1282,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, { @@ -1015,7 +1301,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); } }); @@ -1078,6 +1364,7 @@ export abstract class CacheManager implements ICacheManager { account: AccountInfo, request: BaseAuthRequest, tokenKeys?: TokenKeys, + targetRealm?: string, performanceClient?: IPerformanceClient, correlationId?: string ): AccessTokenEntity | null { @@ -1101,7 +1388,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, @@ -1407,27 +1694,19 @@ export abstract class CacheManager implements ICacheManager { * @param localAccountId * @returns */ - private matchLocalAccountId( - entity: AccountEntity, + private matchLocalAccountIdFromTokenClaims( + tokenClaims: TokenClaims, localAccountId: string ): boolean { - return !!( - typeof entity.localAccountId === "string" && - localAccountId === entity.localAccountId - ); + const idTokenLocalAccountId = tokenClaims.oid || tokenClaims.sub; + return localAccountId === idTokenLocalAccountId; } - /** - * helper to match usernames - * @param entity - * @param username - * @returns - */ - private matchUsername(entity: AccountEntity, username: string): boolean { - return !!( - typeof entity.username === "string" && - username.toLowerCase() === entity.username.toLowerCase() - ); + private matchLocalAccountIdFromTenantProfile( + tenantProfile: TenantProfile, + localAccountId: string + ): boolean { + return tenantProfile.localAccountId === localAccountId; } /** @@ -1436,8 +1715,25 @@ export abstract class CacheManager implements ICacheManager { * @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()); + private matchName(claims: TokenClaims, name: string): boolean { + return !!(name.toLowerCase() === claims.name?.toLowerCase()); + } + + /** + * helper to match usernames + * @param entity + * @param username + * @returns + */ + private matchUsername( + cachedUsername?: string, + filterUsername?: string + ): boolean { + return !!( + cachedUsername && + typeof cachedUsername === "string" && + filterUsername?.toLowerCase() === cachedUsername.toLowerCase() + ); } /** @@ -1537,7 +1833,7 @@ export abstract class CacheManager implements ICacheManager { entity: AccountEntity | CredentialEntity, realm: string ): boolean { - return !!(entity.realm && realm === entity.realm); + return !!(entity.realm?.toLowerCase() === realm.toLowerCase()); } /** @@ -1564,19 +1860,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; } @@ -1585,12 +1881,12 @@ 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 */ private matchSid(idTokenClaims: TokenClaims, sid: string): boolean { - return !!(idTokenClaims?.sid && idTokenClaims.sid === sid); + return idTokenClaims.sid === sid; } private matchAuthorityType( @@ -1688,6 +1984,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); } @@ -1754,4 +2053,7 @@ export class DefaultStorageClass extends CacheManager { updateCredentialCacheKey(): string { throw createClientAuthError(ClientAuthErrorCodes.methodNotImplemented); } + removeOutdatedAccount(): void { + throw createClientAuthError(ClientAuthErrorCodes.methodNotImplemented); + } } diff --git a/lib/msal-common/src/cache/entities/AccountEntity.ts b/lib/msal-common/src/cache/entities/AccountEntity.ts index b050563768..920cfc7ea3 100644 --- a/lib/msal-common/src/cache/entities/AccountEntity.ts +++ b/lib/msal-common/src/cache/entities/AccountEntity.ts @@ -3,18 +3,25 @@ * Licensed under the MIT License. */ -import { Separators, CacheAccountType, Constants } from "../../utils/Constants"; +import { CacheAccountType, Separators } from "../../utils/Constants"; import { Authority } from "../../authority/Authority"; import { ICrypto } from "../../crypto/ICrypto"; -import { buildClientInfo } from "../../account/ClientInfo"; -import { AccountInfo } from "../../account/AccountInfo"; +import { ClientInfo, buildClientInfo } from "../../account/ClientInfo"; +import { + AccountInfo, + TenantProfile, + buildTenantProfileFromIdTokenClaims, +} from "../../account/AccountInfo"; import { createClientAuthError, ClientAuthErrorCodes, } 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"; /** @@ -35,8 +42,8 @@ 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 + * tenantProfiles: Array of tenant profile objects for each tenant that the account has authenticated with in the browser * } * @internal */ @@ -53,8 +60,8 @@ export class AccountEntity { lastModificationApp?: string; cloudGraphHostName?: string; msGraphHost?: string; - idTokenClaims?: TokenClaims; nativeAccountId?: string; + tenantProfiles?: Array; /** * Generate Account Id key component as per the schema: - @@ -88,21 +95,34 @@ export class AccountEntity { username: this.username, localAccountId: this.localAccountId, name: this.name, - idTokenClaims: this.idTokenClaims, nativeAccountId: this.nativeAccountId, authorityType: this.authorityType, + // Deserialize tenant profiles array into a Map + tenantProfiles: new Map( + (this.tenantProfiles || []).map((tenantProfile) => { + return [tenantProfile.tenantId, tenantProfile]; + }) + ), }; } + /** + * Returns true if the account entity is in single tenant format (outdated), false otherwise + */ + isSingleTenant(): boolean { + return !this.tenantProfiles; + } + /** * 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, + accountInterface.environment || "", + homeTenantId || accountInterface.tenantId || "", ]; return accountKey.join(Separators.CACHE_KEY_SEPARATOR).toLowerCase(); @@ -121,8 +141,10 @@ export class AccountEntity { msGraphHost?: string; environment?: string; nativeAccountId?: string; + tenantProfiles?: Array; }, - authority: Authority + authority: Authority, + base64Decode?: (input: string) => string ): AccountEntity { const account: AccountEntity = new AccountEntity(); @@ -134,6 +156,15 @@ export class AccountEntity { account.authorityType = CacheAccountType.GENERIC_ACCOUNT_TYPE; } + let clientInfo: ClientInfo | undefined; + + if (accountDetails.clientInfo && base64Decode) { + clientInfo = buildClientInfo( + accountDetails.clientInfo, + base64Decode + ); + } + account.clientInfo = accountDetails.clientInfo; account.homeAccountId = accountDetails.homeAccountId; account.nativeAccountId = accountDetails.nativeAccountId; @@ -151,13 +182,16 @@ export class AccountEntity { account.environment = env; // non AAD scenarios can have empty realm account.realm = - accountDetails.idTokenClaims.tid || Constants.EMPTY_STRING; + clientInfo?.utid || + getTenantIdFromIdTokenClaims(accountDetails.idTokenClaims) || + ""; // How do you account for MSA CID here? account.localAccountId = + 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. @@ -171,12 +205,26 @@ 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; 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; } @@ -208,6 +256,10 @@ export class AccountEntity { account.cloudGraphHostName = cloudGraphHostName; account.msGraphHost = msGraphHost; + // Serialize tenant profiles map into an array + account.tenantProfiles = Array.from( + accountInfo.tenantProfiles?.values() || [] + ); return account; } @@ -224,31 +276,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.base64Decode + ); + if (clientInfo.uid && clientInfo.utid) { + return `${clientInfo.uid}.${clientInfo.utid}`; + } + } catch (e) {} + } + logger.warning("No client info in response"); } // default to "sub" claim - logger.verbose("No client info in response"); - return accountId; + return idTokenClaims?.sub || ""; } /** diff --git a/lib/msal-common/src/cache/utils/CacheTypes.ts b/lib/msal-common/src/cache/utils/CacheTypes.ts index c09b01dea8..f181ef4310 100644 --- a/lib/msal-common/src/cache/utils/CacheTypes.ts +++ b/lib/msal-common/src/cache/utils/CacheTypes.ts @@ -60,8 +60,19 @@ export type AccountFilter = Omit< realm?: string; loginHint?: string; sid?: string; + isHomeTenant?: boolean; }; +export type TenantProfileFilter = Pick< + AccountFilter, + | "localAccountId" + | "loginHint" + | "name" + | "sid" + | "isHomeTenant" + | "username" +>; + /** * Credential: ------ */ diff --git a/lib/msal-common/src/client/AuthorizationCodeClient.ts b/lib/msal-common/src/client/AuthorizationCodeClient.ts index a4999989a6..f41aa13bbe 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/index.ts b/lib/msal-common/src/index.ts index b40cc50b07..59c99b5ca1 100644 --- a/lib/msal-common/src/index.ts +++ b/lib/msal-common/src/index.ts @@ -28,9 +28,19 @@ export { } from "./config/AppTokenProvider"; export { ClientConfiguration } from "./config/ClientConfiguration"; // Account -export { AccountInfo, ActiveAccountFilters } from "./account/AccountInfo"; +export { + AccountInfo, + ActiveAccountFilters, + TenantProfile, + updateAccountTenantProfileData, + tenantIdMatchesHomeTenant, + buildTenantProfileFromIdTokenClaims, +} 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 { @@ -136,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 9aec0251b0..92de68b6be 100644 --- a/lib/msal-common/src/response/ResponseHandler.ts +++ b/lib/msal-common/src/response/ResponseHandler.ts @@ -42,8 +42,15 @@ 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 { AccountInfo } from "../account/AccountInfo"; +import { + TokenClaims, + getTenantIdFromIdTokenClaims, +} from "../account/TokenClaims"; +import { + AccountInfo, + buildTenantProfileFromIdTokenClaims, + updateAccountTenantProfileData, +} from "../account/AccountInfo"; import * as CacheHelpers from "../cache/utils/CacheHelpers"; /** @@ -337,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" @@ -371,6 +378,7 @@ export class ResponseHandler { await this.persistencePlugin.afterCacheAccess(cacheContext); } } + return ResponseHandler.generateAuthenticationResult( this.cryptoObj, authority, @@ -406,6 +414,8 @@ export class ResponseHandler { ); } + const claimsTenantId = getTenantIdFromIdTokenClaims(idTokenClaims); + // IdToken: non AAD scenarios can have empty realm let cachedIdToken: IdTokenEntity | undefined; let cachedAccount: AccountEntity | undefined; @@ -415,18 +425,20 @@ export class ResponseHandler { env, serverTokenResponse.id_token, this.clientId, - idTokenClaims.tid || "" + claimsTenantId || "" ); - cachedAccount = AccountEntity.createAccount( - { - homeAccountId: this.homeAccountIdentifier, - idTokenClaims: idTokenClaims, - clientInfo: serverTokenResponse.client_info, - cloudGraphHostName: authCodePayload?.cloud_graph_host_name, - msGraphHost: authCodePayload?.msgraph_host, - }, - authority + cachedAccount = buildAccountToCache( + this.cacheStorage, + authority, + this.homeAccountIdentifier, + idTokenClaims, + this.cryptoObj.base64Decode, + serverTokenResponse.client_info, + claimsTenantId, + authCodePayload, + undefined, + this.logger ); } @@ -468,7 +480,7 @@ export class ResponseHandler { env, serverTokenResponse.access_token, this.clientId, - idTokenClaims?.tid || authority.tenant, + claimsTenantId || authority.tenant, responseScopes.printScopes(), tokenExpirationSeconds, extendedTokenExpirationSeconds, @@ -596,10 +608,11 @@ export class ResponseHandler { } const accountInfo: AccountInfo | null = cacheRecord.account - ? { - ...cacheRecord.account.getAccountInfo(), - idTokenClaims, - } + ? updateAccountTenantProfileData( + cacheRecord.account.getAccountInfo(), + undefined, // tenantProfile optional + idTokenClaims + ) : null; return { @@ -633,3 +646,62 @@ export class ResponseHandler { }; } } + +export function buildAccountToCache( + cacheStorage: CacheManager, + authority: Authority, + homeAccountId: string, + idTokenClaims: TokenClaims, + base64Decode: (input: string) => string, + 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, + base64Decode + ); + + 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; +} 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..51d853d07a --- /dev/null +++ b/lib/msal-common/test/account/AccountInfo.spec.ts @@ -0,0 +1,314 @@ +import { AccountEntity } from "../../src"; +import * as AccountInfo from "../../src/account/AccountInfo"; +import { buildAccountFromIdTokenClaims } from "msal-test-utils"; +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 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); + expect(tenantProfile.tenantId).not.toEqual(idTokenClaims.acr); + expect(tenantProfile.tenantId).not.toEqual(""); + }); + + it("from the acr claim 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); + 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/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); 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..88b82abc2e --- /dev/null +++ b/lib/msal-common/test/account/TokenClaims.spec.ts @@ -0,0 +1,23 @@ +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"); + }); +}); 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/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 69d55af2c5..1874e6dec5 100644 --- a/lib/msal-common/test/cache/CacheManager.spec.ts +++ b/lib/msal-common/test/cache/CacheManager.spec.ts @@ -22,6 +22,8 @@ import { TEST_CRYPTO_VALUES, TEST_ACCOUNT_INFO, TEST_TOKEN_LIFETIMES, + ID_TOKEN_ALT_CLAIMS, + GUEST_ID_TOKEN_CLAIMS, } from "../test_kit/StringConstants"; import { ClientAuthErrorCodes, @@ -29,6 +31,7 @@ import { } from "../../src/error/ClientAuthError"; import { AccountInfo } from "../../src/account/AccountInfo"; import { MockCache } from "./MockCache"; +import { buildAccountFromIdTokenClaims, buildIdToken } from "msal-test-utils"; import { mockCrypto } from "../client/ClientTestUtils"; import { TestError } from "../test_kit/TestErrors"; import { CacheManager } from "../../src/cache/CacheManager"; @@ -258,6 +261,7 @@ describe("CacheManager.ts test cases", () => { mockCache.cacheManager.getIdToken( TEST_ACCOUNT_INFO, undefined, + TEST_ACCOUNT_INFO.tenantId, mockPerfClient, correlationId ) @@ -293,12 +297,363 @@ describe("CacheManager.ts test cases", () => { }); }); - it("getAccounts (gets all AccountInfo objects)", async () => { - const accounts = mockCache.cacheManager.getAllAccounts(); + describe("getAllAccounts", () => { + const account1 = + 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(); + + 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 successFilter: AccountFilter = { + loginHint: ID_TOKEN_CLAIMS.login_hint, + }; + + 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 successFilter: AccountFilter = { + loginHint: ID_TOKEN_CLAIMS.preferred_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 successFilter: AccountFilter = { + loginHint: ID_TOKEN_CLAIMS.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); + }); + }); + + 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(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); + }); - expect(accounts).not.toBeNull(); - expect(accounts[0].idToken).toEqual(TEST_TOKENS.IDTOKEN_V2); - expect(accounts[0].idTokenClaims).toEqual(ID_TOKEN_CLAIMS); + 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); + }); + }); + }); + + describe("getAccountInfoFilteredBy", () => { + const multiTenantAccount = buildAccountFromIdTokenClaims( + ID_TOKEN_CLAIMS, + [GUEST_ID_TOKEN_CLAIMS] + ).getAccountInfo(); + it("returns null if no accounts match filter", () => { + expect( + mockCache.cacheManager.getAccountInfoFilteredBy({ + homeAccountId: "inexistent-account-id", + }) + ).toBeNull(); + }); + + 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("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); + + const filter = { + homeAccountId: multiTenantAccount.homeAccountId, + }; + // Remove main ID token + mockCache.cacheManager.removeIdToken(mainIdTokenKey); + const resultAccount = + 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", () => { + const resultAccount = + mockCache.cacheManager.getAccountInfoFilteredBy({ + homeAccountId: multiTenantAccount.homeAccountId, + tenantId: multiTenantAccount.tenantId, + isHomeTenant: true, + }); + expect(resultAccount).not.toBeNull(); + expect(resultAccount).toMatchObject(multiTenantAccount); + }); + }); + + 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 () => { @@ -379,11 +734,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" }; @@ -395,11 +755,12 @@ 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); - expect(Object.keys(accounts).length).toEqual(3); + // Both cached accounts have environments that are aliases of eachother, expect both to match + expect(Object.keys(accounts).length).toEqual(2); sinon.restore(); const wrongFilter: AccountFilter = { environment: "Wrong Env" }; @@ -410,10 +771,12 @@ describe("CacheManager.ts test cases", () => { it("realm filter", () => { // filter by realm - const successFilter: AccountFilter = { realm: "microsoft" }; + const successFilter: AccountFilter = { + realm: matchAccountEntity.realm, + }; 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 = @@ -1167,37 +1530,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-microsoft" - ) + mockCache.cacheManager.getAccount(accountToRemoveKey) ).not.toBeNull(); - await mockCache.cacheManager.removeAccount( - "uid.utid-login.microsoftonline.com-microsoft" - ); + await mockCache.cacheManager.removeAccount(accountToRemoveKey); expect( - mockCache.cacheManager.getAccount( - "uid.utid-login.microsoftonline.com-microsoft" - ) + mockCache.cacheManager.getAccount(accountToRemoveKey) ).toBeNull(); }); @@ -1404,6 +1753,7 @@ describe("CacheManager.ts test cases", () => { mockedAccountInfo, silentFlowRequest, undefined, + undefined, mockPerfClient, correlationId ) @@ -1761,17 +2111,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", () => { @@ -1791,15 +2141,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 455ec18c27..0e44b2de36 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, @@ -16,7 +15,14 @@ import { AuthenticationScheme, } from "../../src"; import { MockStorageClass } from "../client/ClientTestUtils"; -import { TEST_TOKENS, TEST_CRYPTO_VALUES } from "../test_kit/StringConstants"; +import { + TEST_TOKENS, + TEST_CRYPTO_VALUES, + ID_TOKEN_CLAIMS, + ID_TOKEN_ALT_CLAIMS, + GUEST_ID_TOKEN_CLAIMS, +} from "../test_kit/StringConstants"; +import { buildAccountFromIdTokenClaims, buildIdToken } from "msal-test-utils"; export class MockCache { cacheManager: MockStorageClass; @@ -51,84 +57,38 @@ export class MockCache { // create account entries in the cache createAccountEntries(): void { - const accountData = { - username: "John Doe", - localAccountId: "object1234", - realm: "microsoft", - 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: "John Doe", - localAccountId: "object1234", - realm: "microsoft", - environment: "login.microsoftonline.com", - homeAccountId: "uid.utid", - authorityType: "MSSTS", - clientInfo: "eyJ1aWQiOiJ1aWQiLCAidXRpZCI6InV0aWQifQ==", - nativeAccountId: "mocked_native_account_id", - }; - const accountWithNativeAccountId = CacheManager.toObject( - new AccountEntity(), - 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 accountWithNativeAccountId = + buildAccountFromIdTokenClaims(ID_TOKEN_ALT_CLAIMS); + accountWithNativeAccountId.nativeAccountId = "mocked_native_account_id"; - 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); + this.cacheManager.setAccount(accountWithNativeAccountId); } // create id token entries in the cache createIdTokenEntries(): void { - const idToken = { - realm: "microsoft", - 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 = 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 diff --git a/lib/msal-common/test/cache/entities/AccountEntity.spec.ts b/lib/msal-common/test/cache/entities/AccountEntity.spec.ts index 2c61fe56b4..ec87226ad8 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 "msal-test-utils"; const cryptoInterface: ICrypto = { createNewGuid(): string { @@ -129,7 +132,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" ); }); @@ -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; diff --git a/lib/msal-common/test/client/AuthorizationCodeClient.spec.ts b/lib/msal-common/test/client/AuthorizationCodeClient.spec.ts index 2cb8ba356a..af4bb2e8d9 100644 --- a/lib/msal-common/test/client/AuthorizationCodeClient.spec.ts +++ b/lib/msal-common/test/client/AuthorizationCodeClient.spec.ts @@ -745,6 +745,8 @@ describe("AuthorizationCodeClient unit tests", () => { | "amr" | "idp" | "auth_time" + | "tfp" + | "acr" > > = { ver: "2.0", @@ -825,6 +827,8 @@ describe("AuthorizationCodeClient unit tests", () => { | "amr" | "idp" | "auth_time" + | "tfp" + | "acr" > > = { ver: "2.0", diff --git a/lib/msal-common/test/client/ClientTestUtils.ts b/lib/msal-common/test/client/ClientTestUtils.ts index a5ac01ff15..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(); @@ -71,6 +74,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-common/test/client/RefreshTokenClient.spec.ts b/lib/msal-common/test/client/RefreshTokenClient.spec.ts index bb0422d245..bbb5ae6e92 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 "msal-test-utils"; const testAccountEntity: AccountEntity = new AccountEntity(); testAccountEntity.homeAccountId = `${TEST_DATA_CLIENT_INFO.TEST_UID}.${TEST_DATA_CLIENT_INFO.TEST_UTID}`; @@ -299,17 +300,9 @@ describe("RefreshTokenClient unit tests", () => { let config: ClientConfiguration; let client: RefreshTokenClient; - const testAccount: AccountInfo = { - authorityType: "MSSTS", - homeAccountId: ID_TOKEN_CLAIMS.sub, - 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, - }; + const testAccount: AccountInfo = + buildAccountFromIdTokenClaims(ID_TOKEN_CLAIMS).getAccountInfo(); + testAccount.idTokenClaims = ID_TOKEN_CLAIMS; beforeEach(async () => { sinon @@ -322,7 +315,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); @@ -425,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 ); @@ -1055,17 +1048,9 @@ describe("RefreshTokenClient unit tests", () => { let config: ClientConfiguration; let client: RefreshTokenClient; - const testAccount: AccountInfo = { - authorityType: "MSSTS", - homeAccountId: ID_TOKEN_CLAIMS.sub, - 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, - }; + const testAccount: AccountInfo = + buildAccountFromIdTokenClaims(ID_TOKEN_CLAIMS).getAccountInfo(); + testAccount.idTokenClaims = ID_TOKEN_CLAIMS; beforeEach(async () => { sinon @@ -1078,7 +1063,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 8368d0da15..d2c8827783 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 "msal-test-utils"; -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(); }); @@ -477,12 +465,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); @@ -725,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 @@ -745,7 +716,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, @@ -790,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); @@ -869,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); @@ -963,7 +926,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 df5f7f99c1..d7bbf94359 100644 --- a/lib/msal-common/test/test_kit/StringConstants.ts +++ b/lib/msal-common/test/test_kit/StringConstants.ts @@ -18,7 +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: + "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: @@ -51,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, @@ -59,12 +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, @@ -74,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"}', @@ -150,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: @@ -469,8 +509,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}`, }, }; @@ -533,8 +572,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", }, @@ -680,15 +718,15 @@ export const CACHE_MOCKS = { homeAccountId: "uid.utid", localAccountId: "uid", environment: "login.microsoftonline.com", - tenantId: "microsoft", - username: "mocked_username", + tenantId: ID_TOKEN_CLAIMS.tid, + 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: ID_TOKEN_ALT_CLAIMS.tid, + username: "Jane Doe", nativeAccountId: "mocked_native_account_id", }, }; diff --git a/lib/msal-node/docs/accounts.md b/lib/msal-node/docs/accounts.md index 3059b716c6..e75880a82f 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: @@ -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 @@ -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. diff --git a/lib/msal-node/src/cache/NodeStorage.ts b/lib/msal-node/src/cache/NodeStorage.ts index 2804c75a50..93ab126567 100644 --- a/lib/msal-node/src/cache/NodeStorage.ts +++ b/lib/msal-node/src/cache/NodeStorage.ts @@ -218,13 +218,25 @@ 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 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 { + const cachedAccount = this.getItem(accountKey); + return cachedAccount + ? Object.assign(new AccountEntity(), this.getItem(accountKey)) + : null; + } + /** * set account entity * @param account - cache value to be set of type AccountEntity @@ -456,6 +468,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/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 023c25f47e..71048d1874 100644 --- a/lib/msal-node/src/cache/serializer/Deserializer.ts +++ b/lib/msal-node/src/cache/serializer/Deserializer.ts @@ -63,6 +63,11 @@ export class Deserializer { clientInfo: serializedAcc.client_info, lastModificationTime: serializedAcc.last_modification_time, lastModificationApp: serializedAcc.last_modification_app, + tenantProfiles: serializedAcc.tenantProfiles?.map( + (serializedTenantProfile) => { + return JSON.parse(serializedTenantProfile); + } + ), }; 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..908cb50ad4 100644 --- a/lib/msal-node/src/cache/serializer/Serializer.ts +++ b/lib/msal-node/src/cache/serializer/Serializer.ts @@ -50,6 +50,11 @@ export class Serializer { client_info: accountEntity.clientInfo, last_modification_time: accountEntity.lastModificationTime, last_modification_app: accountEntity.lastModificationApp, + 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 aec8abe13a..1ae0afdabe 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; + tenantProfiles?: string[]; }; /** diff --git a/lib/msal-node/src/client/OnBehalfOfClient.ts b/lib/msal-node/src/client/OnBehalfOfClient.ts index 227a79f126..3fcc427955 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; } /** diff --git a/lib/msal-node/test/cache/Storage.spec.ts b/lib/msal-node/test/cache/Storage.spec.ts index d7b21e63f9..d9fc75a9aa 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,24 +142,31 @@ 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 = { username: "Jane Doe", - localAccountId: "object5678", + localAccountId: "uid", realm: "samplerealm", environment: "login.windows.net", homeAccountId: "uid1.utid1", authorityType: "MSSTS", clientInfo: "eyJ1aWQiOiJ1aWQxIiwgInV0aWQiOiJ1dGlkMSJ9", + tenantProfiles: [ + { + tenantId: "utid1", + localAccountId: "uid", + name: "Jane Doe", + isHomeTenant: true, + }, + ], }; let mockAccountEntity = CacheManager.toObject( @@ -173,6 +180,59 @@ 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: "janedoe@microsoft.com", + name: "Jane Doe", + localAccountId: "uid", + 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, + tenantProfiles: [ + { + tenantId: "utid3", + localAccountId: "uid", + name: "Jane Doe", + isHomeTenant: true, + }, + ], + } + ); + 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, @@ -309,8 +369,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", () => { @@ -320,9 +379,7 @@ describe("Storage tests for msal-node: ", () => { DEFAULT_CRYPTO_IMPLEMENTATION ); nodeStorage.setInMemoryCache(inMemoryCache); - - const accountKey = "uid.utid-login.microsoftonline.com-microsoft"; - expect(nodeStorage.getKeys().includes(accountKey)).toBeTruthy; + expect(nodeStorage.getKeys().includes(ACCOUNT_KEY)).toBeTruthy(); }); it("removeItem() tests - removes an account", () => { @@ -333,14 +390,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", () => { @@ -351,11 +407,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) => { 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..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 @@ -5,17 +5,20 @@ } }, "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==", + "tenantProfiles": [ + "{\"tenantId\":\"utid\",\"localAccountId\":\"object1234\",\"isHomeTenant\":true}" + ] } }, "RefreshToken": { @@ -36,11 +39,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 +51,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 +65,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 3c5827efe2..19b6bd9b07 100644 --- a/lib/msal-node/test/cache/cacheConstants.ts +++ b/lib/msal-node/test/cache/cacheConstants.ts @@ -17,7 +17,7 @@ export const mockAccessTokenEntity_1: AccessTokenEntity = { credentialType: "AccessToken", clientId: "mock_client_id", secret: "an access token", - realm: "microsoft", + realm: "utid", target: "scope1 scope2 scope3", cachedAt: "1000", expiresOn: "4600", @@ -31,7 +31,7 @@ export const mockAccessTokenEntity_2: AccessTokenEntity = { credentialType: "AccessToken", clientId: "mock_client_id", secret: "an access token", - realm: "microsoft", + realm: "utid", target: "scope4 scope5", cachedAt: "1000", expiresOn: "4600", @@ -44,7 +44,7 @@ export const mockIdTokenEntity: IdTokenEntity = { credentialType: "IdToken", clientId: "mock_client_id", secret: "header.eyJvaWQiOiAib2JqZWN0MTIzNCIsICJwcmVmZXJyZWRfdXNlcm5hbWUiOiAiSm9obiBEb2UiLCAic3ViIjogInN1YiJ9.signature", - realm: "microsoft", + realm: "utid", }; export const mockRefreshTokenEntity: RefreshTokenEntity = { @@ -67,11 +67,19 @@ export const mockRefreshTokenEntityWithFamilyId: RefreshTokenEntity = { export const mockAccountEntity = { homeAccountId: "uid.utid", environment: "login.microsoftonline.com", - realm: "microsoft", - localAccountId: "object1234", - username: "John Doe", + realm: "utid", + localAccountId: "uid", + username: "johndoe@microsoft.com", authorityType: "MSSTS", clientInfo: "eyJ1aWQiOiJ1aWQiLCAidXRpZCI6InV0aWQifQ==", + 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 66feb0c366..0802b87e72 100644 --- a/lib/msal-node/test/cache/serializer/Deserializer.spec.ts +++ b/lib/msal-node/test/cache/serializer/Deserializer.spec.ts @@ -13,14 +13,22 @@ describe("Deserializer test cases", () => { test("deserializeJSONBlob", () => { const mockAccount = { - "uid.utid-login.microsoftonline.com-microsoft": { - username: "John Doe", - local_account_id: "object1234", - realm: "microsoft", + "uid.utid-login.microsoftonline.com-utid": { + 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==", + 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 b9f1e2475f..2e8b2a11ea 100644 --- a/lib/msal-node/test/cache/serializer/cache.json +++ b/lib/msal-node/test/cache/serializer/cache.json @@ -1,13 +1,16 @@ { "Account": { - "uid.utid-login.microsoftonline.com-microsoft": { - "username": "John Doe", - "local_account_id": "object1234", - "realm": "microsoft", + "uid.utid-login.microsoftonline.com-utid": { + "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==" + "client_info": "eyJ1aWQiOiJ1aWQiLCAidXRpZCI6InV0aWQifQ==", + "tenantProfiles": [ + "{\"tenantId\":\"utid\",\"localAccountId\":\"uid\",\"name\":\"John Doe\",\"isHomeTenant\":true}" + ] } }, "RefreshToken": { @@ -28,11 +31,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 +44,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 +56,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 +72,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", diff --git a/lib/msal-node/test/client/ClientTestUtils.ts b/lib/msal-node/test/client/ClientTestUtils.ts index e3618e0289..24c056468e 100644 --- a/lib/msal-node/test/client/ClientTestUtils.ts +++ b/lib/msal-node/test/client/ClientTestUtils.ts @@ -36,6 +36,7 @@ import { RANDOM_TEST_GUID, TEST_CONFIG, TEST_CRYPTO_VALUES, + TEST_DATA_CLIENT_INFO, TEST_POP_VALUES, TEST_TOKENS, } from "../test_kit/StringConstants"; @@ -55,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; @@ -76,6 +81,10 @@ export class MockStorageClass extends CacheManager { } } + removeOutdatedAccount(accountKey: string): void { + this.removeAccount(accountKey); + } + getAccountKeys(): string[] { return this.store[ACCOUNT_KEYS] || []; } @@ -217,6 +226,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; } diff --git a/lib/msal-node/test/client/PublicClientApplication.spec.ts b/lib/msal-node/test/client/PublicClientApplication.spec.ts index 1765b0c304..db465b3567 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..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 = { @@ -230,9 +231,19 @@ 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, + 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 = { diff --git a/package-lock.json b/package-lock.json index 37010b3244..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/*", @@ -194,6 +195,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", @@ -239,6 +241,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", @@ -38786,6 +38789,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 @@ -74653,6 +74660,15 @@ "eslint-plugin-react-hooks": "^4.1.2", "eslint-plugin-security": "^1.4.0" } + }, + "shared-test-utils": { + "name": "msal-test-utils", + "version": "0.0.1", + "license": "MIT", + "devDependencies": { + "@azure/msal-common": "^14.0.0", + "typescript": "^4.9.5" + } } } } 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/*", diff --git a/samples/e2eTestUtils/src/BrowserCacheTestUtils.ts b/samples/e2eTestUtils/src/BrowserCacheTestUtils.ts index 679474fe5b..910c623fcc 100644 --- a/samples/e2eTestUtils/src/BrowserCacheTestUtils.ts +++ b/samples/e2eTestUtils/src/BrowserCacheTestUtils.ts @@ -112,18 +112,26 @@ 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 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; } async popAccessTokenForScopesExists( @@ -174,7 +182,7 @@ export class BrowserCacheUtils { "-" + tokenVal.environment + "-" + - tokenVal.realm; + tokenVal.homeAccountId.split(".")[1]; if (Object.keys(storage).includes(accountKey)) { return JSON.parse(storage[accountKey]); @@ -198,4 +206,48 @@ 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(); + 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 is null"); + } + expect( + await this.accessTokenForScopesExists( + tokenStore.accessTokens, + scopes, + totalAccessTokens + ) + ).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/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/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/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/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/e2eTestUtils/src/TestUtils.ts b/samples/e2eTestUtils/src/TestUtils.ts index 9878c74209..bf9c351259 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; @@ -134,20 +138,30 @@ export async function setupCredentials( let username = ""; let accountPwd = ""; - if (labConfig.user.upn) { - username = labConfig.user.upn; + const { user, lab } = labConfig; + + if (user.userType === "Guest") { + 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 || ""; } - if (!labConfig.lab.labName) { + if (!lab.labName) { 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]; @@ -208,11 +222,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) => { @@ -237,9 +247,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(() => {}); @@ -260,9 +268,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), @@ -294,9 +300,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(() => {}); @@ -311,9 +315,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(() => {}); @@ -333,9 +335,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(() => {}); @@ -398,11 +398,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/app/customizable-e2e-test/auth.js b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/auth.js index a1ee6b6583..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 @@ -1,18 +1,16 @@ -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 => { @@ -27,39 +25,48 @@ 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); } } else { - // need to call getAccount here? - const currentAccounts = myMSALObj.getAllAccounts(); - 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); } } 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.getActiveAccount() }; if (signOutType === "popup") { @@ -70,8 +77,8 @@ function signOut(signOutType) { } async function getTokenPopup() { - const request = authConfig.request; - const currentAcc = myMSALObj.getAccountByHomeId(homeAccountId); + const request = requestConfig; + const currentAcc = myMSALObj.getActiveAccount(); if (currentAcc) { request.account = currentAcc; response = await myMSALObj.acquireTokenPopup(request).then(handleResponse).catch(error => { @@ -81,8 +88,8 @@ async function getTokenPopup() { } async function getTokenRedirect() { - const request = authConfig.request; - const currentAcc = myMSALObj.getAccountByHomeId(homeAccountId); + const request = requestConfig; + const currentAcc = myMSALObj.getActiveAccount(); if (currentAcc) { request.account = currentAcc; myMSALObj.acquireTokenRedirect(request); @@ -90,8 +97,8 @@ async function getTokenRedirect() { } async function getTokenSilently() { - const request = authConfig.request; - const currentAcc = myMSALObj.getAccountByHomeId(homeAccountId); + const request = requestConfig; + const currentAcc = myMSALObj.getActiveAccount(); if (currentAcc) { request.account = currentAcc; response = await myMSALObj.acquireTokenSilent(request).then(handleResponse).catch(error => { @@ -99,3 +106,28 @@ async function getTokenSilently() { }); } } + +async function getGuestTokenSilently() { + const request = requestConfig; + 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 currentAcc = myMSALObj.getActiveAccount(); + response = await myMSALObj.acquireTokenSilent({ + ...request, + account: currentAcc, + authority: tenantConfig.guest.authority, + cacheLookupPolicy: msal.CacheLookupPolicy.RefreshToken + }).then(handleResponse).catch(error => { + console.error(error); + }); + } + } else { + console.error("Sample Configuration 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/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 new file mode 100644 index 0000000000..4cc4e01394 --- /dev/null +++ b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/test/browserAADMultiTenant.spec.ts @@ -0,0 +1,336 @@ +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/multiTenantTests`; +let sampleHomeUrl = ""; + +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] = 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 BrowserCache.verifyTokenStore({ + scopes: 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 BrowserCache.verifyTokenStore({ + scopes: 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 BrowserCache.verifyTokenStore({ + scopes: 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 BrowserCache.verifyTokenStore({ + scopes: aadTokenRequest.scopes, + numberOfTenants: 2, + }); + }); + + 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 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 b1470b1332..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,27 +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(); - expect(Object.keys(storage).length).toEqual(6); -} - describe("B2C Tests", () => { let browser: puppeteer.Browser; let context: puppeteer.BrowserContext; @@ -133,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 () => { @@ -159,7 +140,9 @@ describe("B2C Tests", () => { popupWindowClosed ); - await verifyTokenStore(BrowserCache, b2cTokenRequest.scopes); + await BrowserCache.verifyTokenStore({ + scopes: b2cTokenRequest.scopes, + }); }); }); @@ -231,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 () => { @@ -249,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 () => { @@ -276,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 () => { @@ -294,7 +283,9 @@ describe("B2C Tests", () => { ); // Verify we now have an access_token - await verifyTokenStore(BrowserCache, b2cTokenRequest.scopes); + await BrowserCache.verifyTokenStore({ + scopes: b2cTokenRequest.scopes, + }); }); }); }); @@ -355,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 () => { @@ -381,7 +374,9 @@ describe("B2C Tests", () => { popupWindowClosed ); - await verifyTokenStore(BrowserCache, b2cTokenRequest.scopes); + await BrowserCache.verifyTokenStore({ + scopes: b2cTokenRequest.scopes, + }); }); }); @@ -453,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 () => { @@ -471,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 () => { @@ -498,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 () => { @@ -516,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 () => { 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", 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) { 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 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/src/CredentialGenerators.ts b/shared-test-utils/src/CredentialGenerators.ts new file mode 100644 index 0000000000..7f2d2a7bc2 --- /dev/null +++ b/shared-test-utils/src/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 }; +} 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";