diff --git a/change/@azure-msal-common-781f4bee-0546-4407-a682-b7b453e07706.json b/change/@azure-msal-common-781f4bee-0546-4407-a682-b7b453e07706.json new file mode 100644 index 0000000000..8be28694f3 --- /dev/null +++ b/change/@azure-msal-common-781f4bee-0546-4407-a682-b7b453e07706.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Fix hardcoded metadata fetching for tenanted authorities #6622", + "packageName": "@azure/msal-common", + "email": "hemoral@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/lib/msal-browser/test/app/PublicClientApplication.spec.ts b/lib/msal-browser/test/app/PublicClientApplication.spec.ts index bacaa3bdbc..d9c1ca85c0 100644 --- a/lib/msal-browser/test/app/PublicClientApplication.spec.ts +++ b/lib/msal-browser/test/app/PublicClientApplication.spec.ts @@ -5202,7 +5202,6 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { testAccount3.clientInfo = TEST_DATA_CLIENT_INFO.TEST_CLIENT_INFO_B64ENCODED; - testAccount3.idTokenClaims = testAccountInfo3.idTokenClaims; const idToken3: IdTokenEntity = { realm: testAccountInfo3.tenantId, @@ -5238,7 +5237,6 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { testAccount4.clientInfo = TEST_DATA_CLIENT_INFO.TEST_CLIENT_INFO_B64ENCODED; - testAccount4.idTokenClaims = testAccountInfo4.idTokenClaims; const idToken4: IdTokenEntity = { realm: testAccountInfo4.tenantId, diff --git a/lib/msal-common/src/authority/Authority.ts b/lib/msal-common/src/authority/Authority.ts index 1d2133918f..f26aeb0178 100644 --- a/lib/msal-common/src/authority/Authority.ts +++ b/lib/msal-common/src/authority/Authority.ts @@ -855,7 +855,7 @@ export class Authority { } else { const hardcodedMetadata = getCloudDiscoveryMetadataFromHardcodedValues( - this.canonicalAuthority + this.hostnameAndPort ); if (hardcodedMetadata) { this.logger.verbose( @@ -1264,13 +1264,11 @@ export function buildStaticAuthorityOptions( authOptions: Partial ): StaticAuthorityOptions { const rawCloudDiscoveryMetadata = authOptions.cloudDiscoveryMetadata; - let cloudDiscoveryMetadata: CloudDiscoveryMetadata[] | undefined = + let cloudDiscoveryMetadata: CloudInstanceDiscoveryResponse | undefined = undefined; if (rawCloudDiscoveryMetadata) { try { - cloudDiscoveryMetadata = JSON.parse( - rawCloudDiscoveryMetadata - ).metadata; + cloudDiscoveryMetadata = JSON.parse(rawCloudDiscoveryMetadata); } catch (e) { throw createClientConfigurationError( ClientConfigurationErrorCodes.invalidCloudDiscoveryMetadata diff --git a/lib/msal-common/src/authority/AuthorityMetadata.ts b/lib/msal-common/src/authority/AuthorityMetadata.ts index 6b64a8dc29..c6e2f44491 100644 --- a/lib/msal-common/src/authority/AuthorityMetadata.ts +++ b/lib/msal-common/src/authority/AuthorityMetadata.ts @@ -3,7 +3,10 @@ * Licensed under the MIT License. */ +import { Logger } from "../logger/Logger"; import { UrlString } from "../url/UrlString"; +import { AuthorityMetadataSource } from "../utils/Constants"; +import { StaticAuthorityOptions } from "./AuthorityOptions"; import { CloudDiscoveryMetadata } from "./CloudDiscoveryMetadata"; export const rawMetdataJSON = { @@ -552,393 +555,47 @@ export const rawMetdataJSON = { }, }, instanceDiscoveryMetadata: { - "https://login.microsoftonline.com/common/": { - tenant_discovery_endpoint: - "https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration", - "api-version": "1.1", - metadata: [ - { - preferred_network: "login.microsoftonline.com", - preferred_cache: "login.windows.net", - aliases: [ - "login.microsoftonline.com", - "login.windows.net", - "login.microsoft.com", - "sts.windows.net", - ], - }, - { - preferred_network: "login.partner.microsoftonline.cn", - preferred_cache: "login.partner.microsoftonline.cn", - aliases: [ - "login.partner.microsoftonline.cn", - "login.chinacloudapi.cn", - ], - }, - { - preferred_network: "login.microsoftonline.de", - preferred_cache: "login.microsoftonline.de", - aliases: ["login.microsoftonline.de"], - }, - { - preferred_network: "login.microsoftonline.us", - preferred_cache: "login.microsoftonline.us", - aliases: [ - "login.microsoftonline.us", - "login.usgovcloudapi.net", - ], - }, - { - preferred_network: "login-us.microsoftonline.com", - preferred_cache: "login-us.microsoftonline.com", - aliases: ["login-us.microsoftonline.com"], - }, - ], - }, - "https://login.chinacloudapi.cn/common/": { - tenant_discovery_endpoint: - "https://login.chinacloudapi.cn/common/v2.0/.well-known/openid-configuration", - "api-version": "1.1", - metadata: [ - { - preferred_network: "login.microsoftonline.com", - preferred_cache: "login.windows.net", - aliases: [ - "login.microsoftonline.com", - "login.windows.net", - "login.microsoft.com", - "sts.windows.net", - ], - }, - { - preferred_network: "login.partner.microsoftonline.cn", - preferred_cache: "login.partner.microsoftonline.cn", - aliases: [ - "login.partner.microsoftonline.cn", - "login.chinacloudapi.cn", - ], - }, - { - preferred_network: "login.microsoftonline.de", - preferred_cache: "login.microsoftonline.de", - aliases: ["login.microsoftonline.de"], - }, - { - preferred_network: "login.microsoftonline.us", - preferred_cache: "login.microsoftonline.us", - aliases: [ - "login.microsoftonline.us", - "login.usgovcloudapi.net", - ], - }, - { - preferred_network: "login-us.microsoftonline.com", - preferred_cache: "login-us.microsoftonline.com", - aliases: ["login-us.microsoftonline.com"], - }, - ], - }, - "https://login.microsoftonline.us/common/": { - tenant_discovery_endpoint: - "https://login.microsoftonline.us/common/v2.0/.well-known/openid-configuration", - "api-version": "1.1", - metadata: [ - { - preferred_network: "login.microsoftonline.com", - preferred_cache: "login.windows.net", - aliases: [ - "login.microsoftonline.com", - "login.windows.net", - "login.microsoft.com", - "sts.windows.net", - ], - }, - { - preferred_network: "login.partner.microsoftonline.cn", - preferred_cache: "login.partner.microsoftonline.cn", - aliases: [ - "login.partner.microsoftonline.cn", - "login.chinacloudapi.cn", - ], - }, - { - preferred_network: "login.microsoftonline.de", - preferred_cache: "login.microsoftonline.de", - aliases: ["login.microsoftonline.de"], - }, - { - preferred_network: "login.microsoftonline.us", - preferred_cache: "login.microsoftonline.us", - aliases: [ - "login.microsoftonline.us", - "login.usgovcloudapi.net", - ], - }, - { - preferred_network: "login-us.microsoftonline.com", - preferred_cache: "login-us.microsoftonline.com", - aliases: ["login-us.microsoftonline.com"], - }, - ], - }, - "https://login.microsoftonline.com/consumers/": { - tenant_discovery_endpoint: - "https://login.microsoftonline.com/consumers/v2.0/.well-known/openid-configuration", - "api-version": "1.1", - metadata: [ - { - preferred_network: "login.microsoftonline.com", - preferred_cache: "login.windows.net", - aliases: [ - "login.microsoftonline.com", - "login.windows.net", - "login.microsoft.com", - "sts.windows.net", - ], - }, - { - preferred_network: "login.partner.microsoftonline.cn", - preferred_cache: "login.partner.microsoftonline.cn", - aliases: [ - "login.partner.microsoftonline.cn", - "login.chinacloudapi.cn", - ], - }, - { - preferred_network: "login.microsoftonline.de", - preferred_cache: "login.microsoftonline.de", - aliases: ["login.microsoftonline.de"], - }, - { - preferred_network: "login.microsoftonline.us", - preferred_cache: "login.microsoftonline.us", - aliases: [ - "login.microsoftonline.us", - "login.usgovcloudapi.net", - ], - }, - { - preferred_network: "login-us.microsoftonline.com", - preferred_cache: "login-us.microsoftonline.com", - aliases: ["login-us.microsoftonline.com"], - }, - ], - }, - "https://login.chinacloudapi.cn/consumers/": { - tenant_discovery_endpoint: - "https://login.chinacloudapi.cn/consumers/v2.0/.well-known/openid-configuration", - "api-version": "1.1", - metadata: [ - { - preferred_network: "login.microsoftonline.com", - preferred_cache: "login.windows.net", - aliases: [ - "login.microsoftonline.com", - "login.windows.net", - "login.microsoft.com", - "sts.windows.net", - ], - }, - { - preferred_network: "login.partner.microsoftonline.cn", - preferred_cache: "login.partner.microsoftonline.cn", - aliases: [ - "login.partner.microsoftonline.cn", - "login.chinacloudapi.cn", - ], - }, - { - preferred_network: "login.microsoftonline.de", - preferred_cache: "login.microsoftonline.de", - aliases: ["login.microsoftonline.de"], - }, - { - preferred_network: "login.microsoftonline.us", - preferred_cache: "login.microsoftonline.us", - aliases: [ - "login.microsoftonline.us", - "login.usgovcloudapi.net", - ], - }, - { - preferred_network: "login-us.microsoftonline.com", - preferred_cache: "login-us.microsoftonline.com", - aliases: ["login-us.microsoftonline.com"], - }, - ], - }, - "https://login.microsoftonline.us/consumers/": { - tenant_discovery_endpoint: - "https://login.microsoftonline.us/consumers/v2.0/.well-known/openid-configuration", - "api-version": "1.1", - metadata: [ - { - preferred_network: "login.microsoftonline.com", - preferred_cache: "login.windows.net", - aliases: [ - "login.microsoftonline.com", - "login.windows.net", - "login.microsoft.com", - "sts.windows.net", - ], - }, - { - preferred_network: "login.partner.microsoftonline.cn", - preferred_cache: "login.partner.microsoftonline.cn", - aliases: [ - "login.partner.microsoftonline.cn", - "login.chinacloudapi.cn", - ], - }, - { - preferred_network: "login.microsoftonline.de", - preferred_cache: "login.microsoftonline.de", - aliases: ["login.microsoftonline.de"], - }, - { - preferred_network: "login.microsoftonline.us", - preferred_cache: "login.microsoftonline.us", - aliases: [ - "login.microsoftonline.us", - "login.usgovcloudapi.net", - ], - }, - { - preferred_network: "login-us.microsoftonline.com", - preferred_cache: "login-us.microsoftonline.com", - aliases: ["login-us.microsoftonline.com"], - }, - ], - }, - "https://login.microsoftonline.com/organizations/": { - tenant_discovery_endpoint: - "https://login.microsoftonline.com/organizations/v2.0/.well-known/openid-configuration", - "api-version": "1.1", - metadata: [ - { - preferred_network: "login.microsoftonline.com", - preferred_cache: "login.windows.net", - aliases: [ - "login.microsoftonline.com", - "login.windows.net", - "login.microsoft.com", - "sts.windows.net", - ], - }, - { - preferred_network: "login.partner.microsoftonline.cn", - preferred_cache: "login.partner.microsoftonline.cn", - aliases: [ - "login.partner.microsoftonline.cn", - "login.chinacloudapi.cn", - ], - }, - { - preferred_network: "login.microsoftonline.de", - preferred_cache: "login.microsoftonline.de", - aliases: ["login.microsoftonline.de"], - }, - { - preferred_network: "login.microsoftonline.us", - preferred_cache: "login.microsoftonline.us", - aliases: [ - "login.microsoftonline.us", - "login.usgovcloudapi.net", - ], - }, - { - preferred_network: "login-us.microsoftonline.com", - preferred_cache: "login-us.microsoftonline.com", - aliases: ["login-us.microsoftonline.com"], - }, - ], - }, - "https://login.chinacloudapi.cn/organizations/": { - tenant_discovery_endpoint: - "https://login.chinacloudapi.cn/organizations/v2.0/.well-known/openid-configuration", - "api-version": "1.1", - metadata: [ - { - preferred_network: "login.microsoftonline.com", - preferred_cache: "login.windows.net", - aliases: [ - "login.microsoftonline.com", - "login.windows.net", - "login.microsoft.com", - "sts.windows.net", - ], - }, - { - preferred_network: "login.partner.microsoftonline.cn", - preferred_cache: "login.partner.microsoftonline.cn", - aliases: [ - "login.partner.microsoftonline.cn", - "login.chinacloudapi.cn", - ], - }, - { - preferred_network: "login.microsoftonline.de", - preferred_cache: "login.microsoftonline.de", - aliases: ["login.microsoftonline.de"], - }, - { - preferred_network: "login.microsoftonline.us", - preferred_cache: "login.microsoftonline.us", - aliases: [ - "login.microsoftonline.us", - "login.usgovcloudapi.net", - ], - }, - { - preferred_network: "login-us.microsoftonline.com", - preferred_cache: "login-us.microsoftonline.com", - aliases: ["login-us.microsoftonline.com"], - }, - ], - }, - "https://login.microsoftonline.us/organizations/": { - tenant_discovery_endpoint: - "https://login.microsoftonline.us/organizations/v2.0/.well-known/openid-configuration", - "api-version": "1.1", - metadata: [ - { - preferred_network: "login.microsoftonline.com", - preferred_cache: "login.windows.net", - aliases: [ - "login.microsoftonline.com", - "login.windows.net", - "login.microsoft.com", - "sts.windows.net", - ], - }, - { - preferred_network: "login.partner.microsoftonline.cn", - preferred_cache: "login.partner.microsoftonline.cn", - aliases: [ - "login.partner.microsoftonline.cn", - "login.chinacloudapi.cn", - ], - }, - { - preferred_network: "login.microsoftonline.de", - preferred_cache: "login.microsoftonline.de", - aliases: ["login.microsoftonline.de"], - }, - { - preferred_network: "login.microsoftonline.us", - preferred_cache: "login.microsoftonline.us", - aliases: [ - "login.microsoftonline.us", - "login.usgovcloudapi.net", - ], - }, - { - preferred_network: "login-us.microsoftonline.com", - preferred_cache: "login-us.microsoftonline.com", - aliases: ["login-us.microsoftonline.com"], - }, - ], - }, + tenant_discovery_endpoint: + "https://{canonicalAuthority}/v2.0/.well-known/openid-configuration", + "api-version": "1.1", + metadata: [ + { + preferred_network: "login.microsoftonline.com", + preferred_cache: "login.windows.net", + aliases: [ + "login.microsoftonline.com", + "login.windows.net", + "login.microsoft.com", + "sts.windows.net", + ], + }, + { + preferred_network: "login.partner.microsoftonline.cn", + preferred_cache: "login.partner.microsoftonline.cn", + aliases: [ + "login.partner.microsoftonline.cn", + "login.chinacloudapi.cn", + ], + }, + { + preferred_network: "login.microsoftonline.de", + preferred_cache: "login.microsoftonline.de", + aliases: ["login.microsoftonline.de"], + }, + { + preferred_network: "login.microsoftonline.us", + preferred_cache: "login.microsoftonline.us", + aliases: [ + "login.microsoftonline.us", + "login.usgovcloudapi.net", + ], + }, + { + preferred_network: "login-us.microsoftonline.com", + preferred_cache: "login-us.microsoftonline.com", + aliases: ["login-us.microsoftonline.com"], + }, + ], }, }; @@ -947,58 +604,96 @@ export const InstanceDiscoveryMetadata = rawMetdataJSON.instanceDiscoveryMetadata; export const InstanceDiscoveryMetadataAliases: Set = new Set(); -for (const key in InstanceDiscoveryMetadata) { - for (const metadata of InstanceDiscoveryMetadata[key].metadata) { - for (const alias of metadata.aliases) { +InstanceDiscoveryMetadata.metadata.forEach( + (metadataEntry: CloudDiscoveryMetadata) => { + metadataEntry.aliases.forEach((alias: string) => { InstanceDiscoveryMetadataAliases.add(alias); - } + }); } -} +); /** - * Returns aliases for the given canonical authority if found in hardcoded Instance Discovery Metadata or null if not found - * @param canonicalAuthority + * Attempts to get an aliases array from the static authority metadata sources based on the canonical authority host + * @param staticAuthorityOptions + * @param logger * @returns */ -export function getHardcodedAliasesForCanonicalAuthority( - canonicalAuthority?: string -): string[] | null { +export function getAliasesFromStaticSources( + staticAuthorityOptions: StaticAuthorityOptions, + logger?: Logger +): string[] { + let staticAliases: string[] | undefined; + const canonicalAuthority = staticAuthorityOptions.canonicalAuthority; if (canonicalAuthority) { - const instanceDiscoveryMetadata = - getCloudDiscoveryMetadataFromHardcodedValues(canonicalAuthority); - if (instanceDiscoveryMetadata) { - return instanceDiscoveryMetadata.aliases; - } + const authorityHost = new UrlString( + canonicalAuthority + ).getUrlComponents().HostNameAndPort; + staticAliases = + getAliasesFromMetadata( + authorityHost, + staticAuthorityOptions.cloudDiscoveryMetadata?.metadata, + AuthorityMetadataSource.CONFIG, + logger + ) || + getAliasesFromMetadata( + authorityHost, + InstanceDiscoveryMetadata.metadata, + AuthorityMetadataSource.HARDCODED_VALUES, + logger + ) || + staticAuthorityOptions.knownAuthorities; } - return null; + + return staticAliases || []; } /** - * Returns aliases for from the raw cloud discovery metadata given in configuration or null if no configuration was provided + * Returns aliases for from the raw cloud discovery metadata passed in + * @param authorityHost * @param rawCloudDiscoveryMetadata * @returns */ -export function getAliasesFromConfigMetadata( - canonicalAuthority?: string, - cloudDiscoveryMetadata?: CloudDiscoveryMetadata[] +export function getAliasesFromMetadata( + authorityHost?: string, + cloudDiscoveryMetadata?: CloudDiscoveryMetadata[], + source?: AuthorityMetadataSource, + logger?: Logger ): string[] | null { - if (canonicalAuthority && cloudDiscoveryMetadata) { - const canonicalAuthorityUrlComponents = new UrlString( - canonicalAuthority - ).getUrlComponents(); + logger?.trace(`getAliasesFromMetadata called with source: ${source}`); + if (authorityHost && cloudDiscoveryMetadata) { const metadata = getCloudDiscoveryMetadataFromNetworkResponse( cloudDiscoveryMetadata, - canonicalAuthorityUrlComponents.HostNameAndPort + authorityHost ); if (metadata) { + logger?.trace( + `getAliasesFromMetadata: found cloud discovery metadata in ${source}, returning aliases` + ); return metadata.aliases; + } else { + logger?.trace( + `getAliasesFromMetadata: did not find cloud discovery metadata in ${source}` + ); } } return null; } +/** + * Get cloud discovery metadata for common authorities + */ +export function getCloudDiscoveryMetadataFromHardcodedValues( + authorityHost: string +): CloudDiscoveryMetadata | null { + const metadata = getCloudDiscoveryMetadataFromNetworkResponse( + InstanceDiscoveryMetadata.metadata, + authorityHost + ); + return metadata; +} + /** * Searches instance discovery network response for the entry that contains the host in the aliases list * @param response @@ -1006,35 +701,14 @@ export function getAliasesFromConfigMetadata( */ export function getCloudDiscoveryMetadataFromNetworkResponse( response: CloudDiscoveryMetadata[], - authority: string + authorityHost: string ): CloudDiscoveryMetadata | null { for (let i = 0; i < response.length; i++) { const metadata = response[i]; - if (metadata.aliases.includes(authority)) { + if (metadata.aliases.includes(authorityHost)) { return metadata; } } return null; } - -/** - * Get cloud discovery metadata for common authorities - */ -export function getCloudDiscoveryMetadataFromHardcodedValues( - canonicalAuthority: string -): CloudDiscoveryMetadata | null { - const canonicalAuthorityUrlComponents = new UrlString( - canonicalAuthority - ).getUrlComponents(); - - if (canonicalAuthority in InstanceDiscoveryMetadata) { - const metadata = getCloudDiscoveryMetadataFromNetworkResponse( - InstanceDiscoveryMetadata[canonicalAuthority].metadata, - canonicalAuthorityUrlComponents.HostNameAndPort - ); - return metadata; - } - - return null; -} diff --git a/lib/msal-common/src/authority/AuthorityOptions.ts b/lib/msal-common/src/authority/AuthorityOptions.ts index 4b494f68c0..5b6e8222a0 100644 --- a/lib/msal-common/src/authority/AuthorityOptions.ts +++ b/lib/msal-common/src/authority/AuthorityOptions.ts @@ -6,7 +6,7 @@ import { ProtocolMode } from "./ProtocolMode"; import { OIDCOptions } from "./OIDCOptions"; import { AzureRegionConfiguration } from "./AzureRegionConfiguration"; -import { CloudDiscoveryMetadata } from "./CloudDiscoveryMetadata"; +import { CloudInstanceDiscoveryResponse } from "./CloudInstanceDiscoveryResponse"; export type AuthorityOptions = { protocolMode: ProtocolMode; @@ -23,7 +23,7 @@ export type StaticAuthorityOptions = Partial< Pick > & { canonicalAuthority?: string; - cloudDiscoveryMetadata?: CloudDiscoveryMetadata[]; + cloudDiscoveryMetadata?: CloudInstanceDiscoveryResponse; }; export const AzureCloudInstance = { diff --git a/lib/msal-common/src/cache/CacheManager.ts b/lib/msal-common/src/cache/CacheManager.ts index c6c6b76af1..2efb8cde5a 100644 --- a/lib/msal-common/src/cache/CacheManager.ts +++ b/lib/msal-common/src/cache/CacheManager.ts @@ -43,10 +43,7 @@ import { BaseAuthRequest } from "../request/BaseAuthRequest"; import { Logger } from "../logger/Logger"; import { name, version } from "../packageMetadata"; import { StoreInCache } from "../request/StoreInCache"; -import { - getAliasesFromConfigMetadata, - getHardcodedAliasesForCanonicalAuthority, -} from "../authority/AuthorityMetadata"; +import { getAliasesFromStaticSources } from "../authority/AuthorityMetadata"; import { StaticAuthorityOptions } from "../authority/AuthorityOptions"; import { TokenClaims } from "../account/TokenClaims"; @@ -260,7 +257,13 @@ export abstract class CacheManager implements ICacheManager { */ getAccountInfoFilteredBy(accountFilter: AccountFilter): AccountInfo | null { const allAccounts = this.getAllAccounts(accountFilter); - if (allAccounts.length > 0) { + 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]; + } 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]; } else { return null; @@ -304,7 +307,7 @@ export abstract class CacheManager implements ICacheManager { return accountInfo; } } - return null; + return accountInfo; } private idTokenClaimsMatchAccountFilter( @@ -1422,17 +1425,11 @@ export abstract class CacheManager implements ICacheManager { ): boolean { // Check static authority options first for cases where authority metadata has not been resolved and cached yet if (this.staticAuthorityOptions) { - const staticAliases = - getAliasesFromConfigMetadata( - this.staticAuthorityOptions.canonicalAuthority, - this.staticAuthorityOptions.cloudDiscoveryMetadata - ) || - getHardcodedAliasesForCanonicalAuthority( - this.staticAuthorityOptions.canonicalAuthority - ) || - this.staticAuthorityOptions.knownAuthorities; + const staticAliases = getAliasesFromStaticSources( + this.staticAuthorityOptions, + this.commonLogger + ); if ( - staticAliases && staticAliases.includes(environment) && staticAliases.includes(entity.environment) ) { diff --git a/lib/msal-common/test/authority/Authority.spec.ts b/lib/msal-common/test/authority/Authority.spec.ts index 9d3f6301ed..fe31499381 100644 --- a/lib/msal-common/test/authority/Authority.spec.ts +++ b/lib/msal-common/test/authority/Authority.spec.ts @@ -1876,7 +1876,7 @@ describe("Authority.ts Class Unit Tests", () => { ); const hardcodedCloudDiscoveryMetadata = - InstanceDiscoveryMetadata[Constants.DEFAULT_AUTHORITY]; + InstanceDiscoveryMetadata; const expectedCloudDiscoveryMetadata = hardcodedCloudDiscoveryMetadata.metadata[0]; diff --git a/lib/msal-common/test/authority/AuthorityMetadata.spec.ts b/lib/msal-common/test/authority/AuthorityMetadata.spec.ts new file mode 100644 index 0000000000..e993e6e644 --- /dev/null +++ b/lib/msal-common/test/authority/AuthorityMetadata.spec.ts @@ -0,0 +1,63 @@ +import { AADAuthorityConstants, StaticAuthorityOptions } from "../../src"; +import { + InstanceDiscoveryMetadata, + getAliasesFromStaticSources, +} from "../../src/authority/AuthorityMetadata"; +import { + CLOUD_HOSTS, + METADATA_ALIASES, + TEST_CONFIG, +} from "../test_kit/StringConstants"; + +function buildCanonicalAuthorityUrl(host: string, tenant: string): string { + return `https://${host}/${tenant}/`; +} + +const TENANTS = [ + ...Object.values(AADAuthorityConstants), + TEST_CONFIG.MSAL_TENANT_ID, +]; +const CLOUD_KEYS = Object.keys(CLOUD_HOSTS); + +describe("AuthorityMetadata.ts Unit Tests", () => { + describe("getAliasesFromStaticSources()", () => { + describe("from config CloudDiscoveryMetadataResponse", () => { + const staticAuthorityOptions: StaticAuthorityOptions = { + cloudDiscoveryMetadata: InstanceDiscoveryMetadata, + }; + it("returns aliases for each cloud and tenant combination", () => { + CLOUD_KEYS.forEach((cloudKey) => { + TENANTS.forEach((tenant) => { + staticAuthorityOptions.canonicalAuthority = + buildCanonicalAuthorityUrl( + CLOUD_HOSTS[cloudKey], + tenant + ); + expect( + getAliasesFromStaticSources(staticAuthorityOptions) + ).toEqual(METADATA_ALIASES[cloudKey]); + }); + }); + }); + }); + + describe("from hardcoded CloudDiscoveryMetadataResponse", () => { + it("returns aliases for each cloud and tenant combination", () => { + CLOUD_KEYS.forEach((cloudKey) => { + TENANTS.forEach((tenant) => { + const staticAuthorityOptions = { + canonicalAuthority: buildCanonicalAuthorityUrl( + CLOUD_HOSTS[cloudKey], + 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 dcf06d9102..71b9b72728 100644 --- a/lib/msal-common/test/cache/CacheManager.spec.ts +++ b/lib/msal-common/test/cache/CacheManager.spec.ts @@ -598,72 +598,87 @@ describe("CacheManager.ts test cases", () => { ).toBe(false); }); - it("with hardcoded cloud discovery metadata", () => { - jest.spyOn( - authorityMetadata, - "getAliasesFromConfigMetadata" - ).mockReturnValue(null); - // filter by environment - expect( - mockCache.cacheManager.credentialMatchesFilter( - testIdToken, - { - environment: testIdToken.environment, - } - ) - ).toBe(true); - expect( - mockCache.cacheManager.credentialMatchesFilter( - testAccessToken, - { - environment: testAccessToken.environment, - } - ) - ).toBe(true); - expect( - mockCache.cacheManager.credentialMatchesFilter( - testRefreshToken, - { - environment: testRefreshToken.environment, - } - ) - ).toBe(true); + describe("with hardcoded cloud discovery metadata", () => { + beforeEach(() => { + jest.spyOn( + authorityMetadata, + "getAliasesFromMetadata" + ).mockReturnValueOnce(null); + }); + + it("ID token matches when filter contains it's own environment", () => { + // filter by environment + expect( + mockCache.cacheManager.credentialMatchesFilter( + testIdToken, + { + environment: testIdToken.environment, + } + ) + ).toBe(true); + }); + + it("Access token matches when filter contains it's own enviroment", () => { + expect( + mockCache.cacheManager.credentialMatchesFilter( + testAccessToken, + { + environment: testAccessToken.environment, + } + ) + ).toBe(true); + }); + + it("Refresh token matches when filter contains it's own environment", () => { + expect( + mockCache.cacheManager.credentialMatchesFilter( + testRefreshToken, + { + environment: testRefreshToken.environment, + } + ) + ).toBe(true); + }); // Test failure cases - expect( - mockCache.cacheManager.credentialMatchesFilter( - testIdToken, - { - environment: "wrong.contoso.com", - } - ) - ).toBe(false); - expect( - mockCache.cacheManager.credentialMatchesFilter( - testAccessToken, - { - environment: "wrong.contoso.com", - } - ) - ).toBe(false); - expect( - mockCache.cacheManager.credentialMatchesFilter( - testRefreshToken, - { - environment: "wrong.contoso.com", - } - ) - ).toBe(false); + it("ID token does not match when filter contains a different environment", () => { + expect( + mockCache.cacheManager.credentialMatchesFilter( + testRefreshToken, + { + environment: testRefreshToken.environment, + } + ) + ).toBe(true); + }); + + it("Access token does not match when filter contains a different environment", () => { + expect( + mockCache.cacheManager.credentialMatchesFilter( + testAccessToken, + { + environment: "wrong.contoso.com", + } + ) + ).toBe(false); + }); + + it("Refresh token does not match when filter contains a different environment", () => { + expect( + mockCache.cacheManager.credentialMatchesFilter( + testRefreshToken, + { + environment: "wrong.contoso.com", + } + ) + ).toBe(false); + }); }); it("with knownAuthorities", () => { jest.spyOn( authorityMetadata, - "getAliasesFromConfigMetadata" - ).mockReturnValue(null); - jest.spyOn( - authorityMetadata, - "getHardcodedAliasesForCanonicalAuthority" + "getAliasesFromMetadata" ).mockReturnValue(null); // filter by environment expect( diff --git a/lib/msal-common/test/test_kit/StringConstants.ts b/lib/msal-common/test/test_kit/StringConstants.ts index b407d2247e..df5f7f99c1 100644 --- a/lib/msal-common/test/test_kit/StringConstants.ts +++ b/lib/msal-common/test/test_kit/StringConstants.ts @@ -242,14 +242,26 @@ export const TEST_STATE_VALUES = { TEST_STATE: `eyJpZCI6IjExNTUzYTliLTcxMTYtNDhiMS05ZDQ4LWY2ZDRhOGZmODM3MSIsInRzIjoxNTkyODQ2NDgyfQ==${Constants.RESOURCE_DELIM}userState`, }; -export const TEST_HOST_LIST = [ - "login.windows.net", - "login.chinacloudapi.cn", - "login.cloudgovapi.us", - "login.microsoftonline.com", - "login.microsoftonline.de", - "login.microsoftonline.us", -]; +export const CLOUD_HOSTS = { + PublicCloud: "login.microsoftonline.com", + ChinaCloud: "login.chinacloudapi.cn", + GermanyCloud: "login.microsoftonline.de", + USGovAGCloud: "login.microsoftonline.us", + USGovCloud: "login-us.microsoftonline.com", +}; + +export const METADATA_ALIASES = { + PublicCloud: [ + "login.microsoftonline.com", + "login.windows.net", + "login.microsoft.com", + "sts.windows.net", + ], + ChinaCloud: ["login.partner.microsoftonline.cn", "login.chinacloudapi.cn"], + GermanyCloud: ["login.microsoftonline.de"], + USGovAGCloud: ["login.microsoftonline.us", "login.usgovcloudapi.net"], + USGovCloud: ["login-us.microsoftonline.com"], +}; export const PREFERRED_CACHE_ALIAS = "login.windows.net"; export const ADFS_AUTHORITY = "myadfs.com/adfs"; diff --git a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/authConfigs/aadTenantedAuthConfig.json b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/authConfigs/aadTenantedAuthConfig.json new file mode 100644 index 0000000000..8d9d0c5996 --- /dev/null +++ b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/authConfigs/aadTenantedAuthConfig.json @@ -0,0 +1,18 @@ +{ + "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"] + } +} 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 new file mode 100644 index 0000000000..1bd900ff81 --- /dev/null +++ b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/test/browserAADTenanted.spec.ts @@ -0,0 +1,457 @@ +import * as puppeteer from "puppeteer"; +import { + Screenshot, + createFolder, + setupCredentials, + enterCredentials, + ONE_SECOND_IN_MS, + clickLoginPopup, + clickLoginRedirect, + clickLogoutPopup, + clickLogoutRedirect, + waitForReturnToApp, + getBrowser, + getHomeUrl, + pcaInitializedPoller, + BrowserCacheUtils, + LabApiQueryParams, + AzureEnvironments, + AppTypes, + LabClient, +} from "e2e-test-utils"; +import { + msalConfig as aadMsalConfig, + request as aadTokenRequest, +} from "../authConfigs/aadTenantedAuthConfig.json"; +import fs from "fs"; +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; + let page: puppeteer.Page; + let BrowserCache: BrowserCacheUtils; + let username = ""; + let accountPwd = ""; + + beforeAll(async () => { + createFolder(SCREENSHOT_BASE_FOLDER_NAME); + browser = await getBrowser(); + sampleHomeUrl = getHomeUrl(); + + const labApiParams: LabApiQueryParams = { + azureEnvironment: AzureEnvironments.CLOUD, + appType: AppTypes.CLOUD, + }; + + 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, + }) + ); + }); + + afterAll(async () => { + await context.close(); + await browser.close(); + }); + + describe("login Tests", () => { + 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); + }); + + afterEach(async () => { + await page.evaluate(() => + Object.assign({}, window.sessionStorage.clear()) + ); + await page.evaluate(() => + Object.assign({}, window.localStorage.clear()) + ); + await page.close(); + }); + + it("Performs loginRedirect", async () => { + const testName = "redirectBaseCase"; + const screenshot = new Screenshot( + `${SCREENSHOT_BASE_FOLDER_NAME}/${testName}` + ); + + await clickLoginRedirect(screenshot, page); + await enterCredentials(page, screenshot, username, accountPwd); + await waitForReturnToApp(screenshot, page); + // Verify browser cache contains Account, idToken, AccessToken and RefreshToken + await verifyTokenStore(BrowserCache, aadTokenRequest.scopes); + }); + + it("Performs loginRedirect from url with empty query string", async () => { + await page.goto(sampleHomeUrl + "?"); + const testName = "redirectEmptyQueryString"; + const screenshot = new Screenshot( + `${SCREENSHOT_BASE_FOLDER_NAME}/${testName}` + ); + + await clickLoginRedirect(screenshot, page); + await enterCredentials(page, screenshot, username, accountPwd); + await waitForReturnToApp(screenshot, page); + // Verify browser cache contains Account, idToken, AccessToken and RefreshToken + await verifyTokenStore(BrowserCache, aadTokenRequest.scopes); + expect(page.url()).toEqual(sampleHomeUrl); + }); + + it("Performs loginRedirect from url with test query string", async () => { + const testUrl = sampleHomeUrl + "?test"; + await page.goto(testUrl); + const testName = "redirectEmptyQueryString"; + const screenshot = new Screenshot( + `${SCREENSHOT_BASE_FOLDER_NAME}/${testName}` + ); + + await clickLoginRedirect(screenshot, page); + await enterCredentials(page, screenshot, username, accountPwd); + await waitForReturnToApp(screenshot, page); + // Verify browser cache contains Account, idToken, AccessToken and RefreshToken + await verifyTokenStore(BrowserCache, aadTokenRequest.scopes); + expect(page.url()).toEqual(testUrl); + }); + + it("Performs loginRedirect with relative redirectUri", async () => { + const relativeRedirectUriRequest: RedirectRequest = { + ...aadTokenRequest, + redirectUri: "/", + }; + fs.writeFileSync( + "./app/customizable-e2e-test/testConfig.json", + JSON.stringify({ + msalConfig: aadMsalConfig, + request: relativeRedirectUriRequest, + }) + ); + page.reload(); + + const testName = "redirectBaseCase"; + const screenshot = new Screenshot( + `${SCREENSHOT_BASE_FOLDER_NAME}/${testName}` + ); + + await clickLoginRedirect(screenshot, page); + await enterCredentials(page, screenshot, username, accountPwd); + await waitForReturnToApp(screenshot, page); + // Verify browser cache contains Account, idToken, AccessToken and RefreshToken + await verifyTokenStore(BrowserCache, aadTokenRequest.scopes); + }); + + it("Performs loginRedirect with relative redirectStartPage", async () => { + const relativeRedirectUriRequest: RedirectRequest = { + ...aadTokenRequest, + redirectStartPage: "/", + }; + fs.writeFileSync( + "./app/customizable-e2e-test/testConfig.json", + JSON.stringify({ + msalConfig: aadMsalConfig, + request: relativeRedirectUriRequest, + }) + ); + page.reload(); + + const testName = "redirectBaseCase"; + const screenshot = new Screenshot( + `${SCREENSHOT_BASE_FOLDER_NAME}/${testName}` + ); + + await clickLoginRedirect(screenshot, page); + await enterCredentials(page, screenshot, username, accountPwd); + await waitForReturnToApp(screenshot, page); + // Verify browser cache contains Account, idToken, AccessToken and RefreshToken + await verifyTokenStore(BrowserCache, aadTokenRequest.scopes); + }); + + it("Performs loginPopup", async () => { + const testName = "popupBaseCase"; + const 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 + ); + + // Verify browser cache contains Account, idToken, AccessToken and RefreshToken + await verifyTokenStore(BrowserCache, aadTokenRequest.scopes); + }); + }); + + 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", async () => { + testName = "acquireTokenRedirect"; + screenshot = new Screenshot( + `${SCREENSHOT_BASE_FOLDER_NAME}/${testName}` + ); + await page.waitForSelector("#acquireTokenRedirect"); + + // Remove access_tokens from cache so we can verify acquisition + const tokenStore = await BrowserCache.getTokens(); + await BrowserCache.removeTokens(tokenStore.refreshTokens); + await BrowserCache.removeTokens(tokenStore.accessTokens); + await page.click("#acquireTokenRedirect"); + await page.waitForSelector("#scopes-acquired"); + await screenshot.takeScreenshot( + page, + "acquireTokenRedirectGotTokens" + ); + + // Verify browser cache contains Account, idToken, AccessToken and RefreshToken + await verifyTokenStore(BrowserCache, aadTokenRequest.scopes); + }); + + it("acquireTokenPopup", async () => { + testName = "acquireTokenPopup"; + screenshot = new Screenshot( + `${SCREENSHOT_BASE_FOLDER_NAME}/${testName}` + ); + await page.waitForSelector("#acquireTokenPopup"); + + // 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("#acquireTokenPopup"); + await page.waitForSelector("#scopes-acquired"); + await screenshot.takeScreenshot(page, "acquireTokenPopupGotTokens"); + + // Verify browser cache contains Account, idToken, AccessToken and RefreshToken + await verifyTokenStore(BrowserCache, aadTokenRequest.scopes); + }); + + it("acquireTokenSilent from Cache", async () => { + testName = "acquireTokenSilentCache"; + screenshot = new Screenshot( + `${SCREENSHOT_BASE_FOLDER_NAME}/${testName}` + ); + await page.waitForSelector("#acquireTokenSilent"); + await page.click("#acquireTokenSilent"); + await page.waitForSelector("#scopes-acquired"); + await screenshot.takeScreenshot( + page, + "acquireTokenSilent-fromCache-GotTokens" + ); + + const telemetryCacheEntry = + await BrowserCache.getTelemetryCacheEntry( + aadMsalConfig.auth.clientId + ); + expect(telemetryCacheEntry).toBeDefined(); + expect(telemetryCacheEntry["cacheHits"]).toEqual(1); + // Remove Telemetry Cache entry for next test + await BrowserCache.removeTokens([ + BrowserCacheUtils.getTelemetryKey(aadMsalConfig.auth.clientId), + ]); + + // Verify browser cache contains Account, idToken, AccessToken and RefreshToken + await verifyTokenStore(BrowserCache, aadTokenRequest.scopes); + }); + + it("acquireTokenSilent via RefreshToken", async () => { + testName = "acquireTokenSilentRT"; + 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-GotTokens" + ); + + // Verify browser cache contains Account, idToken, AccessToken and RefreshToken + await verifyTokenStore(BrowserCache, aadTokenRequest.scopes); + }); + }); +});