Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
8da3c10
Support SNI cert over mTLS PoP and S2S FIC; fix stale region-required…
Robbie-Microsoft Jul 1, 2026
82cedcb
Consolidate two-leg S2S FIC mTLS PoP E2E coverage and fix FMI leg cli…
Robbie-Microsoft Jul 6, 2026
0101442
Merge remote-tracking branch 'origin/main' into rginsburg/sni-mtls-po…
Robbie-Microsoft Jul 6, 2026
c5c0046
docs: clarify global mTLS host is cloud-specific; fix region example
Robbie-Microsoft Jul 7, 2026
043e1ef
Review nits: drop public-only host from region messages; reuse resour…
Robbie-Microsoft Jul 8, 2026
fd0077b
Address review: revert guardrail comment; simplify dead region-error …
Robbie-Microsoft Jul 9, 2026
e1f5aa9
docs/test: correct BindingCertificate wording; cloud-specific global …
Robbie-Microsoft Jul 9, 2026
7aaeca8
Rescope PR to tests + docs: move region-message fix to #6127
Robbie-Microsoft Jul 15, 2026
c923621
Merge remote-tracking branch 'origin/main' into rginsburg/sni-mtls-po…
Robbie-Microsoft Jul 15, 2026
9ec4cfa
Clarify allow-listed resource comment: constant is used by FIC tests;…
Robbie-Microsoft Jul 15, 2026
451c990
Call Microsoft Graph over mTLS in the SNI/FIC mTLS PoP E2E tests
Robbie-Microsoft Jul 27, 2026
7cbf4fe
Merge remote-tracking branch 'origin/main' into rginsburg/sni-mtls-po…
Robbie-Microsoft Jul 27, 2026
44169fb
Remove misdesigned FMI-audience mTLS PoP E2E test
Robbie-Microsoft Jul 27, 2026
4565fb1
Assert SNI/carried cert is presented on the mTLS handshake in unit tests
Robbie-Microsoft Jul 27, 2026
6be21b5
Fail (not skip) when the allow-listed app is rejected calling Graph o…
Robbie-Microsoft Jul 27, 2026
dbe7f5c
Truncate resource body in test log messages; fix region-error doc wor…
Robbie-Microsoft Jul 27, 2026
573e332
Tidy SNI/S2S-FIC mTLS PoP tests and docs per review
Robbie-Microsoft Jul 27, 2026
6f4f8af
Make Graph-over-mTLS resource check strictly 200-or-fail
Robbie-Microsoft Jul 27, 2026
621a288
Trim redundant comments in mTLS PoP E2E tests
Robbie-Microsoft Jul 27, 2026
9e00972
Collapse mTLS PoP E2E tests to 4 canonical credential x output cases
Robbie-Microsoft Jul 28, 2026
ba2e43b
Merge origin/main into rginsburg/sni-mtls-pop-fic
Robbie-Microsoft Jul 29, 2026
225599e
Fold mTLS PoP resource-call assert into an explicit 200 check
Robbie-Microsoft Jul 30, 2026
cb8eb69
Merge remote-tracking branch 'origin/main' into rginsburg/sni-mtls-po…
Robbie-Microsoft Jul 30, 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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
Unreleased
==========

### Changes
Comment thread
Robbie-Microsoft marked this conversation as resolved.
Outdated
- Documented that mTLS Proof-of-Possession does **not** require a region: when no region is set, MSAL uses the global `mtlsauth.microsoft.com` endpoint. Corrected the stale `RegionRequiredForMtlsPop` / `MtlsPopWithoutRegion` / `MtlsBearerWithoutRegion` error-message text and doc comments (the public constants are retained for API compatibility; they are no longer raised).
Comment thread
Robbie-Microsoft marked this conversation as resolved.
Outdated
- Added end-to-end and unit coverage for the two-leg S2S (app) Federated Identity Credential (FIC) flow over mTLS PoP (SNI first leg → federated assertion → resource token bound to the same certificate), including an FMI-audience variant. No public API change; `user_fic` is not supported over mTLS.

4.85.1
======

Expand Down
29 changes: 29 additions & 0 deletions docs/mtls_pop_skills_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,31 @@ Which frameworks and platforms support mTLS PoP in MSAL.NET?

