Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
99b1e95
updates
lalimasharda Apr 27, 2026
a384103
remove flag for issuer validation
lalimasharda Apr 28, 2026
4e84bbc
Merge branch 'dev' of https://github.com/AzureAD/microsoft-authentica…
lalimasharda Apr 28, 2026
7dd0e7f
updating checks
lalimasharda Apr 28, 2026
c003152
Change files
lalimasharda Apr 28, 2026
3dd43a8
Update lib/msal-common/src/authority/Authority.ts
lalimasharda Apr 28, 2026
7bc4022
Update lib/msal-common/src/authority/Authority.ts
lalimasharda Apr 28, 2026
a576d94
Merge branch 'dev' of https://github.com/AzureAD/microsoft-authentica…
lalimasharda Apr 28, 2026
85b9302
Add unit tests for validateIssuer in Authority.spec.ts
Copilot Apr 28, 2026
d13bfb6
Remove package-lock.json changes from PR
Copilot Apr 28, 2026
c1ab91e
Merge branch 'dev' into add_issuer_validation_v5
lalimasharda Apr 28, 2026
58cf58b
updates
lalimasharda Apr 28, 2026
cfd8b98
Merge branch 'add_issuer_validation_v5' of https://github.com/AzureAD…
lalimasharda Apr 28, 2026
743aeb3
Refactor validateIssuer into dedicated rule-checker methods with URL-…
Copilot Apr 30, 2026
9ef58ae
Fix ReDoS in issuerMatchesCiamTenantPattern and safe-guard array access
Copilot Apr 30, 2026
a534242
Optimize trailing-slash stripping to use single slice instead of per-…
Copilot Apr 30, 2026
f256a0b
Revert last 2 commits (ReDoS fix and trailing-slash optimization) per…
Copilot Apr 30, 2026
c40d445
addressing comments
lalimasharda Apr 30, 2026
9b6b1ba
resolving merge conflicts
lalimasharda Apr 30, 2026
b8de0a4
updates
lalimasharda May 1, 2026
ecc5a85
Merge branch 'dev' into add_issuer_validation_v5
lalimasharda May 1, 2026
60c2555
format:fix
lalimasharda May 1, 2026
0e14d60
Merge branch 'add_issuer_validation_v5' of https://github.com/AzureAD…
lalimasharda May 1, 2026
6443480
Fix CIAM Rule 4: use PathSegments[0], add onmicrosoft.com patterns, g…
Copilot May 1, 2026
868b408
Clarify comments in matchesCiamTenantPattern
Copilot May 1, 2026
eb4db2e
updates
lalimasharda May 1, 2026
a32c8a7
updates
lalimasharda May 1, 2026
1a7f37b
updated errors.md
lalimasharda May 1, 2026
c72316f
Merge branch 'dev' into add_issuer_validation_v5
lalimasharda May 4, 2026
1e48cdd
Merge branch 'dev' of https://github.com/AzureAD/microsoft-authentica…
lalimasharda May 6, 2026
1cbdd68
updated issuer URL to use new URL api instead of old URLString api
lalimasharda May 6, 2026
df1d46a
Merge branch 'add_issuer_validation_v5' of https://github.com/AzureAD…
lalimasharda May 6, 2026
68110fc
api doc update
lalimasharda May 6, 2026
a846d68
format fix
lalimasharda May 6, 2026
bea0376
Merge branch 'dev' of https://github.com/AzureAD/microsoft-authentica…
lalimasharda May 7, 2026
ca31979
updating tests
lalimasharda May 7, 2026
5d9581a
updating a code comment
lalimasharda May 7, 2026
06a128b
apiReview and doc update
lalimasharda May 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "Add issuer validation check on OIDC discovery from network [#8570](https://github.com/AzureAD/microsoft-authentication-library-for-js/pull/8570)",
"packageName": "@azure/msal-common",
"email": "lalimasharda@microsoft.com",
"dependentChangeType": "patch"
}
3 changes: 3 additions & 0 deletions docs/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,9 @@ This error occurs when MSAL.js surpasses the allotted storage limit when attempt
### `invalid_request_method_for_EAR`
- The EAR protocol cannot be used with HTTP method `GET`. The `httpMethod` parameter in all requests using `protocolMode: ProtocolMode.EAR` must be either unset or `"POST"`/`HttpMethod.POST`.

