diff --git a/change/@azure-msal-common-334d43a0-8c93-4c68-bb48-1debb1510815.json b/change/@azure-msal-common-334d43a0-8c93-4c68-bb48-1debb1510815.json new file mode 100644 index 0000000000..fc730d7e32 --- /dev/null +++ b/change/@azure-msal-common-334d43a0-8c93-4c68-bb48-1debb1510815.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Catch errors thrown by \"decodeURIComponent\" #6226", + "packageName": "@azure/msal-common", + "email": "kshabelko@microsoft.com", + "dependentChangeType": "patch" +} 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/change/@azure-msal-node-c1a45be3-8104-4384-8fb0-b0c3c22cf892.json b/change/@azure-msal-node-c1a45be3-8104-4384-8fb0-b0c3c22cf892.json new file mode 100644 index 0000000000..9cb3c9e653 --- /dev/null +++ b/change/@azure-msal-node-c1a45be3-8104-4384-8fb0-b0c3c22cf892.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "ClientCredentials: Fixed bug where user-supplied cache is loaded into memory only after network request #6218", + "packageName": "@azure/msal-node", + "email": "rginsburg@microsoft.com", + "dependentChangeType": "patch" +} \ No newline at end of file diff --git a/lib/msal-angular/FAQ.md b/lib/msal-angular/FAQ.md index f52efc9015..fe1e28efff 100644 --- a/lib/msal-angular/FAQ.md +++ b/lib/msal-angular/FAQ.md @@ -127,7 +127,7 @@ Our older [Angular 11 sample](https://github.com/AzureAD/microsoft-authenticatio One of the common reasons your app may be looping while logging in with redirects is due to improper usage of the `loginRedirect()` API. We recommend that you do not call `loginRedirect()` in the `ngOnInit` in the `app.component.ts`, as this will attempt to log in with every page load, often before any redirect has finished processing. -Redirects **must** be handled either with the `MsalRedirectComponent` or with calling `handleRedirectObservable()`. See our docs on redirects [here](https://github.com/AzureAD/microsoft-authentication-library-for-js/tree/dev/lib/msal-angular/docs/redirects.md) for more information. Additionally, any interaction or account validation should be done after subscribing to the `inProgress$` observable and filtering for `InteractionStatus.None`. Please see our [sample](https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/samples/msal-angular-v3-samples/angular15-sample-app/src/app/app.component.ts#L43) for an example. +Redirects **must** be handled either with the `MsalRedirectComponent` or with calling `handleRedirectObservable()`. See our docs on redirects [here](https://github.com/AzureAD/microsoft-authentication-library-for-js/tree/dev/lib/msal-angular/docs/redirects.md) for more information. Additionally, any interaction or account validation should be done after subscribing to the `inProgress$` observable of `MsalBroadcastService` and filtering for `InteractionStatus.None`. Please see our [sample](https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/samples/msal-angular-v3-samples/angular15-sample-app/src/app/app.component.ts#L43) for an example. ## How do I implement self-service sign-up? MSAL Angular supports self-service sign-up in the auth code flow. Please see our docs [here](https://azuread.github.io/microsoft-authentication-library-for-js/ref/modules/_azure_msal_browser.html#popuprequest) for supported prompt values in the request and their expected outcomes, and [here](http://aka.ms/s3u) for an overview of self-service sign-up and configuration changes that need to be made to your Azure tenant. Please note that that self-service sign-up is not available in B2C and test environments. @@ -142,18 +142,18 @@ Please see our [MsalGuard doc](https://github.com/AzureAD/microsoft-authenticati The `@azure/msal-browser` instance used by `@azure/msal-angular` exposes multiple methods for getting account information. We recommend using `getAllAccounts()` to get all accounts, and `getAccountByHomeId()` and `getAccountByLocalId()` to get specific accounts. Note that while `getAccountByUsername()` is available, it should be a secondary choice, as it may be less reliable and is for convenience only. See the [`@azure/msal-browser` docs](https://azuread.github.io/microsoft-authentication-library-for-js/ref/classes/_azure_msal_browser.publicclientapplication.html) for more details on account methods. -We recommend subscribing to the `inProgress$` observable and filtering for `InteractionStatus.None` before retrieving account information. This ensures that all interactions have completed before getting account information. See [our sample](https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/samples/msal-angular-v3-samples/angular15-sample-app/src/app/app.component.ts#L45) for an example of this use. +We recommend subscribing to the `inProgress$` observable of `MsalBroadcastService` and filtering for `InteractionStatus.None` before retrieving account information. This ensures that all interactions have completed before getting account information. See [our sample](https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/samples/msal-angular-v3-samples/angular15-sample-app/src/app/app.component.ts#L45) for an example of this use. ### How do I get and set active accounts? The `msal-browser` instance exposes `getActiveAccount()` and `setActiveAccount()` for active accounts. -We recommend subscribing to the `inProgress$` observable and filtering for `InteractionStatus.None` before retrieving account information with `getActiveAccount()`. This ensures that all interactions have completed before getting account information. +We recommend subscribing to the `inProgress$` observable of `MsalBroadcastService` and filtering for `InteractionStatus.None` before retrieving account information with `getActiveAccount()`. This ensures that all interactions have completed before getting account information. We recommend setting the active account: - After any action that may change the account, especially if your app uses multiple accounts. See [here](https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/samples/msal-angular-v3-samples/angular15-sample-app/src/app/home/home.component.ts#L24) for an example of setting the account after a successful login. -- On initial page load. Wait until all interactions are complete (by subscribing to the `inProgress$` observable and filtering for `InteractionStatus.None`), check if there is an active account, and if there is none, set the active account. This could be the first account retrieved by `getAllAccounts()`, or other account selection logic required by your app. See [here](https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/samples/msal-angular-v3-samples/angular15-sample-app/src/app/app.component.ts#L43) for an example of checking and setting the active account on page load. +- On initial page load. Wait until all interactions are complete (by subscribing to the `inProgress$` observable of `MsalBroadcastService` and filtering for `InteractionStatus.None`), check if there is an active account, and if there is none, set the active account. This could be the first account retrieved by `getAllAccounts()`, or other account selection logic required by your app. See [here](https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/samples/msal-angular-v3-samples/angular15-sample-app/src/app/app.component.ts#L43) for an example of checking and setting the active account on page load. **Note:** Prior to `@azure/msal-browser@2.16.0` active account did not persist across page loads. If you are using `@azure/msal-browser@2.15.0` or earlier we recommend that you set the active account for each page load. In version 2.16.0 and above the active account will be cached in the cache location specified in your MSAL config and retrieved each time `getActiveAccount` is called. diff --git a/lib/msal-angular/docs/redirects.md b/lib/msal-angular/docs/redirects.md index 3d23338504..6c4f7de14d 100644 --- a/lib/msal-angular/docs/redirects.md +++ b/lib/msal-angular/docs/redirects.md @@ -37,7 +37,7 @@ index.html ```js - // Selector for additional bootstrapped component + ``` 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/src/response/ResponseHandler.ts b/lib/msal-common/src/response/ResponseHandler.ts index 37ef05166f..29c6b5586f 100644 --- a/lib/msal-common/src/response/ResponseHandler.ts +++ b/lib/msal-common/src/response/ResponseHandler.ts @@ -82,15 +82,33 @@ export class ResponseHandler { cryptoObj: ICrypto ): void { if (!serverResponseHash.state || !cachedState) { - throw !serverResponseHash.state - ? ClientAuthError.createStateNotFoundError("Server State") - : ClientAuthError.createStateNotFoundError("Cached State"); + throw serverResponseHash.state + ? ClientAuthError.createStateNotFoundError("Cached State") + : ClientAuthError.createStateNotFoundError("Server State"); } - if ( - decodeURIComponent(serverResponseHash.state) !== - decodeURIComponent(cachedState) - ) { + let decodedServerResponseHash: string; + let decodedCachedState: string; + + try { + decodedServerResponseHash = decodeURIComponent(serverResponseHash.state); + } catch (e) { + throw ClientAuthError.createInvalidStateError( + serverResponseHash.state, + `Server response hash URI could not be decoded` + ); + } + + try { + decodedCachedState = decodeURIComponent(cachedState); + } catch (e) { + throw ClientAuthError.createInvalidStateError( + serverResponseHash.state, + `Cached state URI could not be decoded` + ); + } + + if (decodedServerResponseHash !== decodedCachedState) { throw ClientAuthError.createStateMismatchError(); } 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-common/test/response/ResponseHandler.spec.ts b/lib/msal-common/test/response/ResponseHandler.spec.ts index c35e811f5a..caed8cd4cc 100644 --- a/lib/msal-common/test/response/ResponseHandler.spec.ts +++ b/lib/msal-common/test/response/ResponseHandler.spec.ts @@ -847,5 +847,35 @@ describe("ResponseHandler.ts", () => { ); expect(buildClientInfoSpy.notCalled).toBe(true); }); + + it("throws invalid state error", (done) => { + const testServerCodeResponse: ServerAuthorizationCodeResponse = { + code: "testCode", + client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, + state: TEST_STATE_VALUES.URI_ENCODED_LIB_STATE, + }; + + const responseHandler = new ResponseHandler( + "this-is-a-client-id", + testCacheManager, + cryptoInterface, + logger, + null, + null + ); + + try { + responseHandler.validateServerAuthorizationCodeResponse( + testServerCodeResponse, + 'dummy-state-%20%%%30%%%%%40', + cryptoInterface + ); + } catch (e) { + expect(e).toBeInstanceOf(ClientAuthError); + const err = e as ClientAuthError; + expect(err.message).toContain(`Cached state URI could not be decoded`); + done(); + } + }); }); }); 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) diff --git a/lib/msal-node/src/client/ClientCredentialClient.ts b/lib/msal-node/src/client/ClientCredentialClient.ts index be0507fb4c..c406792fb6 100644 --- a/lib/msal-node/src/client/ClientCredentialClient.ts +++ b/lib/msal-node/src/client/ClientCredentialClient.ts @@ -25,6 +25,7 @@ import { ServerAuthorizationTokenResponse, StringUtils, TimeUtils, + TokenCacheContext, UrlString, } from "@azure/msal-common"; @@ -71,8 +72,19 @@ export class ClientCredentialClient extends BaseClient { private async getCachedAuthenticationResult( request: CommonClientCredentialRequest ): Promise { + // read the user-supplied cache into memory, if applicable + let cacheContext; + if (this.config.serializableCache && this.config.persistencePlugin) { + cacheContext = new TokenCacheContext(this.config.serializableCache, false); + await this.config.persistencePlugin.beforeCacheAccess(cacheContext); + } + const cachedAccessToken = this.readAccessTokenFromCache(); + if (this.config.serializableCache && this.config.persistencePlugin && cacheContext) { + await this.config.persistencePlugin.afterCacheAccess(cacheContext); + } + if (!cachedAccessToken) { this.serverTelemetryManager?.setCacheOutcome( CacheOutcome.NO_CACHED_ACCESS_TOKEN diff --git a/samples/msal-node-samples/client-credentials-distributed-cache/src/AuthProvider.ts b/samples/msal-node-samples/client-credentials-distributed-cache/src/AuthProvider.ts index db5e9bbc6a..cce72394bc 100644 --- a/samples/msal-node-samples/client-credentials-distributed-cache/src/AuthProvider.ts +++ b/samples/msal-node-samples/client-credentials-distributed-cache/src/AuthProvider.ts @@ -83,7 +83,6 @@ export class AuthProvider { try { performance.mark("acquireTokenByClientCredential-start"); - await cca.getTokenCache().getAllAccounts(); // required for triggering cache read tokenResponse = await cca.acquireTokenByClientCredential(tokenRequest); performance.mark("acquireTokenByClientCredential-end"); performance.measure(