Use these for token exchange and two-leg flows:

> **Scope note:** FIC over mTLS PoP is supported for **S2S / app (client-credentials)** flows only.
> A user-delegated **`user_fic`** flow is **not** supported over mTLS.

#### SNI First Leg (S2S FIC)

```
How do I use an SNI certificate as the first leg of S2S FIC over mTLS PoP?
```

```
Show me SNI first leg -> S2S FIC -> mtls_pop end to end in MSAL.NET
```

```
How do I carry the Leg-1 BindingCertificate into Leg 2 with WithClientAssertion?
```

```
Is user_fic supported over mTLS PoP? (No - S2S/app FIC only)
```

```
What exchange audience do I use - api://AzureADTokenExchange vs api://AzureFMITokenExchange?
```

#### Understanding FIC Two-Leg Flow

```
Expand Down Expand Up @@ -170,6 +195,10 @@ What do I do if I get "MtlsCertificateNotProvided" error?
What does "MtlsPopWithoutRegion" error mean?
```

```
Is a region required for mTLS PoP? (No - it is optional; MSAL falls back to the global mtlsauth.microsoft.com endpoint)
Comment thread
Robbie-Microsoft marked this conversation as resolved.
Outdated
```

```
How do I troubleshoot mTLS PoP token acquisition failures?
```
Expand Down
80 changes: 78 additions & 2 deletions docs/sni_mtls_pop_token_design.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,12 @@ Bearer tokens are vulnerable to theft. Proof-of-Possession (PoP) tokens mitigate

### MTLS Endpoint Usage

The mTLS PoP flow relies on tenanted endpoints with support for regional configurations. The behavior varies based on the cloud environment:
The mTLS PoP flow relies on tenanted endpoints. A region is **optional** — it is recommended for performance, but when no region is configured (or auto-detection yields none) MSAL falls back to the **global** mTLS endpoint, which is production-ready. The behavior varies based on the cloud environment:

#### Region is optional (global fallback)

- **Regional (recommended):** `https://{region}.mtlsauth.microsoft.com/{tenant_id}` — set via `.WithAzureRegion(...)`.
- **Global (no region):** `https://mtlsauth.microsoft.com/{tenant_id}` — used automatically when no region is set. There is **no "region required" error**; the earlier `RegionRequiredForMtlsPop` message is stale and retained only for API compatibility.
Comment thread
Robbie-Microsoft marked this conversation as resolved.
Outdated

#### Public Cloud

Expand Down Expand Up @@ -97,6 +102,77 @@ AuthenticationResult result = await app.AcquireTokenForClient(scopes).WithMtlsPr
.ExecuteAsync();
```

## Two-Leg S2S (App) FIC over mTLS PoP

A confidential-client app can use its SNI certificate as the **first leg** of a Federated Identity
Credential (FIC) exchange and obtain an **mTLS-bound PoP** token at the **second leg**. This reuses the
existing `WithClientAssertion(...)` seam that accepts a `ClientSignedAssertion` carrying an optional
`TokenBindingCertificate` — no new public API.

> **Scope:** FIC over mTLS PoP is supported for **S2S / app (client-credentials)** flows only.
> **`user_fic` (a user-delegated FIC) is _not_ supported over mTLS.**

- **Leg 1 — SNI cert → federated assertion (cert-bound PoP).** `AcquireTokenForClient` against the
exchange audience with `WithMtlsProofOfPossession()`. Leg 1 being cert-bound PoP is what produces the
`cnf`. The result exposes `AuthenticationResult.BindingCertificate` (public material only — x5c +
`x5t#S256`; never a private key).
Comment thread
Robbie-Microsoft marked this conversation as resolved.
Outdated
- **Leg 2 — federated assertion → final app token (bound to the Leg-1 cert).** `AcquireTokenForClient`
against the final resource, supplying the Leg-1 assertion **and** the Leg-1 binding certificate via
`ClientSignedAssertion.TokenBindingCertificate`, again with `WithMtlsProofOfPossession()`. MSAL sends
`client_assertion` + `client_assertion_type = urn:ietf:params:oauth:client-assertion-type:jwt-pop`
over mTLS, and the returned token is `mtls_pop`, bound to the **same** certificate as Leg 1.

