UNPKG

@azure/msal-common

Version:
977 lines (974 loc) 45 kB
/*! @azure/msal-common v16.6.2 2026-05-19 */ 'use strict'; import { AuthorityType } from './AuthorityType.mjs'; import { isOpenIdConfigResponse } from './OpenIdConfigResponse.mjs'; import { UrlString } from '../url/UrlString.mjs'; import { createClientAuthError } from '../error/ClientAuthError.mjs'; import { CIAM_AUTH_URL, DSTS, ADFS, AuthorityMetadataSource, AZURE_REGION_AUTO_DISCOVER_FLAG, RegionDiscoveryOutcomes, AAD_INSTANCE_DISCOVERY_ENDPT, INVALID_INSTANCE, DEFAULT_AUTHORITY_HOST, AAD_TENANT_DOMAIN_SUFFIX, KNOWN_PUBLIC_CLOUDS, FORWARD_SLASH, AADAuthority, DEFAULT_COMMON_TENANT, REGIONAL_AUTH_PUBLIC_CLOUD_SUFFIX } from '../utils/Constants.mjs'; import { EndpointMetadata, getCloudDiscoveryMetadataFromHardcodedValues, getCloudDiscoveryMetadataFromNetworkResponse, InstanceDiscoveryMetadataAliases } from './AuthorityMetadata.mjs'; import { createClientConfigurationError } from '../error/ClientConfigurationError.mjs'; import { ProtocolMode } from './ProtocolMode.mjs'; import { AzureCloudInstance } from './AuthorityOptions.mjs'; import { isCloudInstanceDiscoveryResponse } from './CloudInstanceDiscoveryResponse.mjs'; import { isCloudInstanceDiscoveryErrorResponse } from './CloudInstanceDiscoveryErrorResponse.mjs'; import { RegionDiscovery } from './RegionDiscovery.mjs'; import { AuthError } from '../error/AuthError.mjs'; import { AuthorityUpdateCloudDiscoveryMetadata, AuthorityUpdateEndpointMetadata, AuthorityUpdateMetadataWithRegionalInformation, AuthorityGetEndpointMetadataFromNetwork, RegionDiscoveryDetectRegion, AuthorityGetCloudDiscoveryMetadataFromNetwork } from '../telemetry/performance/PerformanceEvents.mjs'; import { invokeAsync } from '../utils/FunctionWrappers.mjs'; import { generateAuthorityMetadataExpiresAt, updateAuthorityEndpointMetadata, isAuthorityMetadataExpired, updateCloudDiscoveryMetadata } from '../cache/utils/CacheHelpers.mjs'; import { endpointResolutionError, endSessionEndpointNotSupported, openIdConfigError } from '../error/ClientAuthErrorCodes.mjs'; import { invalidAuthorityMetadata, untrustedAuthority, invalidCloudDiscoveryMetadata, issuerValidationFailed } from '../error/ClientConfigurationErrorCodes.mjs'; /* * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ /** * The authority class validates the authority URIs used by the user, and retrieves the OpenID Configuration Data from the * endpoint. It will store the pertinent config data in this object for use during token calls. * @internal */ class Authority { constructor(authority, networkInterface, cacheManager, authorityOptions, logger, correlationId, performanceClient, managedIdentity) { this.canonicalAuthority = authority; this._canonicalAuthority.validateAsUri(); this.networkInterface = networkInterface; this.cacheManager = cacheManager; this.authorityOptions = authorityOptions; this.regionDiscoveryMetadata = { region_used: undefined, region_source: undefined, region_outcome: undefined, }; this.logger = logger; this.performanceClient = performanceClient; this.correlationId = correlationId; this.managedIdentity = managedIdentity || false; this.regionDiscovery = new RegionDiscovery(networkInterface, this.logger, this.performanceClient, this.correlationId); } /** * Get {@link AuthorityType} * @param authorityUri {@link IUri} * @private */ getAuthorityType(authorityUri) { // CIAM auth url pattern is being standardized as: <tenant>.ciamlogin.com if (authorityUri.HostNameAndPort.endsWith(CIAM_AUTH_URL)) { return AuthorityType.Ciam; } const pathSegments = authorityUri.PathSegments; if (pathSegments.length) { switch (pathSegments[0].toLowerCase()) { case ADFS: return AuthorityType.Adfs; case DSTS: return AuthorityType.Dsts; } } return AuthorityType.Default; } // See above for AuthorityType get authorityType() { return this.getAuthorityType(this.canonicalAuthorityUrlComponents); } /** * ProtocolMode enum representing the way endpoints are constructed. */ get protocolMode() { return this.authorityOptions.protocolMode; } /** * Returns authorityOptions which can be used to reinstantiate a new authority instance */ get options() { return this.authorityOptions; } /** * A URL that is the authority set by the developer */ get canonicalAuthority() { return this._canonicalAuthority.urlString; } /** * Sets canonical authority. */ set canonicalAuthority(url) { this._canonicalAuthority = new UrlString(url); this._canonicalAuthority.validateAsUri(); this._canonicalAuthorityUrlComponents = null; } /** * Get authority components. */ get canonicalAuthorityUrlComponents() { if (!this._canonicalAuthorityUrlComponents) { this._canonicalAuthorityUrlComponents = this._canonicalAuthority.getUrlComponents(); } return this._canonicalAuthorityUrlComponents; } /** * Get hostname and port i.e. login.microsoftonline.com */ get hostnameAndPort() { return this.canonicalAuthorityUrlComponents.HostNameAndPort.toLowerCase(); } /** * Get tenant for authority. */ get tenant() { return this.canonicalAuthorityUrlComponents.PathSegments[0]; } /** * OAuth /authorize endpoint for requests */ get authorizationEndpoint() { if (this.discoveryComplete()) { return this.replacePath(this.metadata.authorization_endpoint); } else { throw createClientAuthError(endpointResolutionError); } } /** * OAuth /token endpoint for requests */ get tokenEndpoint() { if (this.discoveryComplete()) { return this.replacePath(this.metadata.token_endpoint); } else { throw createClientAuthError(endpointResolutionError); } } get deviceCodeEndpoint() { if (this.discoveryComplete()) { return this.replacePath(this.metadata.token_endpoint.replace("/token", "/devicecode")); } else { throw createClientAuthError(endpointResolutionError); } } /** * OAuth logout endpoint for requests */ get endSessionEndpoint() { if (this.discoveryComplete()) { // ROPC policies may not have end_session_endpoint set if (!this.metadata.end_session_endpoint) { throw createClientAuthError(endSessionEndpointNotSupported); } return this.replacePath(this.metadata.end_session_endpoint); } else { throw createClientAuthError(endpointResolutionError); } } /** * OAuth issuer for requests */ get selfSignedJwtAudience() { if (this.discoveryComplete()) { return this.replacePath(this.metadata.issuer); } else { throw createClientAuthError(endpointResolutionError); } } /** * Jwks_uri for token signing keys */ get jwksUri() { if (this.discoveryComplete()) { return this.replacePath(this.metadata.jwks_uri); } else { throw createClientAuthError(endpointResolutionError); } } /** * Returns a flag indicating that tenant name can be replaced in authority {@link IUri} * @param authorityUri {@link IUri} * @private */ canReplaceTenant(authorityUri) { return (authorityUri.PathSegments.length === 1 && !Authority.reservedTenantDomains.has(authorityUri.PathSegments[0]) && this.getAuthorityType(authorityUri) === AuthorityType.Default && this.protocolMode !== ProtocolMode.OIDC); } /** * Replaces tenant in url path with current tenant. Defaults to common. * @param urlString */ replaceTenant(urlString) { return urlString.replace(/{tenant}|{tenantid}/g, this.tenant); } /** * Replaces path such as tenant or policy with the current tenant or policy. * @param urlString */ replacePath(urlString) { let endpoint = urlString; const cachedAuthorityUrl = new UrlString(this.metadata.canonical_authority); const cachedAuthorityUrlComponents = cachedAuthorityUrl.getUrlComponents(); const cachedAuthorityParts = cachedAuthorityUrlComponents.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]; /** * 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("1q3g2x", this.correlationId); cachedPart = tenantId; } } if (currentPart !== cachedPart) { endpoint = endpoint.replace(`/${cachedPart}/`, `/${currentPart}/`); } }); return this.replaceTenant(endpoint); } /** * The default open id configuration endpoint for any canonical authority. */ get defaultOpenIdConfigurationEndpoint() { const canonicalAuthorityHost = this.hostnameAndPort; if (this.canonicalAuthority.endsWith("v2.0/") || this.authorityType === AuthorityType.Adfs || (this.protocolMode === ProtocolMode.OIDC && !this.isAliasOfKnownMicrosoftAuthority(canonicalAuthorityHost))) { return `${this.canonicalAuthority}.well-known/openid-configuration`; } return `${this.canonicalAuthority}v2.0/.well-known/openid-configuration`; } /** * Boolean that returns whether or not tenant discovery has been completed. */ discoveryComplete() { return !!this.metadata; } /** * Perform endpoint discovery to discover aliases, preferred_cache, preferred_network * and the /authorize, /token and logout endpoints. */ async resolveEndpointsAsync() { const metadataEntity = this.getCurrentMetadataEntity(); const cloudDiscoverySource = await invokeAsync(this.updateCloudDiscoveryMetadata.bind(this), AuthorityUpdateCloudDiscoveryMetadata, this.logger, this.performanceClient, this.correlationId)(metadataEntity); this.canonicalAuthority = this.canonicalAuthority.replace(this.hostnameAndPort, metadataEntity.preferred_network); const endpointSource = await invokeAsync(this.updateEndpointMetadata.bind(this), AuthorityUpdateEndpointMetadata, this.logger, this.performanceClient, this.correlationId)(metadataEntity); this.updateCachedMetadata(metadataEntity, cloudDiscoverySource, { source: endpointSource, }); this.performanceClient?.addFields({ cloudDiscoverySource: cloudDiscoverySource, authorityEndpointSource: endpointSource, }, this.correlationId); } /** * Returns metadata entity from cache if it exists, otherwise returns a new metadata entity built * from the configured canonical authority * @returns */ getCurrentMetadataEntity() { let metadataEntity = this.cacheManager.getAuthorityMetadataByAlias(this.hostnameAndPort, this.correlationId); if (!metadataEntity) { metadataEntity = { aliases: [], preferred_cache: this.hostnameAndPort, preferred_network: this.hostnameAndPort, canonical_authority: this.canonicalAuthority, authorization_endpoint: "", token_endpoint: "", end_session_endpoint: "", issuer: "", aliasesFromNetwork: false, endpointsFromNetwork: false, expiresAt: generateAuthorityMetadataExpiresAt(), jwks_uri: "", }; } return metadataEntity; } /** * Updates cached metadata based on metadata source and sets the instance's metadata * property to the same value * @param metadataEntity * @param cloudDiscoverySource * @param endpointMetadataResult */ updateCachedMetadata(metadataEntity, cloudDiscoverySource, endpointMetadataResult) { if (cloudDiscoverySource !== AuthorityMetadataSource.CACHE && endpointMetadataResult?.source !== AuthorityMetadataSource.CACHE) { // Reset the expiration time unless both values came from a successful cache lookup metadataEntity.expiresAt = generateAuthorityMetadataExpiresAt(); metadataEntity.canonical_authority = this.canonicalAuthority; } const cacheKey = this.cacheManager.generateAuthorityMetadataCacheKey(metadataEntity.preferred_cache, this.correlationId); this.cacheManager.setAuthorityMetadata(cacheKey, metadataEntity, this.correlationId); this.metadata = metadataEntity; } /** * Update AuthorityMetadataEntity with new endpoints and return where the information came from * @param metadataEntity */ async updateEndpointMetadata(metadataEntity) { const localMetadata = this.updateEndpointMetadataFromLocalSources(metadataEntity); // Further update may be required for hardcoded metadata if regional metadata is preferred if (localMetadata) { if (localMetadata.source === AuthorityMetadataSource.HARDCODED_VALUES) { // If the user prefers to use an azure region replace the global endpoints with regional information. if (this.authorityOptions.azureRegionConfiguration?.azureRegion) { if (localMetadata.metadata) { const hardcodedMetadata = await invokeAsync(this.updateMetadataWithRegionalInformation.bind(this), AuthorityUpdateMetadataWithRegionalInformation, this.logger, this.performanceClient, this.correlationId)(localMetadata.metadata); updateAuthorityEndpointMetadata(metadataEntity, hardcodedMetadata, false); metadataEntity.canonical_authority = this.canonicalAuthority; } } } return localMetadata.source; } // Get metadata from network if local sources aren't available let metadata = await invokeAsync(this.getEndpointMetadataFromNetwork.bind(this), AuthorityGetEndpointMetadataFromNetwork, this.logger, this.performanceClient, this.correlationId)(); if (metadata) { // Validate the issuer returned by the OIDC discovery document. this.validateIssuer(metadata.issuer); // If the user prefers to use an azure region replace the global endpoints with regional information. if (this.authorityOptions.azureRegionConfiguration?.azureRegion) { metadata = await invokeAsync(this.updateMetadataWithRegionalInformation.bind(this), AuthorityUpdateMetadataWithRegionalInformation, this.logger, this.performanceClient, this.correlationId)(metadata); } updateAuthorityEndpointMetadata(metadataEntity, metadata, true); return AuthorityMetadataSource.NETWORK; } else { // Metadata could not be obtained from the config, cache, network or hardcoded values throw createClientAuthError(openIdConfigError, this.defaultOpenIdConfigurationEndpoint); } } /** * Updates endpoint metadata from local sources and returns where the information was retrieved from and the metadata config * response if the source is hardcoded metadata * @param metadataEntity * @returns */ updateEndpointMetadataFromLocalSources(metadataEntity) { this.logger.verbose("1fi0kc", this.correlationId); const configMetadata = this.getEndpointMetadataFromConfig(); if (configMetadata) { this.logger.verbose("06t0uj", this.correlationId); updateAuthorityEndpointMetadata(metadataEntity, configMetadata, false); return { source: AuthorityMetadataSource.CONFIG, }; } this.logger.verbose("151k0p", this.correlationId); const hardcodedMetadata = this.getEndpointMetadataFromHardcodedValues(); if (hardcodedMetadata) { updateAuthorityEndpointMetadata(metadataEntity, hardcodedMetadata, false); return { source: AuthorityMetadataSource.HARDCODED_VALUES, metadata: hardcodedMetadata, }; } else { this.logger.verbose("1imop5", this.correlationId); } // Check cached metadata entity expiration status const metadataEntityExpired = isAuthorityMetadataExpired(metadataEntity); if (this.isAuthoritySameType(metadataEntity) && metadataEntity.endpointsFromNetwork && !metadataEntityExpired) { // No need to update this.logger.verbose("16uq31", ""); return { source: AuthorityMetadataSource.CACHE }; } else if (metadataEntityExpired) { this.logger.verbose("0uoibc", ""); } return null; } /** * Compares the number of url components after the domain to determine if the cached * authority metadata can be used for the requested authority. Protects against same domain different * authority such as login.microsoftonline.com/tenant and login.microsoftonline.com/tfp/tenant/policy * @param metadataEntity */ isAuthoritySameType(metadataEntity) { const cachedAuthorityUrl = new UrlString(metadataEntity.canonical_authority); const cachedParts = cachedAuthorityUrl.getUrlComponents().PathSegments; return (cachedParts.length === this.canonicalAuthorityUrlComponents.PathSegments.length); } /** * Parse authorityMetadata config option */ getEndpointMetadataFromConfig() { if (this.authorityOptions.authorityMetadata) { try { return JSON.parse(this.authorityOptions.authorityMetadata); } catch (e) { throw createClientConfigurationError(invalidAuthorityMetadata); } } return null; } /** * Gets OAuth endpoints from the given OpenID configuration endpoint. * * @param hasHardcodedMetadata boolean */ async getEndpointMetadataFromNetwork() { const options = {}; /* * TODO: Add a timeout if the authority exists in our library's * hardcoded list of metadata */ const openIdConfigurationEndpoint = this.defaultOpenIdConfigurationEndpoint; this.logger.verbose("1y65x6", this.correlationId); try { const response = await this.networkInterface.sendGetRequestAsync(openIdConfigurationEndpoint, options); const isValidResponse = isOpenIdConfigResponse(response.body); if (isValidResponse) { return response.body; } else { this.logger.verbose("1koyv8", this.correlationId); return null; } } catch (e) { this.logger.verbose("0a9wik", this.correlationId); return null; } } /** * Get OAuth endpoints for common authorities. */ getEndpointMetadataFromHardcodedValues() { if (this.hostnameAndPort in EndpointMetadata) { return EndpointMetadata[this.hostnameAndPort]; } return null; } /** * Update the retrieved metadata with regional information. * User selected Azure region will be used if configured. */ async updateMetadataWithRegionalInformation(metadata) { const userConfiguredAzureRegion = this.authorityOptions.azureRegionConfiguration?.azureRegion; if (userConfiguredAzureRegion) { if (userConfiguredAzureRegion !== AZURE_REGION_AUTO_DISCOVER_FLAG) { this.regionDiscoveryMetadata.region_outcome = RegionDiscoveryOutcomes.CONFIGURED_NO_AUTO_DETECTION; this.regionDiscoveryMetadata.region_used = userConfiguredAzureRegion; return Authority.replaceWithRegionalInformation(metadata, userConfiguredAzureRegion); } const autodetectedRegionName = await invokeAsync(this.regionDiscovery.detectRegion.bind(this.regionDiscovery), RegionDiscoveryDetectRegion, this.logger, this.performanceClient, this.correlationId)(this.authorityOptions.azureRegionConfiguration ?.environmentRegion, this.regionDiscoveryMetadata); if (autodetectedRegionName) { this.regionDiscoveryMetadata.region_outcome = RegionDiscoveryOutcomes.AUTO_DETECTION_REQUESTED_SUCCESSFUL; this.regionDiscoveryMetadata.region_used = autodetectedRegionName; return Authority.replaceWithRegionalInformation(metadata, autodetectedRegionName); } this.regionDiscoveryMetadata.region_outcome = RegionDiscoveryOutcomes.AUTO_DETECTION_REQUESTED_FAILED; } return metadata; } /** * Updates the AuthorityMetadataEntity with new aliases, preferred_network and preferred_cache * and returns where the information was retrieved from * @param metadataEntity * @returns AuthorityMetadataSource */ async updateCloudDiscoveryMetadata(metadataEntity) { const localMetadataSource = this.updateCloudDiscoveryMetadataFromLocalSources(metadataEntity); if (localMetadataSource) { return localMetadataSource; } // Fallback to network as metadata source const metadata = await invokeAsync(this.getCloudDiscoveryMetadataFromNetwork.bind(this), AuthorityGetCloudDiscoveryMetadataFromNetwork, this.logger, this.performanceClient, this.correlationId)(); if (metadata) { updateCloudDiscoveryMetadata(metadataEntity, metadata, true); return AuthorityMetadataSource.NETWORK; } // Metadata could not be obtained from the config, cache, network or hardcoded values throw createClientConfigurationError(untrustedAuthority); } updateCloudDiscoveryMetadataFromLocalSources(metadataEntity) { this.logger.verbose("1tpqlr", this.correlationId); this.logger.verbosePii("1fy7uz", this.correlationId); this.logger.verbosePii("08zabj", this.correlationId); this.logger.verbosePii("1o1kv3", this.correlationId); const metadata = this.getCloudDiscoveryMetadataFromConfig(); if (metadata) { this.logger.verbose("1nakio", this.correlationId); updateCloudDiscoveryMetadata(metadataEntity, 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 hardcoded values this.logger.verbose("1x74aj", this.correlationId); const hardcodedMetadata = getCloudDiscoveryMetadataFromHardcodedValues(this.hostnameAndPort); if (hardcodedMetadata) { this.logger.verbose("0by47c", this.correlationId); updateCloudDiscoveryMetadata(metadataEntity, hardcodedMetadata, false); return AuthorityMetadataSource.HARDCODED_VALUES; } this.logger.verbose("0r2fzy", this.correlationId); const metadataEntityExpired = isAuthorityMetadataExpired(metadataEntity); if (this.isAuthoritySameType(metadataEntity) && metadataEntity.aliasesFromNetwork && !metadataEntityExpired) { this.logger.verbose("1uffgh", ""); // No need to update return AuthorityMetadataSource.CACHE; } else if (metadataEntityExpired) { this.logger.verbose("0uoibc", ""); } return null; } /** * Parse cloudDiscoveryMetadata config or check knownAuthorities */ getCloudDiscoveryMetadataFromConfig() { // CIAM does not support cloud discovery metadata if (this.authorityType === AuthorityType.Ciam) { this.logger.verbose("04y84h", this.correlationId); return Authority.createCloudDiscoveryMetadataFromHost(this.hostnameAndPort); } // Check if network response was provided in config if (this.authorityOptions.cloudDiscoveryMetadata) { this.logger.verbose("0gszr3", this.correlationId); try { this.logger.verbose("1iifkx", this.correlationId); const parsedResponse = JSON.parse(this.authorityOptions.cloudDiscoveryMetadata); const metadata = getCloudDiscoveryMetadataFromNetworkResponse(parsedResponse.metadata, this.hostnameAndPort); this.logger.verbose("0q67e3", ""); if (metadata) { this.logger.verbose("0hzfao", this.correlationId); return metadata; } else { this.logger.verbose("1ajz3u", this.correlationId); } } catch (e) { this.logger.verbose("1wq5tu", this.correlationId); throw createClientConfigurationError(invalidCloudDiscoveryMetadata); } } // If cloudDiscoveryMetadata is empty or does not contain the host, check knownAuthorities if (this.isInKnownAuthorities()) { this.logger.verbose("0mt9al", this.correlationId); return Authority.createCloudDiscoveryMetadataFromHost(this.hostnameAndPort); } return null; } /** * Called to get metadata from network if CloudDiscoveryMetadata was not populated by config * * @param hasHardcodedMetadata boolean */ async getCloudDiscoveryMetadataFromNetwork() { const instanceDiscoveryEndpoint = `${AAD_INSTANCE_DISCOVERY_ENDPT}${this.canonicalAuthority}oauth2/v2.0/authorize`; const options = {}; /* * TODO: Add a timeout if the authority exists in our library's * hardcoded list of metadata */ let match = null; try { const response = await this.networkInterface.sendGetRequestAsync(instanceDiscoveryEndpoint, options); let typedResponseBody; let metadata; if (isCloudInstanceDiscoveryResponse(response.body)) { typedResponseBody = response.body; metadata = typedResponseBody.metadata; this.logger.verbosePii("1vglyt", this.correlationId); } else if (isCloudInstanceDiscoveryErrorResponse(response.body)) { this.logger.warning("062uto", this.correlationId); typedResponseBody = response.body; if (typedResponseBody.error === INVALID_INSTANCE) { this.logger.error("1x90tm", this.correlationId); return null; } this.logger.warning("0wchdm", this.correlationId); this.logger.warning("1s5mpv", this.correlationId); this.logger.warning("1yhqpw", this.correlationId); metadata = []; } else { this.logger.error("0768g0", this.correlationId); return null; } this.logger.verbose("1lrobr", this.correlationId); match = getCloudDiscoveryMetadataFromNetworkResponse(metadata, this.hostnameAndPort); } catch (error) { if (error instanceof AuthError) { this.logger.error("0vwhc7", this.correlationId); } else { this.logger.error("0s2z41", this.correlationId); } return null; } // Custom Domain scenario, host is trusted because Instance Discovery call succeeded if (!match) { this.logger.warning("0jp28q", this.correlationId); this.logger.verbose("130sd8", this.correlationId); match = Authority.createCloudDiscoveryMetadataFromHost(this.hostnameAndPort); } return match; } /** * Helper function to determine if this host is included in the knownAuthorities config option */ isInKnownAuthorities() { const matches = this.authorityOptions.knownAuthorities.filter((authority) => { return (authority && UrlString.getDomainFromUrl(authority).toLowerCase() === this.hostnameAndPort); }); return matches.length > 0; } /** * helper function to populate the authority based on azureCloudOptions * @param authorityString * @param azureCloudOptions */ static generateAuthority(authorityString, azureCloudOptions) { let authorityAzureCloudInstance; if (azureCloudOptions && azureCloudOptions.azureCloudInstance !== AzureCloudInstance.None) { const tenant = azureCloudOptions.tenant ? azureCloudOptions.tenant : DEFAULT_COMMON_TENANT; authorityAzureCloudInstance = `${azureCloudOptions.azureCloudInstance}/${tenant}/`; } return authorityAzureCloudInstance ? authorityAzureCloudInstance : authorityString; } /** * Creates cloud discovery metadata object from a given host * @param host */ static createCloudDiscoveryMetadataFromHost(host) { return { preferred_network: host, preferred_cache: host, aliases: [host], }; } /** * helper function to generate environment from authority object */ getPreferredCache() { if (this.managedIdentity) { return DEFAULT_AUTHORITY_HOST; } else if (this.discoveryComplete()) { return this.metadata.preferred_cache; } else { throw createClientAuthError(endpointResolutionError); } } /** * Returns whether or not the provided host is an alias of this authority instance * @param host */ isAlias(host) { return this.metadata.aliases.indexOf(host) > -1; } /** * Returns whether or not the provided host is an alias of a known Microsoft authority for purposes of endpoint discovery * @param host */ isAliasOfKnownMicrosoftAuthority(host) { return InstanceDiscoveryMetadataAliases.has(host); } /** * Validates the `issuer` returned by an OIDC discovery document against * this authority, per * https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationValidation * * The issuer is accepted when ANY of the following holds: * 1. The issuer scheme + host + port match the authority's (path may * differ). Applies to all authorities. * 2. The authority is a Microsoft cloud authority (public, sovereign, * or CIAM), the issuer is HTTPS, and the issuer host is in the known * Microsoft authority host set. * 3. Same as (2), but the issuer host is a single-label regional variant * of a known Microsoft host (e.g. `westus.login.microsoftonline.com`). * 4. Same as (2), but the issuer host matches the CIAM tenant pattern * `{tenant}.ciamlogin.com` with an optional `/{tenant}[.onmicrosoft.com][/v2.0]` * path. * * @param issuer The `issuer` value returned in the OIDC discovery document. * @throws ClientConfigurationError("issuer_validation_failed") on failure. */ validateIssuer(issuer) { if (!issuer) { throw createClientConfigurationError(issuerValidationFailed); } // Parse with the WHATWG URL API. URL normalizes scheme + host to lowercase per RFC 3986. let issuerUrl; try { issuerUrl = new URL(issuer); } catch { throw createClientConfigurationError(issuerValidationFailed); } const issuerScheme = issuerUrl.protocol; const issuerHost = issuerUrl.host; const authorityScheme = (this.canonicalAuthorityUrlComponents.Protocol || "").toLowerCase(); const authorityHost = (this.canonicalAuthorityUrlComponents.HostNameAndPort || "").toLowerCase(); // Rule 1: Same scheme and host const matchesAuthorityOrigin = this.matchesAuthorityOrigin(issuerScheme, issuerHost, authorityScheme, authorityHost); // Rule 2: The issuer host is a well-known Microsoft authority host (HTTPS only) const matchesKnownMicrosoftHost = issuerScheme === "https:" && this.isAliasOfKnownMicrosoftAuthority(issuerHost); /* * Rule 3: The issuer host is a regional variant ({region}.{host}) of a well-known host * (HTTPS only). E.g. westus2.login.microsoft.com */ const matchesRegionalMicrosoftHost = issuerScheme === "https:" && this.matchesRegionalMicrosoftHost(issuerHost); /* * Rule 4: CIAM-specific validation. In a CIAM scenario the issuer is expected to * have "{tenant}.ciamlogin.com" as the host, even when using a custom domain. */ const matchesCiamTenantPattern = this.matchesCiamTenantPattern(issuerUrl, authorityHost, this.canonicalAuthorityUrlComponents.PathSegments); // Each rule is an independent boolean; the issuer is valid if ANY rule matches. if (matchesAuthorityOrigin || matchesKnownMicrosoftHost || matchesRegionalMicrosoftHost || matchesCiamTenantPattern) { return; } // issuer validation fails if none of the above rules are satisfied throw createClientConfigurationError(issuerValidationFailed); } /** * Rule 1: The issuer scheme + host (and port) match the authority's. Path * may differ. Applies to all authorities. */ matchesAuthorityOrigin(issuerScheme, issuerHost, authorityScheme, authorityHost) { return issuerScheme === authorityScheme && issuerHost === authorityHost; } /** * Rule 3: The issuer host is a regional variant * (`{region}.{host}`) of a known Microsoft authority host. * E.g. `westus2.login.microsoft.com`. */ matchesRegionalMicrosoftHost(issuerHost) { const firstDot = issuerHost.indexOf("."); if (firstDot > 0 && firstDot < issuerHost.length - 1) { const hostWithoutRegion = issuerHost.substring(firstDot + 1); return this.isAliasOfKnownMicrosoftAuthority(hostWithoutRegion); } return false; } /** * Rule 4: The issuer matches one of the well-known CIAM tenant patterns * (`https://{tenant}.ciamlogin.com[/{tenant}[.onmicrosoft.com][/v2.0]]`). * * The bare tenant name is extracted from the authority's first path segment * when available (stripping the `.onmicrosoft.com` suffix that * `transformCIAMAuthority` adds), or otherwise from the leftmost label of * the authority host (to support CIAM custom domain scenarios). * * Both `/{tenant}` and `/{tenant}.onmicrosoft.com` path forms are accepted * because the OIDC issuer may use either form depending on the authority URL * that was used to trigger discovery. */ matchesCiamTenantPattern(issuerUrl, authorityHost, authorityPathSegments) { /* * authorityPathSegments[0] is the first path segment of the *authority * URL* after transformCIAMAuthority runs (e.g. "contoso.onmicrosoft.com"). * Additional CIAM issuer path segments such as "/v2.0" are part of the * issuer string, not the authority URL's PathSegments. */ const pathSegment = authorityPathSegments[0]; /* * Extract the bare tenant name: strip the .onmicrosoft.com suffix when * present (introduced by transformCIAMAuthority), or fall back to the * first label of the authority hostname for non-transformed/custom-domain * CIAM authorities. */ const tenantName = pathSegment ? pathSegment.endsWith(AAD_TENANT_DOMAIN_SUFFIX) ? pathSegment.slice(0, -AAD_TENANT_DOMAIN_SUFFIX.length) : pathSegment : authorityHost.split(".")[0]; if (!tenantName) { return false; } const ciamBaseURL = `https://${tenantName}${CIAM_AUTH_URL}`; const validCiamPatterns = [ ciamBaseURL, `${ciamBaseURL}/${tenantName}`, `${ciamBaseURL}/${tenantName}/v2.0`, `${ciamBaseURL}/${tenantName}${AAD_TENANT_DOMAIN_SUFFIX}`, `${ciamBaseURL}/${tenantName}${AAD_TENANT_DOMAIN_SUFFIX}/v2.0`, // https://{tenant}.ciamlogin.com/{tenant}.onmicrosoft.com/v2.0 ]; /* * Compose the canonical issuer string from URL components and strip any * trailing slashes from the path so it can be compared to the pattern set. */ const issuerPath = issuerUrl.pathname.replace(/\/+$/, ""); const normalizedIssuer = `${issuerUrl.protocol}//${issuerUrl.host}${issuerPath}`; return validCiamPatterns.some((pattern) => pattern === normalizedIssuer); } /** * Checks whether the provided host is that of a public cloud authority * * @param authority string * @returns bool */ static isPublicCloudAuthority(host) { return KNOWN_PUBLIC_CLOUDS.indexOf(host) >= 0; } /** * Rebuild the authority string with the region * * @param host string * @param region string */ static buildRegionalAuthorityString(host, region, queryString) { // Create and validate a Url string object with the initial authority string const authorityUrlInstance = new UrlString(host); authorityUrlInstance.validateAsUri(); const authorityUrlParts = authorityUrlInstance.getUrlComponents(); let hostNameAndPort = `${region}.${authorityUrlParts.HostNameAndPort}`; if (this.isPublicCloudAuthority(authorityUrlParts.HostNameAndPort)) { hostNameAndPort = `${region}.${REGIONAL_AUTH_PUBLIC_CLOUD_SUFFIX}`; } // Include the query string portion of the url const url = UrlString.constructAuthorityUriFromObject({ ...authorityUrlInstance.getUrlComponents(), HostNameAndPort: hostNameAndPort, }).urlString; // Add the query string if a query string was provided if (queryString) return `${url}?${queryString}`; return url; } /** * Replace the endpoints in the metadata object with their regional equivalents. * * @param metadata OpenIdConfigResponse * @param azureRegion string */ static replaceWithRegionalInformation(metadata, azureRegion) { const regionalMetadata = { ...metadata }; regionalMetadata.authorization_endpoint = Authority.buildRegionalAuthorityString(regionalMetadata.authorization_endpoint, azureRegion); regionalMetadata.token_endpoint = Authority.buildRegionalAuthorityString(regionalMetadata.token_endpoint, azureRegion); if (regionalMetadata.end_session_endpoint) { regionalMetadata.end_session_endpoint = Authority.buildRegionalAuthorityString(regionalMetadata.end_session_endpoint, azureRegion); } return regionalMetadata; } /** * Transform CIAM_AUTHORIY as per the below rules: * If no path segments found and it is a CIAM authority (hostname ends with .ciamlogin.com), then transform it * * NOTE: The transformation path should go away once STS supports CIAM with the format: `tenantIdorDomain.ciamlogin.com` * `ciamlogin.com` can also change in the future and we should accommodate the same * * @param authority */ static transformCIAMAuthority(authority) { let ciamAuthority = authority; const authorityUrl = new UrlString(authority); const authorityUrlComponents = authorityUrl.getUrlComponents(); // check if transformation is needed if (authorityUrlComponents.PathSegments.length === 0 && authorityUrlComponents.HostNameAndPort.endsWith(CIAM_AUTH_URL)) { const tenantIdOrDomain = authorityUrlComponents.HostNameAndPort.split(".")[0]; ciamAuthority = `${ciamAuthority}${tenantIdOrDomain}${AAD_TENANT_DOMAIN_SUFFIX}`; } return ciamAuthority; } } // Reserved tenant domain names that will not be replaced with tenant id Authority.reservedTenantDomains = new Set([ "{tenant}", "{tenantid}", AADAuthority.COMMON, AADAuthority.CONSUMERS, AADAuthority.ORGANIZATIONS, ]); /** * Extract tenantId from authority */ function getTenantFromAuthorityString(authority) { const authorityUrl = new UrlString(authority); const authorityUrlComponents = authorityUrl.getUrlComponents(); /** * For credential matching purposes, tenantId is the last path segment of the authority URL: * AAD Authority - domain/tenantId -> Credentials are cached with realm = tenantId * B2C Authority - domain/{tenantId}?/.../policy -> Credentials are cached with realm = policy * tenantId is downcased because B2C policies can have mixed case but tfp claim is downcased * * Note that we may not have any path segments in certain OIDC scenarios. */ const tenantId = authorityUrlComponents.PathSegments.slice(-1)[0]?.toLowerCase(); switch (tenantId) { case AADAuthority.COMMON: case AADAuthority.ORGANIZATIONS: case AADAuthority.CONSUMERS: return undefined; default: return tenantId; } } function formatAuthorityUri(authorityUri) { return authorityUri.endsWith(FORWARD_SLASH) ? authorityUri : `${authorityUri}${FORWARD_SLASH}`; } function buildStaticAuthorityOptions(authOptions) { const rawCloudDiscoveryMetadata = authOptions.cloudDiscoveryMetadata; let cloudDiscoveryMetadata = undefined; if (rawCloudDiscoveryMetadata) { try { cloudDiscoveryMetadata = JSON.parse(rawCloudDiscoveryMetadata); } catch (e) { throw createClientConfigurationError(invalidCloudDiscoveryMetadata); } } return { canonicalAuthority: authOptions.authority ? formatAuthorityUri(authOptions.authority) : undefined, knownAuthorities: authOptions.knownAuthorities, cloudDiscoveryMetadata: cloudDiscoveryMetadata, }; } export { Authority, buildStaticAuthorityOptions, formatAuthorityUri, getTenantFromAuthorityString }; //# sourceMappingURL=Authority.mjs.map