From 024f979a535b793d47189de65366c49080095e55 Mon Sep 17 00:00:00 2001 From: Hector Morales Date: Wed, 12 Jul 2023 16:32:31 -0700 Subject: [PATCH] Prioritize hardcoded metadata over network-sourced metadata (#6231) - Fixes a bug where hardcoded metadata for Cloud Instance Discovery was incorrectly built and therefore never worked - Promotes hardcoded Cloud Instance Discovery metadata over cached and network metadata sources - Fixes a bug where azure region update method mutates the existing metadata object instead of returning a new object causing MSAL to prepend azure region cumulatively - Promotes hardcoded Endpoint metadata over cached and network metadata sources --- ...-60ff8f68-4608-44c2-a24a-539b17c1dcc9.json | 7 + lib/msal-browser/docs/configuration.md | 28 +- lib/msal-common/src/authority/Authority.ts | 334 ++-- .../test/authority/Authority.spec.ts | 1447 +++++++++++------ .../client/AuthorizationCodeClient.spec.ts | 8 +- lib/msal-node/docs/configuration.md | 75 +- 6 files changed, 1266 insertions(+), 633 deletions(-) create mode 100644 change/@azure-msal-common-60ff8f68-4608-44c2-a24a-539b17c1dcc9.json diff --git a/change/@azure-msal-common-60ff8f68-4608-44c2-a24a-539b17c1dcc9.json b/change/@azure-msal-common-60ff8f68-4608-44c2-a24a-539b17c1dcc9.json new file mode 100644 index 0000000000..4462f17283 --- /dev/null +++ b/change/@azure-msal-common-60ff8f68-4608-44c2-a24a-539b17c1dcc9.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Prioritize hardcoded metadata over network-sourced metadata #6231", + "packageName": "@azure/msal-common", + "email": "hemoral@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/lib/msal-browser/docs/configuration.md b/lib/msal-browser/docs/configuration.md index 839d06d6c5..7072d40077 100644 --- a/lib/msal-browser/docs/configuration.md +++ b/lib/msal-browser/docs/configuration.md @@ -74,20 +74,20 @@ const msalInstance = new PublicClientApplication(msalConfig); ### Auth Config Options -| Option | Description | Format | Default Value | -| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | -| `clientId` | App ID of your application. Can be found in your [portal registration](../README#prerequisites). | UUID/GUID | None. This parameter is required in order for MSAL to perform any actions. | -| `authority` | URI of the tenant to authenticate and authorize with. Usually takes the form of `https://{uri}/{tenantid}` (see [Authority](../../msal-common/docs/authority.md)) | String in URI format with tenant - `https://{uri}/{tenantid}` | `https://login.microsoftonline.com/common` | -| `knownAuthorities` | An array of URIs that are known to be valid. Used in B2C scenarios. | Array of strings in URI format | Empty array `[]` | -| `cloudDiscoveryMetadata` | A string containing the cloud discovery response. Used in AAD scenarios. See [Performance](../../msal-common/docs/performance.md) for more info | string | Empty string `""` | -| `authorityMetadata` | A string containing the .well-known/openid-configuration endpoint response. See [Performance](../../msal-common/docs/performance.md) for more info | string | Empty string `""` | -| `redirectUri` | URI where the authorization code response is sent back to. Whatever location is specified here must have the MSAL library available to handle the response. | String in absolute or relative URI format | Login request page (`window.location.href` of page which made auth request) | -| `postLogoutRedirectUri` | URI that is redirected to after a logout() call is made. | String in absolute or relative URI format. Pass `null` to disable post logout redirect. | Login request page (`window.location.href` of page which made auth request) | -| `navigateToLoginRequestUrl` | If `true`, will navigate back to the original request location before processing the authorization code response. If the `redirectUri` is the same as the original request location, this flag should be set to false. | boolean | `true` | -| `clientCapabilities` | Array of capabilities to be added to all network requests as part of the `xms_cc` claims request (see: [Client capability in MSAL](../../msal-common/docs/client-capability.md)) | Array of strings | [] | -| `protocolMode` | Enum representing the protocol mode to use. If `"AAD"`, will function on the OIDC-compliant AAD v2 endpoints; if `"OIDC"`, will function on other OIDC-compliant endpoints. | string | `"AAD"` | -| `azureCloudOptions` | A defined set of azure cloud options for developers to default to their specific cloud authorities, for specific clouds supported please refer to the [AzureCloudInstance](https://aka.ms/msaljs/azure_cloud_instance) | [AzureCloudOptions](https://azuread.github.io/microsoft-authentication-library-for-js/ref/modules/_azure_msal_common.html#azurecloudoptions) | [AzureCloudInstance.None](msaljs/azure_cloud_instance) | -| `skipAuthorityMetadataCache` | A flag to choose whether to use the local metadata cache during authority initialization. Metadata cache would be used if no authority metadata is provided and after a network call for metadata has failed (see [Authority](../../msal-common/docs/authority.md)) | boolean | `false` | +| Option | Description | Format | Default Value | +| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | +| `clientId` | App ID of your application. Can be found in your [portal registration](../README#prerequisites). | UUID/GUID | None. This parameter is required in order for MSAL to perform any actions. | +| `authority` | URI of the tenant to authenticate and authorize with. Usually takes the form of `https://{uri}/{tenantid}` (see [Authority](../../msal-common/docs/authority.md)) | String in URI format with tenant - `https://{uri}/{tenantid}` | `https://login.microsoftonline.com/common` | +| `knownAuthorities` | An array of URIs that are known to be valid. Used in B2C scenarios. | Array of strings in URI format | Empty array `[]` | +| `cloudDiscoveryMetadata` | A string containing the cloud discovery response. Used in AAD scenarios. See [Performance](../../msal-common/docs/performance.md) for more info | string | Empty string `""` | +| `authorityMetadata` | A string containing the .well-known/openid-configuration endpoint response. See [Performance](../../msal-common/docs/performance.md) for more info | string | Empty string `""` | +| `redirectUri` | URI where the authorization code response is sent back to. Whatever location is specified here must have the MSAL library available to handle the response. | String in absolute or relative URI format | Login request page (`window.location.href` of page which made auth request) | +| `postLogoutRedirectUri` | URI that is redirected to after a logout() call is made. | String in absolute or relative URI format. Pass `null` to disable post logout redirect. | Login request page (`window.location.href` of page which made auth request) | +| `navigateToLoginRequestUrl` | If `true`, will navigate back to the original request location before processing the authorization code response. If the `redirectUri` is the same as the original request location, this flag should be set to false. | boolean | `true` | +| `clientCapabilities` | Array of capabilities to be added to all network requests as part of the `xms_cc` claims request (see: [Client capability in MSAL](../../msal-common/docs/client-capability.md)) | Array of strings | [] | +| `protocolMode` | Enum representing the protocol mode to use. If `"AAD"`, will function on the OIDC-compliant AAD v2 endpoints; if `"OIDC"`, will function on other OIDC-compliant endpoints. | string | `"AAD"` | +| `azureCloudOptions` | A defined set of azure cloud options for developers to default to their specific cloud authorities, for specific clouds supported please refer to the [AzureCloudInstance](https://aka.ms/msaljs/azure_cloud_instance) | [AzureCloudOptions](https://azuread.github.io/microsoft-authentication-library-for-js/ref/modules/_azure_msal_common.html#azurecloudoptions) | [AzureCloudInstance.None](msaljs/azure_cloud_instance) | +| `skipAuthorityMetadataCache` | A flag to choose whether to use the local metadata cache during authority initialization. Metadata cache would be used if no authority metadata is provided and before a network call for metadata has been made (see [Authority](../../msal-common/docs/authority.md)) | boolean | `false` | ### Cache Config Options diff --git a/lib/msal-common/src/authority/Authority.ts b/lib/msal-common/src/authority/Authority.ts index 14e2890da0..44a36448c9 100644 --- a/lib/msal-common/src/authority/Authority.ts +++ b/lib/msal-common/src/authority/Authority.ts @@ -73,13 +73,13 @@ export class Authority { // Correlation Id protected correlationId: string | undefined; // Reserved tenant domain names that will not be replaced with tenant id - private static reservedTenantDomains: Set = (new Set([ + private static reservedTenantDomains: Set = new Set([ "{tenant}", "{tenantid}", AADAuthorityConstants.COMMON, AADAuthorityConstants.CONSUMERS, - AADAuthorityConstants.ORGANIZATIONS - ])); + AADAuthorityConstants.ORGANIZATIONS, + ]); constructor( authority: string, @@ -123,7 +123,7 @@ export class Authority { const pathSegments = authorityUri.PathSegments; if (pathSegments.length) { - switch(pathSegments[0].toLowerCase()) { + switch (pathSegments[0].toLowerCase()) { case Constants.ADFS: return AuthorityType.Adfs; case Constants.DSTS: @@ -224,7 +224,9 @@ export class Authority { public get deviceCodeEndpoint(): string { if (this.discoveryComplete()) { - return this.replacePath(this.metadata.token_endpoint.replace("/token", "/devicecode")); + return this.replacePath( + this.metadata.token_endpoint.replace("/token", "/devicecode") + ); } else { throw ClientAuthError.createEndpointDiscoveryIncompleteError( "Discovery incomplete." @@ -281,10 +283,14 @@ export class Authority { * @private */ private canReplaceTenant(authorityUri: IUri): boolean { - return authorityUri.PathSegments.length === 1 - && !Authority.reservedTenantDomains.has(authorityUri.PathSegments[0]) - && this.getAuthorityType(authorityUri) === AuthorityType.Default - && this.protocolMode === ProtocolMode.AAD; + return ( + authorityUri.PathSegments.length === 1 && + !Authority.reservedTenantDomains.has( + authorityUri.PathSegments[0] + ) && + this.getAuthorityType(authorityUri) === AuthorityType.Default && + this.protocolMode === ProtocolMode.AAD + ); } /** @@ -301,28 +307,41 @@ export class Authority { */ private replacePath(urlString: string): string { let endpoint = urlString; - const cachedAuthorityUrl = new UrlString(this.metadata.canonical_authority); - const cachedAuthorityUrlComponents = cachedAuthorityUrl.getUrlComponents(); + const cachedAuthorityUrl = new UrlString( + this.metadata.canonical_authority + ); + const cachedAuthorityUrlComponents = + cachedAuthorityUrl.getUrlComponents(); const cachedAuthorityParts = cachedAuthorityUrlComponents.PathSegments; - const currentAuthorityParts = this.canonicalAuthorityUrlComponents.PathSegments; + const currentAuthorityParts = + this.canonicalAuthorityUrlComponents.PathSegments; currentAuthorityParts.forEach((currentPart, index) => { let cachedPart = cachedAuthorityParts[index]; - if (index === 0 && this.canReplaceTenant(cachedAuthorityUrlComponents)) - { - const tenantId = (new UrlString(this.metadata.authorization_endpoint)).getUrlComponents().PathSegments[0]; + if ( + index === 0 && + this.canReplaceTenant(cachedAuthorityUrlComponents) + ) { + const tenantId = new UrlString( + this.metadata.authorization_endpoint + ).getUrlComponents().PathSegments[0]; /** * Check if AAD canonical authority contains tenant domain name, for example "testdomain.onmicrosoft.com", * by comparing its first path segment to the corresponding authorization endpoint path segment, which is * always resolved with tenant id by OIDC. */ if (cachedPart !== tenantId) { - this.logger.verbose(`Replacing tenant domain name ${cachedPart} with id ${tenantId}`); + this.logger.verbose( + `Replacing tenant domain name ${cachedPart} with id ${tenantId}` + ); cachedPart = tenantId; } } if (currentPart !== cachedPart) { - endpoint = endpoint.replace(`/${cachedPart}/`, `/${currentPart}/`); + endpoint = endpoint.replace( + `/${cachedPart}/`, + `/${currentPart}/` + ); } }); @@ -362,6 +381,7 @@ export class Authority { let metadataEntity = this.cacheManager.getAuthorityMetadataByAlias( this.hostnameAndPort ); + if (!metadataEntity) { metadataEntity = new AuthorityMetadataEntity(); metadataEntity.updateCanonicalAuthority(this.canonicalAuthority); @@ -413,34 +433,96 @@ export class Authority { PerformanceEvents.AuthorityUpdateEndpointMetadata, this.correlationId ); - + this.logger.verbose( + "Attempting to get endpoint metadata from authority configuration" + ); let metadata = this.getEndpointMetadataFromConfig(); if (metadata) { + this.logger.verbose( + "Found endpoint metadata in authority configuration" + ); metadataEntity.updateEndpointMetadata(metadata, false); return AuthorityMetadataSource.CONFIG; } + this.logger.verbose( + "Did not find endpoint metadata in the config... Attempting to get endpoint metadata from the hardcoded values." + ); + + // skipAuthorityMetadataCache is used to bypass hardcoded authority metadata and force a network metadata cache lookup and network metadata request if no cached response is available. + if (this.authorityOptions.skipAuthorityMetadataCache) { + this.logger.verbose( + "Skipping hardcoded metadata cache since skipAuthorityMetadataCache is set to true. Attempting to get endpoint metadata from the network metadata cache." + ); + } else { + let hardcodedMetadata = + this.getEndpointMetadataFromHardcodedValues(); + if (hardcodedMetadata) { + this.logger.verbose( + "Found endpoint metadata from hardcoded values." + ); + // If the user prefers to use an azure region replace the global endpoints with regional information. + if ( + this.authorityOptions.azureRegionConfiguration?.azureRegion + ) { + this.performanceClient?.setPreQueueTime( + PerformanceEvents.AuthorityUpdateMetadataWithRegionalInformation, + this.correlationId + ); + this.logger.verbose( + "Found azure region configuration. Updating endpoints with regional information." + ); + hardcodedMetadata = + await this.updateMetadataWithRegionalInformation( + hardcodedMetadata + ); + } + + metadataEntity.updateEndpointMetadata(hardcodedMetadata, false); + return AuthorityMetadataSource.HARDCODED_VALUES; + } else { + this.logger.verbose( + "Did not find endpoint metadata in hardcoded values... Attempting to get endpoint metadata from the network metadata cache." + ); + } + } + + // Check cached metadata entity expiration status + const metadataEntityExpired = metadataEntity.isExpired(); if ( this.isAuthoritySameType(metadataEntity) && metadataEntity.endpointsFromNetwork && - !metadataEntity.isExpired() + !metadataEntityExpired ) { // No need to update + this.logger.verbose("Found endpoint metadata in the cache."); return AuthorityMetadataSource.CACHE; + } else if (metadataEntityExpired) { + this.logger.verbose("The metadata entity is expired."); } + this.logger.verbose( + "Did not find cached endpoint metadata... Attempting to get endpoint metadata from the network." + ); + this.performanceClient?.setPreQueueTime( PerformanceEvents.AuthorityGetEndpointMetadataFromNetwork, this.correlationId ); metadata = await this.getEndpointMetadataFromNetwork(); if (metadata) { + this.logger.verbose( + "Endpoint metadata was successfully returned from getEndpointMetadataFromNetwork()" + ); // If the user prefers to use an azure region replace the global endpoints with regional information. if (this.authorityOptions.azureRegionConfiguration?.azureRegion) { this.performanceClient?.setPreQueueTime( PerformanceEvents.AuthorityUpdateMetadataWithRegionalInformation, this.correlationId ); + this.logger.verbose( + "Found azure region configuration. Updating endpoints with regional information." + ); metadata = await this.updateMetadataWithRegionalInformation( metadata ); @@ -448,28 +530,11 @@ export class Authority { metadataEntity.updateEndpointMetadata(metadata, true); return AuthorityMetadataSource.NETWORK; - } - - let harcodedMetadata = this.getEndpointMetadataFromHardcodedValues(); - if ( - harcodedMetadata && - !this.authorityOptions.skipAuthorityMetadataCache - ) { - // If the user prefers to use an azure region replace the global endpoints with regional information. - if (this.authorityOptions.azureRegionConfiguration?.azureRegion) { - this.performanceClient?.setPreQueueTime( - PerformanceEvents.AuthorityUpdateMetadataWithRegionalInformation, - this.correlationId - ); - harcodedMetadata = - await this.updateMetadataWithRegionalInformation( - harcodedMetadata - ); - } - - metadataEntity.updateEndpointMetadata(harcodedMetadata, false); - return AuthorityMetadataSource.HARDCODED_VALUES; } else { + // Metadata could not be obtained from the config, cache, network or hardcoded values + this.logger.error( + "Did not find endpoint metadata from network... Metadata could not be obtained from config, cache, network or hardcoded values. Throwing Untrusted Authority Error." + ); throw ClientAuthError.createUnableToGetOpenidConfigError( this.defaultOpenIdConfigurationEndpoint ); @@ -524,7 +589,10 @@ export class Authority { this.correlationId ); - const perfEvent = this.performanceClient?.startMeasurement(PerformanceEvents.AuthorityGetEndpointMetadataFromNetwork, this.correlationId); + const perfEvent = this.performanceClient?.startMeasurement( + PerformanceEvents.AuthorityGetEndpointMetadataFromNetwork, + this.correlationId + ); const options: ImdsOptions = {}; /* @@ -532,8 +600,11 @@ export class Authority { * hardcoded list of metadata */ - const openIdConfigurationEndpoint = this.defaultOpenIdConfigurationEndpoint; - this.logger.verbose(`Authority.getEndpointMetadataFromNetwork: attempting to retrieve OAuth endpoints from ${openIdConfigurationEndpoint}`); + const openIdConfigurationEndpoint = + this.defaultOpenIdConfigurationEndpoint; + this.logger.verbose( + `Authority.getEndpointMetadataFromNetwork: attempting to retrieve OAuth endpoints from ${openIdConfigurationEndpoint}` + ); try { const response = @@ -546,13 +617,23 @@ export class Authority { perfEvent?.endMeasurement({ success: true }); return response.body; } else { - perfEvent?.endMeasurement({ success: false, errorCode: "invalid_response" }); - this.logger.verbose(`Authority.getEndpointMetadataFromNetwork: could not parse response as OpenID configuration`); + perfEvent?.endMeasurement({ + success: false, + errorCode: "invalid_response", + }); + this.logger.verbose( + `Authority.getEndpointMetadataFromNetwork: could not parse response as OpenID configuration` + ); return null; } } catch (e) { - perfEvent?.endMeasurement({ success: false, errorCode: "request_failure" }); - this.logger.verbose(`Authority.getEndpointMetadataFromNetwork: ${e}`); + perfEvent?.endMeasurement({ + success: false, + errorCode: "request_failure", + }); + this.logger.verbose( + `Authority.getEndpointMetadataFromNetwork: ${e}` + ); return null; } } @@ -580,38 +661,49 @@ export class Authority { this.correlationId ); - const userConfiguredAzureRegion = this.authorityOptions.azureRegionConfiguration?.azureRegion; + const userConfiguredAzureRegion = + this.authorityOptions.azureRegionConfiguration?.azureRegion; if (userConfiguredAzureRegion) { - if (userConfiguredAzureRegion !== Constants.AZURE_REGION_AUTO_DISCOVER_FLAG) { - this.regionDiscoveryMetadata.region_outcome = RegionDiscoveryOutcomes.CONFIGURED_NO_AUTO_DETECTION; - this.regionDiscoveryMetadata.region_used = userConfiguredAzureRegion; + if ( + userConfiguredAzureRegion !== + Constants.AZURE_REGION_AUTO_DISCOVER_FLAG + ) { + this.regionDiscoveryMetadata.region_outcome = + RegionDiscoveryOutcomes.CONFIGURED_NO_AUTO_DETECTION; + this.regionDiscoveryMetadata.region_used = + userConfiguredAzureRegion; return Authority.replaceWithRegionalInformation( - metadata, + metadata, userConfiguredAzureRegion ); } - + this.performanceClient?.setPreQueueTime( PerformanceEvents.RegionDiscoveryDetectRegion, this.correlationId ); - - const autodetectedRegionName = await this.regionDiscovery.detectRegion( - this.authorityOptions.azureRegionConfiguration?.environmentRegion, - this.regionDiscoveryMetadata - ); - + + const autodetectedRegionName = + await this.regionDiscovery.detectRegion( + this.authorityOptions.azureRegionConfiguration + ?.environmentRegion, + this.regionDiscoveryMetadata + ); + if (autodetectedRegionName) { - this.regionDiscoveryMetadata.region_outcome = RegionDiscoveryOutcomes.AUTO_DETECTION_REQUESTED_SUCCESSFUL; - this.regionDiscoveryMetadata.region_used = autodetectedRegionName; + this.regionDiscoveryMetadata.region_outcome = + RegionDiscoveryOutcomes.AUTO_DETECTION_REQUESTED_SUCCESSFUL; + this.regionDiscoveryMetadata.region_used = + autodetectedRegionName; return Authority.replaceWithRegionalInformation( - metadata, + metadata, autodetectedRegionName ); } - - this.regionDiscoveryMetadata.region_outcome = RegionDiscoveryOutcomes.AUTO_DETECTION_REQUESTED_FAILED; + + this.regionDiscoveryMetadata.region_outcome = + RegionDiscoveryOutcomes.AUTO_DETECTION_REQUESTED_FAILED; } return metadata; @@ -631,7 +723,7 @@ export class Authority { this.correlationId ); this.logger.verbose( - "Attempting to get cloud discovery metadata in the config" + "Attempting to get cloud discovery metadata from authority configuration" ); this.logger.verbosePii( `Known Authorities: ${ @@ -653,23 +745,47 @@ export class Authority { let metadata = this.getCloudDiscoveryMetadataFromConfig(); if (metadata) { this.logger.verbose( - "Found cloud discovery metadata in the config." + "Found cloud discovery metadata in authority configuration" ); metadataEntity.updateCloudDiscoveryMetadata(metadata, false); return AuthorityMetadataSource.CONFIG; } - // If the cached metadata came from config but that config was not passed to this instance, we must go to the network + // If the cached metadata came from config but that config was not passed to this instance, we must go to hardcoded values this.logger.verbose( - "Did not find cloud discovery metadata in the config... Attempting to get cloud discovery metadata from the cache." + "Did not find cloud discovery metadata in the config... Attempting to get cloud discovery metadata from the hardcoded values." ); + + if (this.options.skipAuthorityMetadataCache) { + this.logger.verbose( + "Skipping hardcoded cloud discovery metadata cache since skipAuthorityMetadataCache is set to true. Attempting to get cloud discovery metadata from the network metadata cache." + ); + } else { + const hardcodedMetadata = + this.getCloudDiscoveryMetadataFromHardcodedValues(); + if (hardcodedMetadata) { + this.logger.verbose( + "Found cloud discovery metadata from hardcoded values." + ); + metadataEntity.updateCloudDiscoveryMetadata( + hardcodedMetadata, + false + ); + return AuthorityMetadataSource.HARDCODED_VALUES; + } + + this.logger.verbose( + "Did not find cloud discovery metadata in hardcoded values... Attempting to get cloud discovery metadata from the network metadata cache." + ); + } + const metadataEntityExpired = metadataEntity.isExpired(); if ( this.isAuthoritySameType(metadataEntity) && metadataEntity.aliasesFromNetwork && !metadataEntityExpired ) { - this.logger.verbose("Found metadata in the cache."); + this.logger.verbose("Found cloud discovery metadata in the cache."); // No need to update return AuthorityMetadataSource.CACHE; } else if (metadataEntityExpired) { @@ -679,11 +795,13 @@ export class Authority { this.logger.verbose( "Did not find cloud discovery metadata in the cache... Attempting to get cloud discovery metadata from the network." ); + this.performanceClient?.setPreQueueTime( PerformanceEvents.AuthorityGetCloudDiscoveryMetadataFromNetwork, this.correlationId ); metadata = await this.getCloudDiscoveryMetadataFromNetwork(); + if (metadata) { this.logger.verbose( "cloud discovery metadata was successfully returned from getCloudDiscoveryMetadataFromNetwork()" @@ -692,25 +810,9 @@ export class Authority { return AuthorityMetadataSource.NETWORK; } - this.logger.verbose( - "Did not find cloud discovery metadata from the network... Attempting to get cloud discovery metadata from hardcoded values." - ); - const harcodedMetadata = - this.getCloudDiscoveryMetadataFromHarcodedValues(); - if (harcodedMetadata && !this.options.skipAuthorityMetadataCache) { - this.logger.verbose( - "Found cloud discovery metadata from hardcoded values." - ); - metadataEntity.updateCloudDiscoveryMetadata( - harcodedMetadata, - false - ); - return AuthorityMetadataSource.HARDCODED_VALUES; - } - // Metadata could not be obtained from the config, cache, network or hardcoded values this.logger.error( - "Did not find cloud discovery metadata from hardcoded values... Metadata could not be obtained from config, cache, network or hardcoded values. Throwing Untrusted Authority Error." + "Did not find cloud discovery metadata from network... Metadata could not be obtained from config, cache, network or hardcoded values. Throwing Untrusted Authority Error." ); throw ClientConfigurationError.createUntrustedAuthorityError(); } @@ -719,11 +821,14 @@ export class Authority { * Parse cloudDiscoveryMetadata config or check knownAuthorities */ private getCloudDiscoveryMetadataFromConfig(): CloudDiscoveryMetadata | null { - // CIAM does not support cloud discovery metadata if (this.authorityType === AuthorityType.Ciam) { - this.logger.verbose("CIAM authorities do not support cloud discovery metadata, generate the aliases from authority host."); - return Authority.createCloudDiscoveryMetadataFromHost(this.hostnameAndPort); + this.logger.verbose( + "CIAM authorities do not support cloud discovery metadata, generate the aliases from authority host." + ); + return Authority.createCloudDiscoveryMetadataFromHost( + this.hostnameAndPort + ); } // Check if network response was provided in config @@ -799,7 +904,6 @@ export class Authority { | CloudInstanceDiscoveryResponse | CloudInstanceDiscoveryErrorResponse >(instanceDiscoveryEndpoint, options); - let typedResponseBody: | CloudInstanceDiscoveryResponse | CloudInstanceDiscoveryErrorResponse; @@ -885,9 +989,16 @@ export class Authority { /** * Get cloud discovery metadata for common authorities */ - private getCloudDiscoveryMetadataFromHarcodedValues(): CloudDiscoveryMetadata | null { + private getCloudDiscoveryMetadataFromHardcodedValues(): CloudDiscoveryMetadata | null { if (this.canonicalAuthority in InstanceDiscoveryMetadata) { - return InstanceDiscoveryMetadata[this.canonicalAuthority]; + const hardcodedMetadataResponse = + InstanceDiscoveryMetadata[this.canonicalAuthority]; + const metadata = + Authority.getCloudDiscoveryMetadataFromNetworkResponse( + hardcodedMetadataResponse.metadata, + this.hostnameAndPort + ); + return metadata; } return null; @@ -1044,27 +1155,29 @@ export class Authority { metadata: OpenIdConfigResponse, azureRegion: string ): OpenIdConfigResponse { - metadata.authorization_endpoint = + const regionalMetadata = { ...metadata }; + regionalMetadata.authorization_endpoint = Authority.buildRegionalAuthorityString( - metadata.authorization_endpoint, + regionalMetadata.authorization_endpoint, azureRegion ); // TODO: Enquire on whether we should leave the query string or remove it before releasing the feature - metadata.token_endpoint = Authority.buildRegionalAuthorityString( - metadata.token_endpoint, - azureRegion, - Constants.REGIONAL_AUTH_NON_MSI_QUERY_STRING - ); + regionalMetadata.token_endpoint = + Authority.buildRegionalAuthorityString( + regionalMetadata.token_endpoint, + azureRegion, + Constants.REGIONAL_AUTH_NON_MSI_QUERY_STRING + ); - if (metadata.end_session_endpoint) { - metadata.end_session_endpoint = + if (regionalMetadata.end_session_endpoint) { + regionalMetadata.end_session_endpoint = Authority.buildRegionalAuthorityString( - metadata.end_session_endpoint, + regionalMetadata.end_session_endpoint, azureRegion ); } - return metadata; + return regionalMetadata; } /** @@ -1077,14 +1190,21 @@ export class Authority { * @param authority */ static transformCIAMAuthority(authority: string): string { - - let ciamAuthority = authority.endsWith(Constants.FORWARD_SLASH) ? authority : `${authority}${Constants.FORWARD_SLASH}`; + let ciamAuthority = authority.endsWith(Constants.FORWARD_SLASH) + ? authority + : `${authority}${Constants.FORWARD_SLASH}`; const authorityUrl = new UrlString(authority); const authorityUrlComponents = authorityUrl.getUrlComponents(); // check if transformation is needed - if (authorityUrlComponents.PathSegments.length === 0 && (authorityUrlComponents.HostNameAndPort.endsWith(Constants.CIAM_AUTH_URL))){ - const tenantIdOrDomain = authorityUrlComponents.HostNameAndPort.split(".")[0]; + if ( + authorityUrlComponents.PathSegments.length === 0 && + authorityUrlComponents.HostNameAndPort.endsWith( + Constants.CIAM_AUTH_URL + ) + ) { + const tenantIdOrDomain = + authorityUrlComponents.HostNameAndPort.split(".")[0]; ciamAuthority = `${ciamAuthority}${tenantIdOrDomain}${Constants.AAD_TENANT_DOMAIN_SUFFIX}`; } diff --git a/lib/msal-common/test/authority/Authority.spec.ts b/lib/msal-common/test/authority/Authority.spec.ts index 9c5e8dc2cc..91edf97fc6 100644 --- a/lib/msal-common/test/authority/Authority.spec.ts +++ b/lib/msal-common/test/authority/Authority.spec.ts @@ -27,6 +27,7 @@ import { AuthorityMetadataEntity } from "../../src/cache/entities/AuthorityMetad import { OpenIdConfigResponse } from "../../src/authority/OpenIdConfigResponse"; import { Logger, LogLevel, UrlString } from "../../src"; import { RegionDiscovery } from "../../src/authority/RegionDiscovery"; +import { InstanceDiscoveryMetadata } from "../../src/authority/AuthorityMetadata"; let mockStorage: MockStorageClass; @@ -35,7 +36,6 @@ const authorityOptions: AuthorityOptions = { knownAuthorities: [Constants.DEFAULT_AUTHORITY_HOST], cloudDiscoveryMetadata: "", authorityMetadata: "", - skipAuthorityMetadataCache: true, }; const loggerOptions = { @@ -389,76 +389,145 @@ describe("Authority.ts Class Unit Tests", () => { for (const [key, value] of Object.entries(response)) { if (typeof response[key] === "string") { // @ts-ignore - response[key] = value.replace('{tenant}', tenant); + response[key] = value.replace("{tenant}", tenant); } } it("Returns correct endpoint for AAD", async () => { - jest.spyOn(Authority.prototype, "getEndpointMetadataFromNetwork").mockResolvedValue(response); + jest.spyOn( + Authority.prototype, + "getEndpointMetadataFromHardcodedValues" + ).mockReturnValue(response); - const authority = new Authority(Constants.DEFAULT_AUTHORITY, networkInterface, mockStorage, authorityOptions, logger); + const authority = new Authority( + Constants.DEFAULT_AUTHORITY, + networkInterface, + mockStorage, + authorityOptions, + logger + ); await authority.resolveEndpointsAsync(); - expect(authority.authorizationEndpoint).toBe(response.authorization_endpoint); + expect(authority.authorizationEndpoint).toBe( + response.authorization_endpoint + ); - const newAuthorityEndpoint = response.authorization_endpoint.replace(tenant, newTenant); - const urlComponents = new UrlString(newAuthorityEndpoint).getUrlComponents(); + const newAuthorityEndpoint = + response.authorization_endpoint.replace(tenant, newTenant); + const urlComponents = new UrlString( + newAuthorityEndpoint + ).getUrlComponents(); // Mimic tenant switching // @ts-ignore - authority.metadata.authorization_endpoint = newAuthorityEndpoint; - jest.spyOn(Authority.prototype, "canonicalAuthorityUrlComponents", "get").mockReturnValue(urlComponents); + authority.metadata.authorization_endpoint = + newAuthorityEndpoint; + jest.spyOn( + Authority.prototype, + "canonicalAuthorityUrlComponents", + "get" + ).mockReturnValue(urlComponents); - expect(authority.authorizationEndpoint).toBe(response.authorization_endpoint.replace(tenant, newTenant)); + expect(authority.authorizationEndpoint).toBe( + response.authorization_endpoint.replace(tenant, newTenant) + ); }); it("Returns correct endpoint for B2C", async () => { - jest.spyOn(Authority.prototype, "getEndpointMetadataFromNetwork").mockResolvedValue(b2cResponse); + jest.spyOn( + Authority.prototype, + "getEndpointMetadataFromNetwork" + ).mockResolvedValue(b2cResponse); const baseAuthority = `https://msidlabb2c.b2clogin.com/tfp/${tenantDomain}/`; - const customAuthority = new Authority(baseAuthority, networkInterface, mockStorage, { - ...authorityOptions, - knownAuthorities: ["msidlabb2c.b2clogin.com"], - }, logger); + const customAuthority = new Authority( + baseAuthority, + networkInterface, + mockStorage, + { + ...authorityOptions, + knownAuthorities: ["msidlabb2c.b2clogin.com"], + }, + logger + ); await customAuthority.resolveEndpointsAsync(); - expect(customAuthority.authorizationEndpoint).toBe(b2cResponse.authorization_endpoint); + expect(customAuthority.authorizationEndpoint).toBe( + b2cResponse.authorization_endpoint + ); - const newAuthorityEndpoint = b2cResponse.authorization_endpoint.replace(tenantDomain, newTenantDomain); - const urlComponents = new UrlString(newAuthorityEndpoint).getUrlComponents(); + const newAuthorityEndpoint = + b2cResponse.authorization_endpoint.replace( + tenantDomain, + newTenantDomain + ); + const urlComponents = new UrlString( + newAuthorityEndpoint + ).getUrlComponents(); // Mimic tenant switching // @ts-ignore - customAuthority.metadata.authorization_endpoint = newAuthorityEndpoint; - jest.spyOn(Authority.prototype, "canonicalAuthorityUrlComponents", "get").mockReturnValue(urlComponents); - - expect(customAuthority.authorizationEndpoint).toBe(b2cResponse.authorization_endpoint.replace(tenantDomain, newTenantDomain)); + customAuthority.metadata.authorization_endpoint = + newAuthorityEndpoint; + jest.spyOn( + Authority.prototype, + "canonicalAuthorityUrlComponents", + "get" + ).mockReturnValue(urlComponents); + + expect(customAuthority.authorizationEndpoint).toBe( + b2cResponse.authorization_endpoint.replace( + tenantDomain, + newTenantDomain + ) + ); }); it("Returns correct endpoint when AAD cached canonical endpoint contains tenant name", async () => { - jest.spyOn(Authority.prototype, "getEndpointMetadataFromNetwork").mockResolvedValue(response); - + jest.spyOn( + Authority.prototype, + "getEndpointMetadataFromNetwork" + ).mockResolvedValue(response); - const customAuthority = new Authority(`https://login.microsoftonline.com/${tenantDomain}/`, networkInterface, mockStorage, authorityOptions, logger); + const customAuthority = new Authority( + `https://login.microsoftonline.com/${tenantDomain}/`, + networkInterface, + mockStorage, + authorityOptions, + logger + ); await customAuthority.resolveEndpointsAsync(); - expect(customAuthority.authorizationEndpoint).toBe(response.authorization_endpoint.replace(tenant, tenantDomain)); + expect(customAuthority.authorizationEndpoint).toBe( + response.authorization_endpoint.replace( + tenant, + tenantDomain + ) + ); - const newAuthorityEndpoint = response.authorization_endpoint.replace(tenant, newTenant); - const urlComponents = new UrlString(newAuthorityEndpoint).getUrlComponents(); + const newAuthorityEndpoint = + response.authorization_endpoint.replace(tenant, newTenant); + const urlComponents = new UrlString( + newAuthorityEndpoint + ).getUrlComponents(); // Mimic tenant switching // @ts-ignore - customAuthority.metadata.authorization_endpoint = newAuthorityEndpoint; - jest.spyOn(Authority.prototype, "canonicalAuthorityUrlComponents", "get").mockReturnValue(urlComponents); + customAuthority.metadata.authorization_endpoint = + newAuthorityEndpoint; + jest.spyOn( + Authority.prototype, + "canonicalAuthorityUrlComponents", + "get" + ).mockReturnValue(urlComponents); - expect(customAuthority.authorizationEndpoint).toBe(response.authorization_endpoint.replace(tenant, newTenant)); + expect(customAuthority.authorizationEndpoint).toBe( + response.authorization_endpoint.replace(tenant, newTenant) + ); }); }); }); - - describe("Regional authorities", () => { const networkInterface: INetworkModule = { sendGetRequestAsync( @@ -603,7 +672,10 @@ describe("Authority.ts Class Unit Tests", () => { ); }; - const regionDiscoverySpy = jest.spyOn(RegionDiscovery.prototype, "detectRegion"); + const regionDiscoverySpy = jest.spyOn( + RegionDiscovery.prototype, + "detectRegion" + ); const authority = new Authority( Constants.DEFAULT_AUTHORITY, @@ -817,6 +889,31 @@ describe("Authority.ts Class Unit Tests", () => { }); describe("Endpoint Metadata", () => { + let getEndpointMetadataFromConfigSpy: jest.SpyInstance; + let getEndpointMetadataFromHarcodedValuesSpy: jest.SpyInstance; + let getEndpointMetadataFromNetworkSpy: jest.SpyInstance; + + beforeEach(() => { + getEndpointMetadataFromConfigSpy = jest.spyOn( + Authority.prototype as any, + "getEndpointMetadataFromConfig" + ); + + getEndpointMetadataFromHarcodedValuesSpy = jest.spyOn( + Authority.prototype as any, + "getEndpointMetadataFromHardcodedValues" + ); + + getEndpointMetadataFromNetworkSpy = jest.spyOn( + Authority.prototype as any, + "getEndpointMetadataFromNetwork" + ); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + it("Gets endpoints from config", async () => { const options = { protocolMode: ProtocolMode.AAD, @@ -891,6 +988,14 @@ describe("Authority.ts Class Unit Tests", () => { false ); } + + expect(getEndpointMetadataFromConfigSpy).toHaveBeenCalled(); + expect( + getEndpointMetadataFromHarcodedValuesSpy + ).not.toHaveBeenCalled(); + expect( + getEndpointMetadataFromNetworkSpy + ).not.toHaveBeenCalled(); }); it("Throws error if authorityMetadata cannot be parsed to json", (done) => { @@ -917,7 +1022,7 @@ describe("Authority.ts Class Unit Tests", () => { }); }); - it("Throws error if authority does not containn end_session_endpoint but calls logout", async () => { + it("Throws error if authority does not contain end_session_endpoint but calls logout", async () => { const authorityJson = { ...DEFAULT_OPENID_CONFIG_RESPONSE.body, end_session_endpoint: undefined, @@ -943,7 +1048,147 @@ describe("Authority.ts Class Unit Tests", () => { ); }); - it("Gets endpoints from cache", async () => { + it("Gets endpoints from hardcoded values", async () => { + const customAuthorityOptions: AuthorityOptions = { + protocolMode: ProtocolMode.AAD, + knownAuthorities: [Constants.DEFAULT_AUTHORITY_HOST], + cloudDiscoveryMetadata: "", + authorityMetadata: "", + skipAuthorityMetadataCache: false, + }; + + networkInterface.sendGetRequestAsync = ( + url: string, + options?: NetworkRequestOptions + ): any => { + return null; + }; + + authority = new Authority( + Constants.DEFAULT_AUTHORITY, + networkInterface, + mockStorage, + customAuthorityOptions, + logger + ); + + await authority.resolveEndpointsAsync(); + + expect(authority.discoveryComplete()).toBe(true); + expect(authority.authorizationEndpoint).toBe( + DEFAULT_OPENID_CONFIG_RESPONSE.body.authorization_endpoint.replace( + "{tenant}", + "common" + ) + ); + expect(authority.tokenEndpoint).toBe( + DEFAULT_OPENID_CONFIG_RESPONSE.body.token_endpoint.replace( + "{tenant}", + "common" + ) + ); + expect(authority.deviceCodeEndpoint).toBe( + authority.tokenEndpoint.replace("/token", "/devicecode") + ); + expect(authority.endSessionEndpoint).toBe( + DEFAULT_OPENID_CONFIG_RESPONSE.body.end_session_endpoint.replace( + "{tenant}", + "common" + ) + ); + expect(authority.selfSignedJwtAudience).toBe( + DEFAULT_OPENID_CONFIG_RESPONSE.body.issuer.replace( + "{tenant}", + "common" + ) + ); + + expect(getEndpointMetadataFromConfigSpy).toHaveBeenCalled(); + expect( + getEndpointMetadataFromHarcodedValuesSpy + ).toHaveBeenCalled(); + expect( + getEndpointMetadataFromNetworkSpy + ).not.toHaveBeenCalled(); + }); + + it("Gets endpoints from hardcoded values with regional information", async () => { + const customAuthorityOptions: AuthorityOptions = { + protocolMode: ProtocolMode.AAD, + knownAuthorities: [Constants.DEFAULT_AUTHORITY_HOST], + cloudDiscoveryMetadata: "", + authorityMetadata: "", + skipAuthorityMetadataCache: false, + azureRegionConfiguration: { + azureRegion: "westus2", + environmentRegion: undefined, + }, + }; + + const expectedHardcodedRegionalValues = { + authorization_endpoint: + "https://westus2.login.microsoft.com/common/oauth2/v2.0/authorize/", + canonical_authority: + "https://login.microsoftonline.com/common/", + end_session_endpoint: + "https://westus2.login.microsoft.com/common/oauth2/v2.0/logout/", + endpointsFromNetwork: false, + issuer: "https://login.microsoftonline.com/{tenantid}/v2.0", + jwks_uri: + "https://login.microsoftonline.com/common/discovery/v2.0/keys", + token_endpoint: + "https://westus2.login.microsoft.com/common/oauth2/v2.0/token/?allowestsrnonmsi=true", + }; + + networkInterface.sendGetRequestAsync = ( + url: string, + options?: NetworkRequestOptions + ): any => { + return null; + }; + + authority = new Authority( + Constants.DEFAULT_AUTHORITY, + networkInterface, + mockStorage, + customAuthorityOptions, + logger + ); + await authority.resolveEndpointsAsync(); + + expect(authority.discoveryComplete()).toBe(true); + expect(authority.authorizationEndpoint).toBe( + expectedHardcodedRegionalValues.authorization_endpoint + ); + expect(authority.tokenEndpoint).toBe( + expectedHardcodedRegionalValues.token_endpoint + ); + expect(authority.deviceCodeEndpoint).toBe( + expectedHardcodedRegionalValues.token_endpoint.replace( + "/token", + "/devicecode" + ) + ); + expect(authority.endSessionEndpoint).toBe( + expectedHardcodedRegionalValues.end_session_endpoint + ); + expect(authority.selfSignedJwtAudience).toBe( + expectedHardcodedRegionalValues.issuer.replace( + "{tenantid}", + "common" + ) + ); + + expect(getEndpointMetadataFromConfigSpy).toHaveBeenCalled(); + expect( + getEndpointMetadataFromHarcodedValuesSpy + ).toHaveBeenCalled(); + expect( + getEndpointMetadataFromNetworkSpy + ).not.toHaveBeenCalled(); + }); + + it("Gets endpoints from cache if not present in configuration or hardcoded metadata", async () => { const key = `authority-metadata-${TEST_CONFIG.MSAL_CLIENT_ID}-${Constants.DEFAULT_AUTHORITY_HOST}`; const value = new AuthorityMetadataEntity(); value.updateCloudDiscoveryMetadata( @@ -964,6 +1209,9 @@ describe("Authority.ts Class Unit Tests", () => { authorityOptions, logger ); + + // Force hardcoded metadata to return null + getEndpointMetadataFromHarcodedValuesSpy.mockReturnValue(null); await authority.resolveEndpointsAsync(); expect(authority.discoveryComplete()).toBe(true); @@ -1021,9 +1269,17 @@ describe("Authority.ts Class Unit Tests", () => { true ); } + + expect(getEndpointMetadataFromConfigSpy).toHaveBeenCalled(); + expect( + getEndpointMetadataFromHarcodedValuesSpy + ).toHaveBeenCalled(); + expect( + getEndpointMetadataFromNetworkSpy + ).not.toHaveBeenCalled(); }); - it("Gets endpoints from network if cached metadata is expired", async () => { + it("Gets endpoints from cache skipping hardcoded metadata if skipAuthorityMetadataCache is set to true", async () => { const key = `authority-metadata-${TEST_CONFIG.MSAL_CLIENT_ID}-${Constants.DEFAULT_AUTHORITY_HOST}`; const value = new AuthorityMetadataEntity(); value.updateCloudDiscoveryMetadata( @@ -1037,24 +1293,16 @@ describe("Authority.ts Class Unit Tests", () => { value.updateCanonicalAuthority(Constants.DEFAULT_AUTHORITY); mockStorage.setAuthorityMetadata(key, value); - jest.spyOn( - AuthorityMetadataEntity.prototype, - "isExpired" - ).mockReturnValue(true); - - networkInterface.sendGetRequestAsync = ( - url: string, - options?: NetworkRequestOptions - ): any => { - return DEFAULT_OPENID_CONFIG_RESPONSE; - }; authority = new Authority( Constants.DEFAULT_AUTHORITY, networkInterface, mockStorage, - authorityOptions, + { ...authorityOptions, skipAuthorityMetadataCache: true }, logger ); + + // Force hardcoded metadata to return null + getEndpointMetadataFromHarcodedValuesSpy.mockReturnValue(null); await authority.resolveEndpointsAsync(); expect(authority.discoveryComplete()).toBe(true); @@ -1112,9 +1360,35 @@ describe("Authority.ts Class Unit Tests", () => { true ); } + + expect(getEndpointMetadataFromConfigSpy).toHaveBeenCalled(); + expect( + getEndpointMetadataFromHarcodedValuesSpy + ).not.toHaveBeenCalled(); + expect( + getEndpointMetadataFromNetworkSpy + ).not.toHaveBeenCalled(); }); - it("Gets endpoints from network", async () => { + it("Gets endpoints from network if cached metadata is expired and metadata was not included in configuration or hardcoded values", async () => { + const key = `authority-metadata-${TEST_CONFIG.MSAL_CLIENT_ID}-${Constants.DEFAULT_AUTHORITY_HOST}`; + const value = new AuthorityMetadataEntity(); + value.updateCloudDiscoveryMetadata( + DEFAULT_TENANT_DISCOVERY_RESPONSE.body.metadata[0], + true + ); + value.updateEndpointMetadata( + DEFAULT_OPENID_CONFIG_RESPONSE.body, + true + ); + value.updateCanonicalAuthority(Constants.DEFAULT_AUTHORITY); + mockStorage.setAuthorityMetadata(key, value); + + jest.spyOn( + AuthorityMetadataEntity.prototype, + "isExpired" + ).mockReturnValue(true); + networkInterface.sendGetRequestAsync = ( url: string, options?: NetworkRequestOptions @@ -1128,6 +1402,10 @@ describe("Authority.ts Class Unit Tests", () => { authorityOptions, logger ); + + // Force hardcoded metadata to return null + getEndpointMetadataFromHarcodedValuesSpy.mockReturnValue(null); + await authority.resolveEndpointsAsync(); expect(authority.discoveryComplete()).toBe(true); @@ -1160,7 +1438,6 @@ describe("Authority.ts Class Unit Tests", () => { ); // Test that the metadata is cached - const key = `authority-metadata-${TEST_CONFIG.MSAL_CLIENT_ID}-${Constants.DEFAULT_AUTHORITY_HOST}`; const cachedAuthorityMetadata = mockStorage.getAuthorityMetadata(key); if (!cachedAuthorityMetadata) { @@ -1186,31 +1463,30 @@ describe("Authority.ts Class Unit Tests", () => { true ); } - }); - it("Gets endpoints from hardcoded values", async () => { - const customAuthorityOptions: AuthorityOptions = { - protocolMode: ProtocolMode.AAD, - knownAuthorities: [Constants.DEFAULT_AUTHORITY_HOST], - cloudDiscoveryMetadata: "", - authorityMetadata: "", - skipAuthorityMetadataCache: false, - }; + expect(getEndpointMetadataFromConfigSpy).toHaveBeenCalled(); + expect( + getEndpointMetadataFromHarcodedValuesSpy + ).toHaveBeenCalled(); + expect(getEndpointMetadataFromNetworkSpy).toHaveBeenCalled(); + }); + it("Gets endpoints from network", async () => { networkInterface.sendGetRequestAsync = ( url: string, options?: NetworkRequestOptions ): any => { - return null; + return DEFAULT_OPENID_CONFIG_RESPONSE; }; - authority = new Authority( Constants.DEFAULT_AUTHORITY, networkInterface, mockStorage, - customAuthorityOptions, + authorityOptions, logger ); + getEndpointMetadataFromHarcodedValuesSpy.mockReturnValue(null); + await authority.resolveEndpointsAsync(); expect(authority.discoveryComplete()).toBe(true); @@ -1250,134 +1526,31 @@ describe("Authority.ts Class Unit Tests", () => { throw Error("Cached AuthorityMetadata should not be null!"); } else { expect(cachedAuthorityMetadata.authorization_endpoint).toBe( - DEFAULT_OPENID_CONFIG_RESPONSE.body.authorization_endpoint.replace( - "{tenant}", - "common" - ) + DEFAULT_OPENID_CONFIG_RESPONSE.body + .authorization_endpoint ); expect(cachedAuthorityMetadata.token_endpoint).toBe( - DEFAULT_OPENID_CONFIG_RESPONSE.body.token_endpoint.replace( - "{tenant}", - "common" - ) + DEFAULT_OPENID_CONFIG_RESPONSE.body.token_endpoint ); expect(cachedAuthorityMetadata.end_session_endpoint).toBe( - DEFAULT_OPENID_CONFIG_RESPONSE.body.end_session_endpoint.replace( - "{tenant}", - "common" - ) + DEFAULT_OPENID_CONFIG_RESPONSE.body.end_session_endpoint ); expect(cachedAuthorityMetadata.issuer).toBe( - DEFAULT_OPENID_CONFIG_RESPONSE.body.issuer.replace( - "{tenant}", - "{tenantid}" - ) + DEFAULT_OPENID_CONFIG_RESPONSE.body.issuer ); expect(cachedAuthorityMetadata.jwks_uri).toBe( - DEFAULT_OPENID_CONFIG_RESPONSE.body.jwks_uri.replace( - "{tenant}", - "common" - ) + DEFAULT_OPENID_CONFIG_RESPONSE.body.jwks_uri ); expect(cachedAuthorityMetadata.endpointsFromNetwork).toBe( - false + true ); } - }); - - it("Gets endpoints from hardcoded values with regional information", async () => { - const customAuthorityOptions: AuthorityOptions = { - protocolMode: ProtocolMode.AAD, - knownAuthorities: [Constants.DEFAULT_AUTHORITY_HOST], - cloudDiscoveryMetadata: "", - authorityMetadata: "", - skipAuthorityMetadataCache: false, - azureRegionConfiguration: { - azureRegion: "westus2", - environmentRegion: undefined, - }, - }; - - const expectedHardcodedRegionalValues = { - authorization_endpoint: - "https://westus2.login.microsoft.com/common/oauth2/v2.0/authorize/", - canonical_authority: - "https://login.microsoftonline.com/common/", - end_session_endpoint: - "https://westus2.login.microsoft.com/common/oauth2/v2.0/logout/", - endpointsFromNetwork: false, - issuer: "https://login.microsoftonline.com/{tenantid}/v2.0", - jwks_uri: - "https://login.microsoftonline.com/common/discovery/v2.0/keys", - token_endpoint: - "https://westus2.login.microsoft.com/common/oauth2/v2.0/token/?allowestsrnonmsi=true", - }; - - networkInterface.sendGetRequestAsync = ( - url: string, - options?: NetworkRequestOptions - ): any => { - return null; - }; - - authority = new Authority( - Constants.DEFAULT_AUTHORITY, - networkInterface, - mockStorage, - customAuthorityOptions, - logger - ); - await authority.resolveEndpointsAsync(); - - expect(authority.discoveryComplete()).toBe(true); - expect(authority.authorizationEndpoint).toBe( - expectedHardcodedRegionalValues.authorization_endpoint - ); - expect(authority.tokenEndpoint).toBe( - expectedHardcodedRegionalValues.token_endpoint - ); - expect(authority.deviceCodeEndpoint).toBe( - expectedHardcodedRegionalValues.token_endpoint.replace( - "/token", - "/devicecode" - ) - ); - expect(authority.endSessionEndpoint).toBe( - expectedHardcodedRegionalValues.end_session_endpoint - ); - expect(authority.selfSignedJwtAudience).toBe( - expectedHardcodedRegionalValues.issuer.replace( - "{tenantid}", - "common" - ) - ); - // Test that the metadata is cached - const key = `authority-metadata-${TEST_CONFIG.MSAL_CLIENT_ID}-${Constants.DEFAULT_AUTHORITY_HOST}`; - const cachedAuthorityMetadata = - mockStorage.getAuthorityMetadata(key); - if (!cachedAuthorityMetadata) { - throw Error("Cached AuthorityMetadata should not be null!"); - } else { - expect(cachedAuthorityMetadata.authorization_endpoint).toBe( - expectedHardcodedRegionalValues.authorization_endpoint - ); - expect(cachedAuthorityMetadata.token_endpoint).toBe( - expectedHardcodedRegionalValues.token_endpoint - ); - expect(cachedAuthorityMetadata.end_session_endpoint).toBe( - expectedHardcodedRegionalValues.end_session_endpoint - ); - expect(cachedAuthorityMetadata.issuer).toBe( - expectedHardcodedRegionalValues.issuer - ); - expect(cachedAuthorityMetadata.jwks_uri).toBe( - expectedHardcodedRegionalValues.jwks_uri - ); - expect(cachedAuthorityMetadata.endpointsFromNetwork).toBe( - false - ); - } + expect(getEndpointMetadataFromConfigSpy).toHaveBeenCalled(); + expect( + getEndpointMetadataFromHarcodedValuesSpy + ).toHaveBeenCalled(); + expect(getEndpointMetadataFromNetworkSpy).toHaveBeenCalled(); }); it("Throws error if openid-configuration network call fails", (done) => { @@ -1391,7 +1564,7 @@ describe("Authority.ts Class Unit Tests", () => { Constants.DEFAULT_AUTHORITY, networkInterface, mockStorage, - authorityOptions, + { ...authorityOptions, skipAuthorityMetadataCache: true }, logger ); authority.resolveEndpointsAsync().catch((e) => { @@ -1463,15 +1636,24 @@ describe("Authority.ts Class Unit Tests", () => { } }); - it("Sets instance metadata from cloudDiscoveryMetadata config & change canonicalAuthority to preferred_network", async () => { + it("Correctly sets instance metadata from cloudDiscoveryMetadata config and changes canonicalAuthority to preferred_network", async () => { + const tenantDiscoveryResponseBody = + DEFAULT_TENANT_DISCOVERY_RESPONSE.body; + + const expectedCloudDiscoveryMetadata = + tenantDiscoveryResponseBody.metadata[0]; + + const configAliases = expectedCloudDiscoveryMetadata.aliases; + const authorityOptions: AuthorityOptions = { protocolMode: ProtocolMode.AAD, knownAuthorities: [], cloudDiscoveryMetadata: JSON.stringify( - DEFAULT_TENANT_DISCOVERY_RESPONSE.body + tenantDiscoveryResponseBody ), authorityMetadata: "", }; + networkInterface.sendGetRequestAsync = ( url: string, options?: NetworkRequestOptions @@ -1486,142 +1668,47 @@ describe("Authority.ts Class Unit Tests", () => { authorityOptions, logger ); + await authority.resolveEndpointsAsync(); - expect(authority.isAlias("login.microsoftonline.com")).toBe( - true + expect(authority.isAlias(configAliases[0])).toBe(true); + expect(authority.isAlias(configAliases[1])).toBe(true); + expect(authority.isAlias(configAliases[2])).toBe(true); + expect(authority.isAlias(configAliases[3])).toBe(true); + + expect(authority.getPreferredCache()).toBe( + expectedCloudDiscoveryMetadata.preferred_cache ); - expect(authority.isAlias("login.windows.net")).toBe(true); - expect(authority.isAlias("sts.windows.net")).toBe(true); - expect(authority.getPreferredCache()).toBe("sts.windows.net"); expect( - authority.canonicalAuthority.includes("login.windows.net") + authority.canonicalAuthority.includes( + expectedCloudDiscoveryMetadata.preferred_network + ) ).toBe(true); - - // Test that the metadata is cached - const key = `authority-metadata-${TEST_CONFIG.MSAL_CLIENT_ID}-sts.windows.net`; - const cachedAuthorityMetadata = - mockStorage.getAuthorityMetadata(key); - if (!cachedAuthorityMetadata) { - throw Error("Cached AuthorityMetadata should not be null!"); - } else { - expect(cachedAuthorityMetadata.aliases).toContain( - "login.microsoftonline.com" - ); - expect(cachedAuthorityMetadata.aliases).toContain( - "login.windows.net" - ); - expect(cachedAuthorityMetadata.aliases).toContain( - "sts.windows.net" - ); - expect(cachedAuthorityMetadata.preferred_cache).toBe( - "sts.windows.net" - ); - expect(cachedAuthorityMetadata.preferred_network).toBe( - "login.windows.net" - ); - expect(cachedAuthorityMetadata.aliasesFromNetwork).toBe( - false - ); - } }); - it("Sets instance metadata from cache", async () => { - const authorityOptions: AuthorityOptions = { - protocolMode: ProtocolMode.AAD, - knownAuthorities: [], - cloudDiscoveryMetadata: "", - authorityMetadata: "", - }; - - const key = `authority-metadata-${TEST_CONFIG.MSAL_CLIENT_ID}-sts.windows.net`; - const value = new AuthorityMetadataEntity(); - value.updateCloudDiscoveryMetadata( - DEFAULT_TENANT_DISCOVERY_RESPONSE.body.metadata[0], - true - ); - value.updateCanonicalAuthority(Constants.DEFAULT_AUTHORITY); - mockStorage.setAuthorityMetadata(key, value); - jest.spyOn( - Authority.prototype, - "updateEndpointMetadata" - ).mockResolvedValue("cache"); - authority = new Authority( - Constants.DEFAULT_AUTHORITY, - networkInterface, - mockStorage, - authorityOptions, - logger - ); + it("Correctly caches instance metadata from configuration", async () => { + const tenantDiscoveryResponseBody = + DEFAULT_TENANT_DISCOVERY_RESPONSE.body; - await authority.resolveEndpointsAsync(); - expect(authority.isAlias("login.microsoftonline.com")).toBe( - true - ); - expect(authority.isAlias("login.windows.net")).toBe(true); - expect(authority.isAlias("sts.windows.net")).toBe(true); - expect(authority.getPreferredCache()).toBe("sts.windows.net"); - expect( - authority.canonicalAuthority.includes("login.windows.net") - ).toBe(true); + const expectedCloudDiscoveryMetadata = + tenantDiscoveryResponseBody.metadata[0]; - // Test that the metadata is cached - const cachedAuthorityMetadata = - mockStorage.getAuthorityMetadata(key); - if (!cachedAuthorityMetadata) { - throw Error("Cached AuthorityMetadata should not be null!"); - } else { - expect(cachedAuthorityMetadata.aliases).toContain( - "login.microsoftonline.com" - ); - expect(cachedAuthorityMetadata.aliases).toContain( - "login.windows.net" - ); - expect(cachedAuthorityMetadata.aliases).toContain( - "sts.windows.net" - ); - expect(cachedAuthorityMetadata.preferred_cache).toBe( - "sts.windows.net" - ); - expect(cachedAuthorityMetadata.preferred_network).toBe( - "login.windows.net" - ); - expect(cachedAuthorityMetadata.aliasesFromNetwork).toBe( - true - ); - } - }); + const configAliases = expectedCloudDiscoveryMetadata.aliases; - it("Sets instance metadata from network if cached metadata is expired", async () => { const authorityOptions: AuthorityOptions = { protocolMode: ProtocolMode.AAD, knownAuthorities: [], - cloudDiscoveryMetadata: "", + cloudDiscoveryMetadata: JSON.stringify( + tenantDiscoveryResponseBody + ), authorityMetadata: "", }; - - const key = `authority-metadata-${TEST_CONFIG.MSAL_CLIENT_ID}-sts.windows.net`; - const value = new AuthorityMetadataEntity(); - value.updateCloudDiscoveryMetadata( - DEFAULT_TENANT_DISCOVERY_RESPONSE.body.metadata[0], - true - ); - value.updateCanonicalAuthority(Constants.DEFAULT_AUTHORITY); - mockStorage.setAuthorityMetadata(key, value); - jest.spyOn( - AuthorityMetadataEntity.prototype, - "isExpired" - ).mockReturnValue(true); - jest.spyOn( - Authority.prototype, - "updateEndpointMetadata" - ).mockResolvedValue("cache"); - networkInterface.sendGetRequestAsync = ( url: string, options?: NetworkRequestOptions ): any => { - return DEFAULT_TENANT_DISCOVERY_RESPONSE; + return DEFAULT_OPENID_CONFIG_RESPONSE; }; + authority = new Authority( Constants.DEFAULT_AUTHORITY, networkInterface, @@ -1629,229 +1716,641 @@ describe("Authority.ts Class Unit Tests", () => { authorityOptions, logger ); - await authority.resolveEndpointsAsync(); - expect(authority.isAlias("login.microsoftonline.com")).toBe( - true - ); - expect(authority.isAlias("login.windows.net")).toBe(true); - expect(authority.isAlias("sts.windows.net")).toBe(true); - expect(authority.getPreferredCache()).toBe("sts.windows.net"); - expect( - authority.canonicalAuthority.includes("login.windows.net") - ).toBe(true); - // Test that the metadata is cached + const key = `authority-metadata-${TEST_CONFIG.MSAL_CLIENT_ID}-sts.windows.net`; const cachedAuthorityMetadata = mockStorage.getAuthorityMetadata(key); if (!cachedAuthorityMetadata) { throw Error("Cached AuthorityMetadata should not be null!"); } else { expect(cachedAuthorityMetadata.aliases).toContain( - "login.microsoftonline.com" + configAliases[0] ); expect(cachedAuthorityMetadata.aliases).toContain( - "login.windows.net" + configAliases[1] ); expect(cachedAuthorityMetadata.aliases).toContain( - "sts.windows.net" + configAliases[2] + ); + expect(cachedAuthorityMetadata.aliases).toContain( + configAliases[3] ); expect(cachedAuthorityMetadata.preferred_cache).toBe( - "sts.windows.net" + expectedCloudDiscoveryMetadata.preferred_cache ); expect(cachedAuthorityMetadata.preferred_network).toBe( - "login.windows.net" + expectedCloudDiscoveryMetadata.preferred_network ); expect(cachedAuthorityMetadata.aliasesFromNetwork).toBe( - true + false ); } }); - it("Sets instance metadata from network", async () => { - const authorityOptions: AuthorityOptions = { - protocolMode: ProtocolMode.AAD, - knownAuthorities: [], - cloudDiscoveryMetadata: "", - authorityMetadata: "", - }; - networkInterface.sendGetRequestAsync = ( - url: string, - options?: NetworkRequestOptions - ): any => { - return DEFAULT_TENANT_DISCOVERY_RESPONSE; - }; - jest.spyOn( - Authority.prototype, - "updateEndpointMetadata" - ).mockResolvedValue("cache"); - authority = new Authority( - Constants.DEFAULT_AUTHORITY, - networkInterface, - mockStorage, - authorityOptions, - logger - ); + describe("Metadata sources", () => { + let getCloudDiscoveryMetadataFromConfigSpy: jest.SpyInstance; + let getCloudDiscoveryMetadataFromHarcodedValuesSpy: jest.SpyInstance; + let getCloudDiscoveryMetadataFromNetworkSpy: jest.SpyInstance; - await authority.resolveEndpointsAsync(); - expect(authority.isAlias("login.microsoftonline.com")).toBe( - true - ); - expect(authority.isAlias("login.windows.net")).toBe(true); - expect(authority.isAlias("sts.windows.net")).toBe(true); - expect(authority.getPreferredCache()).toBe("sts.windows.net"); - expect( - authority.canonicalAuthority.includes("login.windows.net") - ).toBe(true); + beforeEach(() => { + getCloudDiscoveryMetadataFromConfigSpy = jest.spyOn( + Authority.prototype as any, + "getCloudDiscoveryMetadataFromConfig" + ); - // Test that the metadata is cached - const key = `authority-metadata-${TEST_CONFIG.MSAL_CLIENT_ID}-sts.windows.net`; - const cachedAuthorityMetadata = - mockStorage.getAuthorityMetadata(key); - if (!cachedAuthorityMetadata) { - throw Error("Cached AuthorityMetadata should not be null!"); - } else { - expect(cachedAuthorityMetadata.aliases).toContain( - "login.microsoftonline.com" + getCloudDiscoveryMetadataFromHarcodedValuesSpy = jest.spyOn( + Authority.prototype as any, + "getCloudDiscoveryMetadataFromHardcodedValues" ); - expect(cachedAuthorityMetadata.aliases).toContain( - "login.windows.net" + + getCloudDiscoveryMetadataFromNetworkSpy = jest.spyOn( + Authority.prototype as any, + "getCloudDiscoveryMetadataFromNetwork" ); - expect(cachedAuthorityMetadata.aliases).toContain( - "sts.windows.net" + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + /** + * Order of precedence for cloud discovery metadata: + * 1. Metadata passed in as authorityMetadata config + * 2. Hardcoded Metadata + * 3. Cached metadata previously obtained from network + * 4. Network call to instance discovery endpoint + */ + it("Sets instance metadata from cloudDiscoveryMetadata config source", async () => { + const tenantDiscoveryResponseBody = + DEFAULT_TENANT_DISCOVERY_RESPONSE.body; + + const expectedCloudDiscoveryMetadata = + tenantDiscoveryResponseBody.metadata[0]; + + const configAliases = + expectedCloudDiscoveryMetadata.aliases; + + const authorityOptions: AuthorityOptions = { + protocolMode: ProtocolMode.AAD, + knownAuthorities: [], + cloudDiscoveryMetadata: JSON.stringify( + tenantDiscoveryResponseBody + ), + authorityMetadata: "", + }; + networkInterface.sendGetRequestAsync = ( + url: string, + options?: NetworkRequestOptions + ): any => { + return DEFAULT_OPENID_CONFIG_RESPONSE; + }; + + authority = new Authority( + Constants.DEFAULT_AUTHORITY, + networkInterface, + mockStorage, + authorityOptions, + logger ); - expect(cachedAuthorityMetadata.preferred_cache).toBe( - "sts.windows.net" + + await authority.resolveEndpointsAsync(); + expect( + getCloudDiscoveryMetadataFromConfigSpy + ).toHaveBeenCalled(); + expect( + getCloudDiscoveryMetadataFromHarcodedValuesSpy + ).not.toHaveBeenCalled(); + expect( + getCloudDiscoveryMetadataFromNetworkSpy + ).not.toHaveBeenCalled(); + + expect(authority.isAlias(configAliases[0])).toBe(true); + expect(authority.isAlias(configAliases[1])).toBe(true); + expect(authority.isAlias(configAliases[2])).toBe(true); + expect(authority.isAlias(configAliases[3])).toBe(true); + + expect(authority.getPreferredCache()).toBe( + expectedCloudDiscoveryMetadata.preferred_cache ); - expect(cachedAuthorityMetadata.preferred_network).toBe( - "login.windows.net" + expect( + authority.canonicalAuthority.includes( + expectedCloudDiscoveryMetadata.preferred_network + ) + ).toBe(true); + }); + + it("sets instance metadata from hardcoded values if not present in config", async () => { + const authorityOptions: AuthorityOptions = { + protocolMode: ProtocolMode.AAD, + knownAuthorities: [], + cloudDiscoveryMetadata: "", + authorityMetadata: "", + }; + + networkInterface.sendGetRequestAsync = ( + url: string, + options?: NetworkRequestOptions + ): any => { + return DEFAULT_OPENID_CONFIG_RESPONSE; + }; + + authority = new Authority( + Constants.DEFAULT_AUTHORITY, + networkInterface, + mockStorage, + authorityOptions, + logger ); - expect(cachedAuthorityMetadata.aliasesFromNetwork).toBe( + + const hardcodedCloudDiscoveryMetadata = + InstanceDiscoveryMetadata[Constants.DEFAULT_AUTHORITY]; + + const expectedCloudDiscoveryMetadata = + hardcodedCloudDiscoveryMetadata.metadata[0]; + + const configAliases = + expectedCloudDiscoveryMetadata.aliases; + + await authority.resolveEndpointsAsync(); + expect(authority.isAlias(configAliases[0])).toBe(true); + expect(authority.isAlias(configAliases[1])).toBe(true); + expect(authority.isAlias(configAliases[2])).toBe(true); + expect(authority.getPreferredCache()).toBe( + expectedCloudDiscoveryMetadata.preferred_cache + ); + expect( + authority.canonicalAuthority.includes( + expectedCloudDiscoveryMetadata.preferred_network + ) + ).toBe(true); + + expect( + getCloudDiscoveryMetadataFromConfigSpy + ).toHaveBeenCalled(); + expect( + getCloudDiscoveryMetadataFromHarcodedValuesSpy + ).toHaveBeenCalled(); + expect( + getCloudDiscoveryMetadataFromNetworkSpy + ).not.toHaveBeenCalled(); + }); + + it("Sets instance metadata from cache skipping hardcoded values if skipAuthorityMetadataCache is set to true", async () => { + const authorityOptions: AuthorityOptions = { + protocolMode: ProtocolMode.AAD, + knownAuthorities: [], + cloudDiscoveryMetadata: "", + authorityMetadata: "", + }; + + const tenantDiscoveryResponseBody = + DEFAULT_TENANT_DISCOVERY_RESPONSE.body; + + const expectedCloudDiscoveryMetadata = + tenantDiscoveryResponseBody.metadata[0]; + + const configAliases = + expectedCloudDiscoveryMetadata.aliases; + + const key = `authority-metadata-${TEST_CONFIG.MSAL_CLIENT_ID}-sts.windows.net`; + const value = new AuthorityMetadataEntity(); + value.updateCloudDiscoveryMetadata( + expectedCloudDiscoveryMetadata, true ); - } - }); + value.updateCanonicalAuthority(Constants.DEFAULT_AUTHORITY); + mockStorage.setAuthorityMetadata(key, value); + jest.spyOn( + Authority.prototype, + "updateEndpointMetadata" + ).mockResolvedValue("cache"); + authority = new Authority( + Constants.DEFAULT_AUTHORITY, + networkInterface, + mockStorage, + { + ...authorityOptions, + skipAuthorityMetadataCache: true, + }, + logger + ); - it("Sets metadata from host if network call succeeds but does not explicitly include the host", async () => { - const authorityOptions: AuthorityOptions = { - protocolMode: ProtocolMode.AAD, - knownAuthorities: [], - cloudDiscoveryMetadata: "", - authorityMetadata: "", - }; - networkInterface.sendGetRequestAsync = ( - url: string, - options?: NetworkRequestOptions - ): any => { - return DEFAULT_TENANT_DISCOVERY_RESPONSE; - }; - jest.spyOn( - Authority.prototype, - "updateEndpointMetadata" - ).mockResolvedValue("cache"); - authority = new Authority( - "https://custom-domain.microsoft.com", - networkInterface, - mockStorage, - authorityOptions, - logger - ); + getCloudDiscoveryMetadataFromHarcodedValuesSpy.mockReturnValue( + null + ); - await authority.resolveEndpointsAsync(); - expect(authority.isAlias("custom-domain.microsoft.com")).toBe( - true - ); - expect(authority.getPreferredCache()).toBe( - "custom-domain.microsoft.com" - ); - expect( - authority.canonicalAuthority.includes( - "custom-domain.microsoft.com" - ) - ); + await authority.resolveEndpointsAsync(); + expect(authority.isAlias(configAliases[0])).toBe(true); + expect(authority.isAlias(configAliases[1])).toBe(true); + expect(authority.isAlias(configAliases[2])).toBe(true); + expect(authority.getPreferredCache()).toBe( + expectedCloudDiscoveryMetadata.preferred_cache + ); + expect( + authority.canonicalAuthority.includes( + expectedCloudDiscoveryMetadata.preferred_network + ) + ).toBe(true); - // Test that the metadata is cached - const key = `authority-metadata-${TEST_CONFIG.MSAL_CLIENT_ID}-custom-domain.microsoft.com`; - const cachedAuthorityMetadata = - mockStorage.getAuthorityMetadata(key); - if (!cachedAuthorityMetadata) { - throw Error("Cached AuthorityMetadata should not be null!"); - } else { - expect(cachedAuthorityMetadata.aliases).toContain( - "custom-domain.microsoft.com" + // Test that the metadata is cached + const cachedAuthorityMetadata = + mockStorage.getAuthorityMetadata(key); + if (!cachedAuthorityMetadata) { + throw Error( + "Cached AuthorityMetadata should not be null!" + ); + } else { + expect(cachedAuthorityMetadata.aliases).toContain( + configAliases[0] + ); + expect(cachedAuthorityMetadata.aliases).toContain( + configAliases[1] + ); + expect(cachedAuthorityMetadata.aliases).toContain( + configAliases[2] + ); + expect(cachedAuthorityMetadata.preferred_cache).toBe( + expectedCloudDiscoveryMetadata.preferred_cache + ); + expect(cachedAuthorityMetadata.preferred_network).toBe( + expectedCloudDiscoveryMetadata.preferred_network + ); + expect(cachedAuthorityMetadata.aliasesFromNetwork).toBe( + true + ); + } + + expect( + getCloudDiscoveryMetadataFromConfigSpy + ).toHaveBeenCalled(); + expect( + getCloudDiscoveryMetadataFromHarcodedValuesSpy + ).not.toHaveBeenCalled(); + expect( + getCloudDiscoveryMetadataFromNetworkSpy + ).not.toHaveBeenCalled(); + }); + + it("Sets instance metadata from cache when not present in configuration or hardcoded values", async () => { + const authorityOptions: AuthorityOptions = { + protocolMode: ProtocolMode.AAD, + knownAuthorities: [], + cloudDiscoveryMetadata: "", + authorityMetadata: "", + }; + + const tenantDiscoveryResponseBody = + DEFAULT_TENANT_DISCOVERY_RESPONSE.body; + + const expectedCloudDiscoveryMetadata = + tenantDiscoveryResponseBody.metadata[0]; + + const configAliases = + expectedCloudDiscoveryMetadata.aliases; + + const key = `authority-metadata-${TEST_CONFIG.MSAL_CLIENT_ID}-sts.windows.net`; + const value = new AuthorityMetadataEntity(); + value.updateCloudDiscoveryMetadata( + expectedCloudDiscoveryMetadata, + true ); - expect(cachedAuthorityMetadata.preferred_cache).toBe( - "custom-domain.microsoft.com" + value.updateCanonicalAuthority(Constants.DEFAULT_AUTHORITY); + mockStorage.setAuthorityMetadata(key, value); + jest.spyOn( + Authority.prototype, + "updateEndpointMetadata" + ).mockResolvedValue("cache"); + authority = new Authority( + Constants.DEFAULT_AUTHORITY, + networkInterface, + mockStorage, + authorityOptions, + logger ); - expect(cachedAuthorityMetadata.preferred_network).toBe( - "custom-domain.microsoft.com" + + getCloudDiscoveryMetadataFromHarcodedValuesSpy.mockReturnValue( + null ); - expect(cachedAuthorityMetadata.aliasesFromNetwork).toBe( + + await authority.resolveEndpointsAsync(); + expect(authority.isAlias(configAliases[0])).toBe(true); + expect(authority.isAlias(configAliases[1])).toBe(true); + expect(authority.isAlias(configAliases[2])).toBe(true); + expect(authority.getPreferredCache()).toBe( + expectedCloudDiscoveryMetadata.preferred_cache + ); + expect( + authority.canonicalAuthority.includes( + expectedCloudDiscoveryMetadata.preferred_network + ) + ).toBe(true); + + // Test that the metadata is cached + const cachedAuthorityMetadata = + mockStorage.getAuthorityMetadata(key); + if (!cachedAuthorityMetadata) { + throw Error( + "Cached AuthorityMetadata should not be null!" + ); + } else { + expect(cachedAuthorityMetadata.aliases).toContain( + configAliases[0] + ); + expect(cachedAuthorityMetadata.aliases).toContain( + configAliases[1] + ); + expect(cachedAuthorityMetadata.aliases).toContain( + configAliases[2] + ); + expect(cachedAuthorityMetadata.preferred_cache).toBe( + expectedCloudDiscoveryMetadata.preferred_cache + ); + expect(cachedAuthorityMetadata.preferred_network).toBe( + expectedCloudDiscoveryMetadata.preferred_network + ); + expect(cachedAuthorityMetadata.aliasesFromNetwork).toBe( + true + ); + } + + expect( + getCloudDiscoveryMetadataFromConfigSpy + ).toHaveBeenCalled(); + expect( + getCloudDiscoveryMetadataFromHarcodedValuesSpy + ).toHaveBeenCalled(); + expect( + getCloudDiscoveryMetadataFromNetworkSpy + ).not.toHaveBeenCalled(); + }); + + it("sets instance metadata from network if not present in config, hardcoded values or cache", async () => { + const authorityOptions: AuthorityOptions = { + protocolMode: ProtocolMode.AAD, + knownAuthorities: [], + cloudDiscoveryMetadata: "", + authorityMetadata: "", + }; + networkInterface.sendGetRequestAsync = ( + url: string, + options?: NetworkRequestOptions + ): any => { + if (url.includes("discovery/instance")) { + return DEFAULT_TENANT_DISCOVERY_RESPONSE; + } else { + return DEFAULT_OPENID_CONFIG_RESPONSE; + } + }; + + authority = new Authority( + Constants.DEFAULT_AUTHORITY, + networkInterface, + mockStorage, + authorityOptions, + logger + ); + getCloudDiscoveryMetadataFromHarcodedValuesSpy.mockReturnValue( + null + ); + await authority.resolveEndpointsAsync(); + expect( + getCloudDiscoveryMetadataFromConfigSpy + ).toHaveBeenCalled(); + expect( + getCloudDiscoveryMetadataFromHarcodedValuesSpy + ).toHaveBeenCalled(); + expect( + getCloudDiscoveryMetadataFromNetworkSpy + ).toHaveBeenCalled(); + }); + + it("Sets instance metadata from network if not present in config, or hardcoded values and cache entry is expired", async () => { + const authorityOptions: AuthorityOptions = { + protocolMode: ProtocolMode.AAD, + knownAuthorities: [], + cloudDiscoveryMetadata: "", + authorityMetadata: "", + }; + + const key = `authority-metadata-${TEST_CONFIG.MSAL_CLIENT_ID}-sts.windows.net`; + const value = new AuthorityMetadataEntity(); + value.updateCloudDiscoveryMetadata( + DEFAULT_TENANT_DISCOVERY_RESPONSE.body.metadata[0], true ); - } - }); + value.updateCanonicalAuthority(Constants.DEFAULT_AUTHORITY); + mockStorage.setAuthorityMetadata(key, value); + jest.spyOn( + AuthorityMetadataEntity.prototype, + "isExpired" + ).mockReturnValue(true); + jest.spyOn( + Authority.prototype, + "updateEndpointMetadata" + ).mockResolvedValue("cache"); + + networkInterface.sendGetRequestAsync = ( + url: string, + options?: NetworkRequestOptions + ): any => { + if (url.includes("discovery/instance")) { + return DEFAULT_TENANT_DISCOVERY_RESPONSE; + } else { + return DEFAULT_OPENID_CONFIG_RESPONSE; + } + }; - it("Sets metadata from host for DSTS authority", async () => { - const authorityOptions: AuthorityOptions = { - protocolMode: ProtocolMode.AAD, - knownAuthorities: [ - "https://custom-domain.microsoft.com/dstsv2", - ], - cloudDiscoveryMetadata: "", - authorityMetadata: "", - }; - networkInterface.sendGetRequestAsync = ( - url: string, - options?: NetworkRequestOptions - ): any => { - return DEFAULT_TENANT_DISCOVERY_RESPONSE; - }; - jest.spyOn( - Authority.prototype, - "updateEndpointMetadata" - ).mockResolvedValue("cache"); - authority = new Authority( - "https://custom-domain.microsoft.com/dstsv2", - networkInterface, - mockStorage, - authorityOptions, - logger - ); + authority = new Authority( + Constants.DEFAULT_AUTHORITY, + networkInterface, + mockStorage, + authorityOptions, + logger + ); - await authority.resolveEndpointsAsync(); - expect(authority.isAlias("custom-domain.microsoft.com")).toBe( - true - ); - expect(authority.getPreferredCache()).toBe( - "custom-domain.microsoft.com" - ); - expect( - authority.canonicalAuthority.includes( - "custom-domain.microsoft.com" - ) - ); + getCloudDiscoveryMetadataFromHarcodedValuesSpy.mockReturnValue( + null + ); - // Test that the metadata is cached - const key = `authority-metadata-${TEST_CONFIG.MSAL_CLIENT_ID}-custom-domain.microsoft.com`; - const cachedAuthorityMetadata = - mockStorage.getAuthorityMetadata(key); - if (!cachedAuthorityMetadata) { - throw Error("Cached AuthorityMetadata should not be null!"); - } else { - expect(cachedAuthorityMetadata.aliases).toContain( - "custom-domain.microsoft.com" + await authority.resolveEndpointsAsync(); + expect(authority.isAlias("login.microsoftonline.com")).toBe( + true ); - expect(cachedAuthorityMetadata.preferred_cache).toBe( + expect(authority.isAlias("login.windows.net")).toBe(true); + expect(authority.isAlias("sts.windows.net")).toBe(true); + expect(authority.getPreferredCache()).toBe( + "sts.windows.net" + ); + expect( + authority.canonicalAuthority.includes( + "login.windows.net" + ) + ).toBe(true); + + // Test that the metadata is cached + const cachedAuthorityMetadata = + mockStorage.getAuthorityMetadata(key); + if (!cachedAuthorityMetadata) { + throw Error( + "Cached AuthorityMetadata should not be null!" + ); + } else { + expect(cachedAuthorityMetadata.aliases).toContain( + "login.microsoftonline.com" + ); + expect(cachedAuthorityMetadata.aliases).toContain( + "login.windows.net" + ); + expect(cachedAuthorityMetadata.aliases).toContain( + "sts.windows.net" + ); + expect(cachedAuthorityMetadata.preferred_cache).toBe( + "sts.windows.net" + ); + expect(cachedAuthorityMetadata.preferred_network).toBe( + "login.windows.net" + ); + expect(cachedAuthorityMetadata.aliasesFromNetwork).toBe( + true + ); + } + + expect( + getCloudDiscoveryMetadataFromConfigSpy + ).toHaveBeenCalled(); + expect( + getCloudDiscoveryMetadataFromHarcodedValuesSpy + ).toHaveBeenCalled(); + expect( + getCloudDiscoveryMetadataFromNetworkSpy + ).toHaveBeenCalled(); + }); + + it("Sets metadata from host if network call succeeds but does not explicitly include the host", async () => { + const authorityOptions: AuthorityOptions = { + protocolMode: ProtocolMode.AAD, + knownAuthorities: [], + cloudDiscoveryMetadata: "", + authorityMetadata: "", + }; + networkInterface.sendGetRequestAsync = ( + url: string, + options?: NetworkRequestOptions + ): any => { + return DEFAULT_TENANT_DISCOVERY_RESPONSE; + }; + jest.spyOn( + Authority.prototype, + "updateEndpointMetadata" + ).mockResolvedValue("cache"); + authority = new Authority( + "https://custom-domain.microsoft.com", + networkInterface, + mockStorage, + authorityOptions, + logger + ); + + await authority.resolveEndpointsAsync(); + expect( + authority.isAlias("custom-domain.microsoft.com") + ).toBe(true); + expect(authority.getPreferredCache()).toBe( "custom-domain.microsoft.com" ); - expect(cachedAuthorityMetadata.preferred_network).toBe( + expect( + authority.canonicalAuthority.includes( + "custom-domain.microsoft.com" + ) + ); + + // Test that the metadata is cached + const key = `authority-metadata-${TEST_CONFIG.MSAL_CLIENT_ID}-custom-domain.microsoft.com`; + const cachedAuthorityMetadata = + mockStorage.getAuthorityMetadata(key); + if (!cachedAuthorityMetadata) { + throw Error( + "Cached AuthorityMetadata should not be null!" + ); + } else { + expect(cachedAuthorityMetadata.aliases).toContain( + "custom-domain.microsoft.com" + ); + expect(cachedAuthorityMetadata.preferred_cache).toBe( + "custom-domain.microsoft.com" + ); + expect(cachedAuthorityMetadata.preferred_network).toBe( + "custom-domain.microsoft.com" + ); + expect(cachedAuthorityMetadata.aliasesFromNetwork).toBe( + true + ); + } + }); + + it("Sets metadata from host for DSTS authority", async () => { + const authorityOptions: AuthorityOptions = { + protocolMode: ProtocolMode.AAD, + knownAuthorities: [ + "https://custom-domain.microsoft.com/dstsv2", + ], + cloudDiscoveryMetadata: "", + authorityMetadata: "", + }; + networkInterface.sendGetRequestAsync = ( + url: string, + options?: NetworkRequestOptions + ): any => { + return DEFAULT_TENANT_DISCOVERY_RESPONSE; + }; + jest.spyOn( + Authority.prototype, + "updateEndpointMetadata" + ).mockResolvedValue("cache"); + authority = new Authority( + "https://custom-domain.microsoft.com/dstsv2", + networkInterface, + mockStorage, + authorityOptions, + logger + ); + + await authority.resolveEndpointsAsync(); + expect( + authority.isAlias("custom-domain.microsoft.com") + ).toBe(true); + expect(authority.getPreferredCache()).toBe( "custom-domain.microsoft.com" ); - expect(cachedAuthorityMetadata.aliasesFromNetwork).toBe( - false + expect( + authority.canonicalAuthority.includes( + "custom-domain.microsoft.com" + ) ); - } + + // Test that the metadata is cached + const key = `authority-metadata-${TEST_CONFIG.MSAL_CLIENT_ID}-custom-domain.microsoft.com`; + const cachedAuthorityMetadata = + mockStorage.getAuthorityMetadata(key); + if (!cachedAuthorityMetadata) { + throw Error( + "Cached AuthorityMetadata should not be null!" + ); + } else { + expect(cachedAuthorityMetadata.aliases).toContain( + "custom-domain.microsoft.com" + ); + expect(cachedAuthorityMetadata.preferred_cache).toBe( + "custom-domain.microsoft.com" + ); + expect(cachedAuthorityMetadata.preferred_network).toBe( + "custom-domain.microsoft.com" + ); + expect(cachedAuthorityMetadata.aliasesFromNetwork).toBe( + false + ); + } + }); }); it("Throws if cloudDiscoveryMetadata cannot be parsed into json", (done) => { @@ -2019,7 +2518,7 @@ describe("Authority.ts Class Unit Tests", () => { ); }); - it("DSTS authority uses v2 well-known endpoint with common y", async () => { + it("DSTS authority uses v2 well-known endpoint with common authority", async () => { const authorityUrl = "https://login.microsoftonline.com/dstsv2/common/"; let endpoint = ""; diff --git a/lib/msal-common/test/client/AuthorizationCodeClient.spec.ts b/lib/msal-common/test/client/AuthorizationCodeClient.spec.ts index c15ddfbc65..5f97cc22e7 100644 --- a/lib/msal-common/test/client/AuthorizationCodeClient.spec.ts +++ b/lib/msal-common/test/client/AuthorizationCodeClient.spec.ts @@ -1016,9 +1016,9 @@ describe("AuthorizationCodeClient unit tests", () => { sinon .stub( Authority.prototype, - "getEndpointMetadataFromNetwork" + "getEndpointMetadataFromHardcodedValues" ) - .resolves({ + .returns({ token_endpoint: "https://login.windows.net/common/oauth2/v2.0/token?param1=value1", issuer: "https://login.windows.net/{tenantid}/v2.0", @@ -3456,9 +3456,9 @@ describe("AuthorizationCodeClient unit tests", () => { sinon .stub( Authority.prototype, - "getEndpointMetadataFromNetwork" + "getEndpointMetadataFromHardcodedValues" ) - .resolves({ + .returns({ token_endpoint: "https://login.windows.net/common/oauth2/v2.0/token?param1=value1", issuer: "https://login.windows.net/{tenantid}/v2.0", diff --git a/lib/msal-node/docs/configuration.md b/lib/msal-node/docs/configuration.md index 054fd9bec6..c7aecf6bee 100644 --- a/lib/msal-node/docs/configuration.md +++ b/lib/msal-node/docs/configuration.md @@ -5,8 +5,9 @@ Before you start here, make sure you understand how to [initialize an app object The MSAL library has a set of configuration options that can be used to customize the behavior of your authentication flows. These options can be set either in the constructor of the [PublicClientApplication](https://azuread.github.io/microsoft-authentication-library-for-js/ref/classes/_azure_msal_node.publicclientapplication.html) object or as part of the [request APIs](request.md). Here we describe the configuration object that can be passed into the [PublicClientApplication](https://azuread.github.io/microsoft-authentication-library-for-js/ref/classes/_azure_msal_node.publicclientapplication.html) constructor. In this document: -- [Usage](#usage) -- [Options](#options) + +- [Usage](#usage) +- [Options](#options) ## Usage @@ -66,54 +67,60 @@ const msalInstance = new PublicClientApplication(msalConfig); ## Options ### Auth Config Options -| Option | Description | Format | Default Value | -| ------ | ----------- | ------ | ------------- | -| `clientId` | App ID of your application. Can be found in your [portal registration](../README.md#prerequisites). | UUID/GUID | None. This parameter is required in order for MSAL to perform any actions. | -| `authority` | URI of the tenant to authenticate and authorize with. Usually takes the form of `https://{uri}/{tenantid}` (see [Authority](../../msal-common/docs/authority.md)) | String in URI format with tenant - `https://{uri}/{tenantid}` | `https://login.microsoftonline.com/common` | -| `knownAuthorities` | An array of URIs that are known to be valid. Used in B2C scenarios. | Array of strings in URI format | Empty array `[]` | -| `cloudDiscoveryMetadata` | A string containing the cloud discovery response. Used in AAD scenarios. See [Performance](../../msal-common/docs/performance.md) for more info | string | Empty string `""` | -| `authorityMetadata` | A string containing the .well-known/openid-configuration endpoint response. See [Performance](../../msal-common/docs/performance.md) for more info | string | Empty string `""` | -| `clientCapabilities` | Array of capabilities to be added to all network requests as part of the `xms_cc` claims request (see: [Client capability in MSAL](../../msal-common/docs/client-capability.md)) | Array of strings | [] | -| `protocolMode` | Enum representing the protocol mode to use. If `"AAD"`, will function on the AAD v2 endpoints; if `"OIDC"`, will function on OIDC-compliant endpoints. | string | `"AAD"` | -| `azureCloudOptions` | A defined set of azure cloud options for developers to default to their specific cloud authorities, for specific clouds supported please refer to the [AzureCloudInstance](aka.ms/msaljs/azure_cloud_instance) | [AzureCloudOptions](https://azuread.github.io/microsoft-authentication-library-for-js/ref/modules/_azure_msal_common.html#azurecloudoptions) | [AzureCloudInstance.None](msaljs/azure_cloud_instance) | -| `skipAuthorityMetadataCache` | A flag to choose whether to use the local metadata cache during authority initialization. Metadata cache would be used if no authority metadata is provided and after a network call for metadata has failed (see [Authority](../../msal-common/docs/authority.md)) | boolean | `false` | + +| Option | Description | Format | Default Value | +| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | +| `clientId` | App ID of your application. Can be found in your [portal registration](../README.md#prerequisites). | UUID/GUID | None. This parameter is required in order for MSAL to perform any actions. | +| `authority` | URI of the tenant to authenticate and authorize with. Usually takes the form of `https://{uri}/{tenantid}` (see [Authority](../../msal-common/docs/authority.md)) | String in URI format with tenant - `https://{uri}/{tenantid}` | `https://login.microsoftonline.com/common` | +| `knownAuthorities` | An array of URIs that are known to be valid. Used in B2C scenarios. | Array of strings in URI format | Empty array `[]` | +| `cloudDiscoveryMetadata` | A string containing the cloud discovery response. Used in AAD scenarios. See [Performance](../../msal-common/docs/performance.md) for more info | string | Empty string `""` | +| `authorityMetadata` | A string containing the .well-known/openid-configuration endpoint response. See [Performance](../../msal-common/docs/performance.md) for more info | string | Empty string `""` | +| `clientCapabilities` | Array of capabilities to be added to all network requests as part of the `xms_cc` claims request (see: [Client capability in MSAL](../../msal-common/docs/client-capability.md)) | Array of strings | [] | +| `protocolMode` | Enum representing the protocol mode to use. If `"AAD"`, will function on the AAD v2 endpoints; if `"OIDC"`, will function on OIDC-compliant endpoints. | string | `"AAD"` | +| `azureCloudOptions` | A defined set of azure cloud options for developers to default to their specific cloud authorities, for specific clouds supported please refer to the [AzureCloudInstance](aka.ms/msaljs/azure_cloud_instance) | [AzureCloudOptions](https://azuread.github.io/microsoft-authentication-library-for-js/ref/modules/_azure_msal_common.html#azurecloudoptions) | [AzureCloudInstance.None](msaljs/azure_cloud_instance) | +| `skipAuthorityMetadataCache` | A flag to choose whether to use the local metadata cache during authority initialization. Metadata cache would be used if no authority metadata is provided in configuration and before a network call for metadata has been made (see [Authority](../../msal-common/docs/authority.md)) | boolean | `false` | ### Cache Config Options -| Option | Description | Format | Default Value | -| ------ | ----------- | ------ | ------------- | -| `cachePlugin` | Cache plugin with call backs to reading and writing into the cache persistence (see also: [caching](caching.md)) | [ICachePlugin](https://azuread.github.io/microsoft-authentication-library-for-js/ref/modules/_azure_msal_node.html#icacheplugin) | null + +| Option | Description | Format | Default Value | +| ------------- | ---------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | ------------- | +| `cachePlugin` | Cache plugin with call backs to reading and writing into the cache persistence (see also: [caching](caching.md)) | [ICachePlugin](https://azuread.github.io/microsoft-authentication-library-for-js/ref/modules/_azure_msal_node.html#icacheplugin) | null | ### Broker Config Options -| Option | Description | Format | Default Value | -| ------ | ----------- | ------ | ------------- | -| `nativeBrokerPlugin` | Broker plugin for acquiring tokens via a native token broker (see also: [brokering](brokering.md)) | INativeBrokerPlugin | null + +| Option | Description | Format | Default Value | +| -------------------- | -------------------------------------------------------------------------------------------------- | ------------------- | ------------- | +| `nativeBrokerPlugin` | Broker plugin for acquiring tokens via a native token broker (see also: [brokering](brokering.md)) | INativeBrokerPlugin | null | ### System Config Options -| Option | Description | Format | Default Value | -| ------ | ----------- | ------ | ------------- | -| `loggerOptions` | Config object for logger. | See [below](#logger-config-options). | See [below](#logger-config-options). | -| `NetworkClient` | Custom HTTP implementation | INetworkModule | [HttpClient.ts](https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-node/src/network/HttpClient.ts) | -| `proxyUrl` | The URL of the proxy the app is running behind | string | Empty string `""` | -| `customAgentOptions` | Set of configurable options to set on a http(s) agent | Object - [NodeJS documentation on alloweable options](https://nodejs.org/docs/latest-v16.x/api/http.html#new-agentoptions) | Empty Object `{}` | + +| Option | Description | Format | Default Value | +| -------------------- | ----------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| `loggerOptions` | Config object for logger. | See [below](#logger-config-options). | See [below](#logger-config-options). | +| `NetworkClient` | Custom HTTP implementation | INetworkModule | [HttpClient.ts](https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-node/src/network/HttpClient.ts) | +| `proxyUrl` | The URL of the proxy the app is running behind | string | Empty string `""` | +| `customAgentOptions` | Set of configurable options to set on a http(s) agent | Object - [NodeJS documentation on alloweable options](https://nodejs.org/docs/latest-v16.x/api/http.html#new-agentoptions) | Empty Object `{}` | #### Logger Config Options -| Option | Description | Format | Default Value | -| ------ | ----------- | ------ | ------------- | -| `loggerCallback` | Callback function which handles the logging of MSAL statements. | Function - `loggerCallback: (level: LogLevel, message: string, containsPii: boolean): void` | See [above](#usage). | -| `piiLoggingEnabled` | If true, personally identifiable information (PII) is included in logs. | boolean | `false` | + +| Option | Description | Format | Default Value | +| ------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | -------------------- | +| `loggerCallback` | Callback function which handles the logging of MSAL statements. | Function - `loggerCallback: (level: LogLevel, message: string, containsPii: boolean): void` | See [above](#usage). | +| `piiLoggingEnabled` | If true, personally identifiable information (PII) is included in logs. | boolean | `false` | ### Telemetry Config Options -| Option | Description | Format | Default Value | -| ------ | ----------- | ------ | ------------- | +| Option | Description | Format | Default Value | +| ------------- | ------------------------------------------------ | ----------------------------------- | ----------------------------------- | | `application` | Telemetry options for applications using MSAL.js | See [below](#application-telemetry) | See [below](#application-telemetry) | #### Application Telemetry -| Option | Description | Format | Default Value | -| ------ | ----------- | ------ | ------------- | -| `appName` | Unique string name of an application | string | Empty string "" | +| Option | Description | Format | Default Value | +| ------------ | ------------------------------------- | ------ | --------------- | +| `appName` | Unique string name of an application | string | Empty string "" | | `appVersion` | Version of the application using MSAL | string | Empty string "" | ## Next Steps + Proceed to understand the public APIs provided by `msal-node` for acquiring tokens [here](request.md)