```csharp
X509Certificate2 sniCert = GetCertificateFromStore("Use The Lab Auth Cert");

// Leg 1: SNI cert -> federated assertion (mtls_pop)
var leg1App = ConfidentialClientApplicationBuilder.Create(clientId)
.WithAuthority(authority)
.WithCertificate(sniCert, sendX5C: true)
.Build();

AuthenticationResult leg1 = await leg1App
.AcquireTokenForClient(new[] { "api://AzureADTokenExchange/.default" })
.WithMtlsProofOfPossession()
.ExecuteAsync();
// leg1.TokenType == "mtls_pop"; leg1.BindingCertificate == the SNI cert (public material).
Comment thread
Robbie-Microsoft marked this conversation as resolved.
Outdated

// Leg 2: carry the Leg-1 assertion + binding cert -> resource token (mtls_pop)
var leg2App = ConfidentialClientApplicationBuilder.Create(clientId)
.WithAuthority(authority)
.WithClientAssertion((options, ct) => Task.FromResult(new ClientSignedAssertion
{
Assertion = leg1.AccessToken, // the federated assertion
TokenBindingCertificate = leg1.BindingCertificate // carry the SAME cert forward
}))
.Build();

AuthenticationResult leg2 = await leg2App
.AcquireTokenForClient(new[] { "https://vault.azure.net/.default" })
.WithMtlsProofOfPossession()
.ExecuteAsync();
// leg2.TokenType == "mtls_pop", bound to leg1.BindingCertificate.Thumbprint.
```

### Exchange audience is caller-supplied

MSAL does **not** hardcode the exchange audience — it passes through whatever scope the caller requests
at Leg 1:

- **Generic S2S FIC:** `api://AzureADTokenExchange/.default`.
- **FMI variant:** `api://AzureFMITokenExchange/.default`, using the reserved client id
`urn:microsoft:identity:fmi`.

Both produce the same PoP outcome; the choice is the caller's.

### Transport ownership (mTLS requires MSAL's built-in handler)

mTLS requires MSAL to **own the HTTP transport handler** so it can attach the client certificate to the
outbound TLS connection. The built-in factory (`IMsalMtlsHttpClientFactory` / `SimpleHttpClientFactory`,
which is the default) does this. A caller-supplied **plain** `IMsalHttpClientFactory` (a bare
`HttpClient`) **cannot** carry the mTLS certificate — supply an mTLS-capable factory or rely on the
built-in transport.
Comment thread
Robbie-Microsoft marked this conversation as resolved.

## Tests to Validate mTLS PoP Tokens

### Certificate Validation Tests
Expand All @@ -114,7 +190,7 @@ AuthenticationResult result = await app.AcquireTokenForClient(scopes).WithMtlsPr

### Region Validation Tests

- Ensure `MsalClientException` is thrown when no region is set and `WithMtlsProofOfPossession()` is called.
- Ensure a **no-region** `WithMtlsProofOfPossession()` request succeeds against the **global** `mtlsauth.microsoft.com` endpoint (region is optional; there is no "region required" failure).
- Validate successful token acquisition with a specified region.
- Test auto-detected region functionality and confirm the expected region is used.
- Region is not required if the authority is DSTS.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,15 @@ internal class RegionAndMtlsDiscoveryProvider : IRegionDiscoveryProvider
public const string PublicEnvForRegional = "login.microsoft.com";
public const string PublicEnvForRegionalMtlsAuth = "mtlsauth.microsoft.com";