### `issuer_validation_failed`
- Issuer returned from OpenID configuration endpoint does not match with the authority configured by the application.

## Interaction required errors

### `no_tokens_found`
Expand Down
19 changes: 13 additions & 6 deletions lib/msal-common/apiReview/msal-common.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1581,7 +1581,8 @@ declare namespace ClientConfigurationErrorCodes {
cannotSetOIDCOptions,
cannotAllowPlatformBroker,
authorityMismatch,
invalidRequestMethodForEAR
invalidRequestMethodForEAR,
issuerValidationFailed
}
}
export { ClientConfigurationErrorCodes }
Expand Down Expand Up @@ -2901,6 +2902,11 @@ function isServerTelemetryEntity(key: string, entity?: object): boolean;
// @public
function isSingleTenant(accountEntity: AccountEntity): boolean;

// Warning: (ae-missing-release-tag) "issuerValidationFailed" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
const issuerValidationFailed = "issuer_validation_failed";

// Warning: (tsdoc-escape-greater-than) The ">" character should be escaped using a backslash to avoid confusion with an HTML tag
// Warning: (tsdoc-html-tag-missing-greater-than) The HTML tag has invalid syntax: Expecting an attribute or ">" or "/>"
// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
Expand Down Expand Up @@ -4822,11 +4828,12 @@ const X_MS_LIB_CAPABILITY_VALUE: string;
// src/authority/Authority.ts:464:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// src/authority/Authority.ts:465:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// src/authority/Authority.ts:500:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// src/authority/Authority.ts:579:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// src/authority/Authority.ts:653:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// src/authority/Authority.ts:691:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// src/authority/Authority.ts:802:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// src/authority/Authority.ts:1000:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// src/authority/Authority.ts:582:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// src/authority/Authority.ts:656:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// src/authority/Authority.ts:694:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// src/authority/Authority.ts:805:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// src/authority/Authority.ts:1003:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// src/authority/Authority.ts:1218:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// src/authority/AuthorityOptions.ts:25:5 - (ae-forgotten-export) The symbol "CloudInstanceDiscoveryResponse" needs to be exported by the entry point index.d.ts
// src/cache/CacheManager.ts:355:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// src/cache/CacheManager.ts:356:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
Expand Down
8 changes: 8 additions & 0 deletions lib/msal-common/docs/authority.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ The authority URL guides MSAL where to look for the 3 endpoints that are require

> :bulb: Certain OAuth 2.0 grants may skip the authorize endpoint and go directly for the token endpoint, e.g. [OAuth 2.0 Client Credentials Grant](https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow)

## Issuer validation

When MSAL retrieves the OpenID configuration document from the network, it validates the `issuer` field returned by the IdP against the configured authority, per the [OpenID Connect Discovery 1.0 spec](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationValidation). This protects against accepting metadata from a malicious or misconfigured service that hosts an OpenID configuration document under an unrelated domain. The issuer is accepted when its scheme and host (and port) match the configured authority, or — for Microsoft cloud authorities — when it is HTTPS and its host is a known Microsoft authority host (including regional variants and `{tenant}.ciamlogin.com` patterns).

If the issuer does not satisfy these conditions, MSAL throws a `ClientConfigurationError` with error code `issuer_validation_failed` and the authentication flow is aborted. This validation is applied only to OpenID configuration documents fetched from the network — cached, hardcoded, and config-supplied metadata are not re-validated.

> Warning: An IdP whose `issuer` does not satisfy the conditions above will fail discovery. If you are using a non-Microsoft OIDC provider whose issuer does not exactly match the authority host you configured, ensure the authority you pass to MSAL has the same scheme and host as the value the IdP returns in its discovery document.

## Authority configuration

In MSAL, authority can be set in 2 locations:
Expand Down
191 changes: 189 additions & 2 deletions lib/msal-common/src/authority/Authority.ts
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,7 @@ export class Authority {
}