// Map of unsupported sovereign cloud hosts for mTLS PoP to their error messages
// ─────────────────────────────────────────────────────────────────────────────
// SOVEREIGN GUARDRAIL — single override point for mTLS cloud availability.
Comment thread
Robbie-Microsoft marked this conversation as resolved.
Outdated
Comment thread
Robbie-Microsoft marked this conversation as resolved.
Outdated
// mTLS PoP is currently rejected for sovereign clouds (US Gov, China) and any
// non-"login." host. mtlsauth.* is rolling out across clouds (Azure Government /
// AGC: available; Bleu / Delos: TBD). To enable a cloud, remove its entry here
// (and, if needed, relax the non-"login." host check below) — this is the ONLY
// place cloud eligibility is enforced, so do not scatter equivalent checks
// elsewhere in the codebase.
// ─────────────────────────────────────────────────────────────────────────────
Comment thread
Robbie-Microsoft marked this conversation as resolved.
Outdated
private static readonly Dictionary<string, string> s_unsupportedMtlsHosts =
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
Expand All @@ -32,7 +40,8 @@ public RegionAndMtlsDiscoveryProvider(IHttpManager httpManager)

public async Task<InstanceDiscoveryMetadataEntry> GetMetadataAsync(Uri authority, RequestContext requestContext)
{
// Fail fast: Check for unsupported mTLS hosts before any region discovery
// SOVEREIGN GUARDRAIL (see s_unsupportedMtlsHosts above): fail fast for clouds
// where mTLS PoP is not yet available, before any region discovery.
if (requestContext.IsMtlsRequested)
{
string host = authority.Host;
Expand Down
12 changes: 6 additions & 6 deletions src/client/Microsoft.Identity.Client/MsalError.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1185,14 +1185,14 @@ public static class MsalError
public const string CryptographicError = "cryptographic_error";

/// <summary>
/// <para>What happened?</para>mTLS Proof of Possession (mTLS PoP) is configured but a region was not specified.
/// <para>Mitigation</para>Ensure that the AzureRegion configuration is set when using mTLS PoP as it requires a regional endpoint.
/// <para>What happened?</para>Historical error code. A region is <b>not</b> required for mTLS Proof of Possession — when none is specified, MSAL uses the global mTLS endpoint (mtlsauth.microsoft.com). This code is no longer raised.
/// <para>Mitigation</para>No action needed. Optionally set AzureRegion for improved performance.
/// </summary>
public const string MtlsPopWithoutRegion = "mtls_pop_without_region";

/// <summary>
/// <para>What happened?</para>mTLS Bearer is configured but a region was not specified.
/// <para>Mitigation</para>Ensure that the AzureRegion configuration is set when using mTLS Bearer as it requires a regional endpoint.
/// <para>What happened?</para>Historical error code. A region is <b>not</b> required for Bearer-over-mTLS — when none is specified, MSAL uses the global mTLS endpoint (mtlsauth.microsoft.com). This code is no longer raised.
/// <para>Mitigation</para>No action needed. Optionally set AzureRegion for improved performance.
/// </summary>
public const string MtlsBearerWithoutRegion = "mtls_bearer_without_region";

Expand All @@ -1203,8 +1203,8 @@ public static class MsalError
public const string MtlsCertificateNotProvided = "mtls_certificate_not_provided";

/// <summary>
/// <para>What happened?</para> mTLS Proof of Possession (mTLS PoP) requires a specific Azure region to be specified.
/// <para>Mitigation:</para> Ensure that the AzureRegion configuration is set when using mTLS PoP as it requires a regional endpoint.
/// <para>What happened?</para> Historical error code. A region is <b>not</b> required for mTLS Proof of Possession (mTLS PoP). When regional auto-detection does not yield a region, MSAL falls back to the global mTLS endpoint (mtlsauth.microsoft.com). This code is no longer raised.
/// <para>Mitigation:</para> No action needed. Optionally set AzureRegion via WithAzureRegion for improved performance.
/// </summary>
public const string RegionRequiredForMtlsPop = "region_required_for_mtls_pop";

Expand Down
6 changes: 3 additions & 3 deletions src/client/Microsoft.Identity.Client/MsalErrorMessage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -437,15 +437,15 @@ public static string InvalidTokenProviderResponseValue(string invalidValueName)
public const string SetCiamAuthorityAtRequestLevelNotSupported = "Setting the CIAM authority (ex. \"{tenantName}.ciamlogin.com\") at the request level is not supported. The CIAM authority must be set during application creation";
public const string ClaimsChallenge = "The returned error contains a claims challenge. For additional info on how to handle claims related to multifactor authentication, Conditional Access, and incremental consent, see https://aka.ms/msal-conditional-access-claims. If you are using the On-Behalf-Of flow, see https://aka.ms/msal-conditional-access-claims-obo for details.";
public const string CryptographicError = "A cryptographic exception occurred. Possible cause: the certificate has been disposed. See inner exception for full details.";
public const string MtlsPopWithoutRegion = "mTLS Proof of Possession requires a region to be specified. Please set AzureRegion in the configuration at the application level.";
public const string MtlsPopWithoutRegion = "mTLS Proof-of-Possession does not require a region. When no region is specified, MSAL uses the global mTLS endpoint (mtlsauth.microsoft.com). Setting AzureRegion is recommended for performance. See https://aka.ms/msal-net-pop for details.";
public const string MtlsCertificateNotProvidedMessage = "mTLS Proof‑of‑Possession requires a certificate for this request. Either configure the application with .WithCertificate(...) or pass a certificate‑bound client‑assertion and chain .WithMtlsProofOfPossession() on the request builder. See https://aka.ms/msal-net-pop for details.";
public const string MtlsNotSupportedWithClientClaimsMessage = "mTLS transport (Proof-of-Possession via .WithMtlsProofOfPossession() or Bearer-over-mTLS via CertificateOptions.SendCertificateOverMtls = true) is not supported when the credential is configured via .WithClientClaims(certificate, claimsToSign, ...). Custom client claims require a signed JWT-bearer client assertion, which is not produced in mTLS mode. To use mTLS, configure the application with .WithCertificate(certificate) without .WithClientClaims(...), or use .WithClientAssertion(...) with a signed assertion that carries the binding certificate. See https://aka.ms/msal-net-pop for details.";
public const string MtlsBearerWithoutRegion = "mTLS Bearer requires a region to be specified. Please set AzureRegion in the configuration at the application level.";
public const string MtlsBearerWithoutRegion = "Bearer-over-mTLS does not require a region. When no region is specified, MSAL uses the global mTLS endpoint (mtlsauth.microsoft.com). Setting AzureRegion is recommended for performance. See https://aka.ms/msal-net-pop for details.";
public const string MtlsInvalidAuthorityTypeMessage = "mTLS PoP is only supported for AAD authority type. See https://aka.ms/msal-net-pop for details.";
public const string MtlsNonTenantedAuthorityNotAllowedMessage = "mTLS authentication requires a tenanted authority. Using 'common', 'organizations', or similar non-tenanted authorities is not allowed. Please provide an authority with a specific tenant ID (e.g., 'https://login.microsoftonline.com/{tenantId}'). See https://aka.ms/msal-net-pop for details.";
public const string MtlsNotSupportedForManagedIdentityMessage = "IMDSv2 flow is not supported on .NET Framework 4.6.2. Cryptographic operations required for managed identity authentication are unavailable on this platform.";
public const string MtlsNotSupportedForNonWindowsMessage = "mTLS PoP with Managed Identity is not supported on this OS. See https://aka.ms/msal-net-pop.";
public const string RegionRequiredForMtlsPopMessage = "Regional auto-detect failed. mTLS Proof-of-Possession requires a region to be specified, as there is no global endpoint for mTLS. See https://aka.ms/msal-net-pop for details.";
public const string RegionRequiredForMtlsPopMessage = "mTLS Proof-of-Possession does not require a region. When regional auto-detection does not yield a region, MSAL falls back to the global mTLS endpoint (mtlsauth.microsoft.com). Setting AzureRegion via WithAzureRegion is recommended for performance. See https://aka.ms/msal-net-pop for details.";
public const string MtlsPopNotSupportedForUsGovCloudApiMessage = "login.usgovcloudapi.net is not supported for mTLS PoP, please use login.microsoftonline.us";
public const string MtlsPopNotSupportedForChinaCloudApiMessage = "login.chinacloudapi.cn is not supported for mTLS PoP, please use login.partner.microsoftonline.cn";
public const string MtlsPopNotSupportedForNonLoginHostMessage = "mTLS PoP is only supported for hosts that start with 'login.'. The provided authority host does not meet this requirement. See https://aka.ms/msal-net-pop for details.";
Expand Down
Loading
Loading