/**
* Returns metadata entity from cache if it exists, otherwiser returns a new metadata entity built
* Returns metadata entity from cache if it exists, otherwise returns a new metadata entity built
* from the configured canonical authority
* @returns
*/
Expand Down Expand Up @@ -547,6 +547,9 @@ export class Authority {
this.correlationId
)();
if (metadata) {
// Validate the issuer returned by the OIDC discovery document.
this.validateIssuer(metadata.issuer);

Comment thread
lalimasharda marked this conversation as resolved.
Comment thread
lalimasharda marked this conversation as resolved.
// If the user prefers to use an azure region replace the global endpoints with regional information.
if (this.authorityOptions.azureRegionConfiguration?.azureRegion) {
metadata = await invokeAsync(
Expand Down Expand Up @@ -839,7 +842,7 @@ export class Authority {
metadataEntity: AuthorityMetadataEntity
): Constants.AuthorityMetadataSource | null {
this.logger.verbose(
"Attempting to get cloud discovery metadata from authority configuration",
"Attempting to get cloud discovery metadata from authority configuration",
this.correlationId
);
this.logger.verbosePii(
Expand Down Expand Up @@ -1195,6 +1198,190 @@ export class Authority {
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.
*/
private validateIssuer(issuer: string): void {
if (!issuer) {
throw createClientConfigurationError(
ClientConfigurationErrorCodes.issuerValidationFailed
);
}

// Parse with the WHATWG URL API. URL normalizes scheme + host to lowercase per RFC 3986.
let issuerUrl: URL;
try {
issuerUrl = new URL(issuer);
} catch {
throw createClientConfigurationError(
ClientConfigurationErrorCodes.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(
ClientConfigurationErrorCodes.issuerValidationFailed
);
}

/**
* Rule 1: The issuer scheme + host (and port) match the authority's. Path
* may differ. Applies to all authorities.
*/
private matchesAuthorityOrigin(
issuerScheme: string,
issuerHost: string,
authorityScheme: string,
authorityHost: string
): boolean {
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`.
*/
private matchesRegionalMicrosoftHost(issuerHost: string): boolean {
const firstDot = issuerHost.indexOf(".");
if (firstDot > 0 && firstDot < issuerHost.length - 1) {
const hostWithoutRegion = issuerHost.substring(firstDot + 1);
return this.isAliasOfKnownMicrosoftAuthority(hostWithoutRegion);
Comment thread
lalimasharda marked this conversation as resolved.
}
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.
*/
private matchesCiamTenantPattern(
issuerUrl: URL,
authorityHost: string,
authorityPathSegments: string[]
): boolean {
/*
* 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(Constants.AAD_TENANT_DOMAIN_SUFFIX)
? pathSegment.slice(
0,
-Constants.AAD_TENANT_DOMAIN_SUFFIX.length
)
: pathSegment
: authorityHost.split(".")[0];

if (!tenantName) {
return false;
}

const ciamBaseURL = `https://${tenantName}${Constants.CIAM_AUTH_URL}`;
const validCiamPatterns: string[] = [
ciamBaseURL, // https://{tenant}.ciamlogin.com
`${ciamBaseURL}/${tenantName}`, // https://{tenant}.ciamlogin.com/{tenant}
`${ciamBaseURL}/${tenantName}/v2.0`, // https://{tenant}.ciamlogin.com/{tenant}/v2.0
`${ciamBaseURL}/${tenantName}${Constants.AAD_TENANT_DOMAIN_SUFFIX}`, // https://{tenant}.ciamlogin.com/{tenant}.onmicrosoft.com
`${ciamBaseURL}/${tenantName}${Constants.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
*
Expand Down
9 changes: 9 additions & 0 deletions lib/msal-common/src/authority/AuthorityMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,15 @@ export const rawMetdataJSON: RawMetadata = {
preferred_cache: "login.sovcloud-identity.sg",
aliases: ["login.sovcloud-identity.sg"],
},
{
preferred_network: "login.windows-ppe.net",
preferred_cache: "login.windows-ppe.net",
aliases: [
"login.windows-ppe.net",
"sts.windows-ppe.net",
"login.microsoft-ppe.com",
],
},
],
},
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@ export const cannotSetOIDCOptions = "cannot_set_OIDCOptions";
export const cannotAllowPlatformBroker = "cannot_allow_platform_broker";
export const authorityMismatch = "authority_mismatch";
export const invalidRequestMethodForEAR = "invalid_request_method_for_EAR";
export const issuerValidationFailed = "issuer_validation_failed";
Loading
Loading