Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
d8cac48
Add SN/I certificate support over mTLS Proof-of-Possession (PoP)
Robbie-Microsoft Jul 1, 2026
150a554
Extract FIC Leg 2 (mTLS PoP) into a follow-up PR
Robbie-Microsoft Jul 6, 2026
68d3715
Suppress CredScan finding for self-signed mTLS test certificate
Robbie-Microsoft Jul 6, 2026
66d59db
Fix mTLS PoP socket factory on JDK 8 and address review feedback
Robbie-Microsoft Jul 7, 2026
e6dee5f
Build mTLS PoP socket factory from a live key manager (support non-ex…
Robbie-Microsoft Jul 7, 2026
288df02
Point MtlsPopIT at the SN/I-allow-listed app for mTLS PoP
Robbie-Microsoft Jul 7, 2026
6bf6024
Fail closed on mTLS PoP token_type downgrade
Robbie-Microsoft Jul 8, 2026
f848b54
Remove unused telemetryValue from TokenType
Robbie-Microsoft Jul 8, 2026
3ff929c
Align mTLS PoP with MSAL.NET: sovereign-cloud guard, cache metadata, …
Robbie-Microsoft Jul 8, 2026
3ef7681
Align mTLS PoP cloud eligibility with MSAL.NET: allow Azure Gov + cur…
Robbie-Microsoft Jul 8, 2026
d576db6
Wrap non-base64 x5c decode as CertificateException in mTLS binding cert
Robbie-Microsoft Jul 8, 2026
683e54e
Harden SNI mTLS PoP E2E: prove real resource usability, skip off-CI, …
Robbie-Microsoft Jul 30, 2026
3858dc3
Assert mtls_pop directly in E2E; drop skip-on-downgrade escape hatch
Robbie-Microsoft Jul 30, 2026
d22059a
Exercise regional mtlsauth endpoint on the deterministic Bearer E2E cell
Robbie-Microsoft Jul 30, 2026
48fd92d
Merge origin/dev into rginsburg/sni-mtls-pop (resync mTLS PoP with base)
Robbie-Microsoft Jul 30, 2026
29fc91b
Surface Graph error body on non-2xx in mTLS PoP resource-call helper
Robbie-Microsoft Jul 30, 2026
1805895
Set explicit Accept header in mTLS PoP resource call to avoid Graph 400
Robbie-Microsoft Jul 31, 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
3 changes: 3 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
- Acquire access tokens for protected APIs (Microsoft Graph, custom APIs)
- Token caching and automatic refresh
- Support for various authentication flows (interactive, silent, client credentials, on-behalf-of, device code, managed identity)
- mTLS Proof-of-Possession (PoP) tokens for confidential clients using an SN/I certificate (see Client Credentials)
- Multi-cloud and B2C support

### Repository Structure
Expand Down Expand Up @@ -137,6 +138,8 @@ MSAL4J supports multiple authentication flows, each with a public `*Parameters`
- **Parameters**: `ClientCredentialParameters` - App-only authentication (daemon apps)
- **Internal**: `ClientCredentialRequest` → `AcquireTokenByClientCredentialSupplier`
- **Key Classes**: `IClientCredential`, `ClientSecret`, `ClientCertificate`, `ClientAssertion`
- **mTLS Proof-of-Possession (PoP)**: Opt in with `ClientCredentialParameters.builder(...).mtlsProofOfPossession()` to obtain an mTLS-bound PoP token (`token_type=mtls_pop`). The app's SN/I `IClientCertificate` is presented as the client TLS certificate to a rewritten `mtlsauth.*` endpoint (no `client_assertion` on the direct path) instead of signing an x5c assertion (the existing SNI+Bearer path is unchanged). Requires a tenanted authority and an ESTS allow-listed resource; region is optional (global `mtlsauth.microsoft.com` when absent). The result exposes `metadata().tokenType()` and `metadata().bindingCertificate()` (public material only — x5c chain + `x5t#S256`).
- **Key Classes**: `TokenType`, `BindingCertificate`, `MtlsClientCertificateHelper`, `MtlsEndpointHelper`; internal `AuthScheme`. Not supported: US Gov / China clouds, and user-scoped (`user_fic`) FIC over mTLS.

**On-Behalf-Of (OBO)**
- **Public API**: `acquireToken(OnBehalfOfParameters)`
Expand Down
6 changes: 5 additions & 1 deletion build/credscan-exclude.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
{
"tool": "Credential Scanner",
"suppressions": [
{
"file": "msal4j-sdk/src/test/resources/mtls_test_cert.p12",
"_justification": "Self-signed, test-only certificate (CN=msal4j-mtls-test) used by unit tests for mTLS Proof-of-Possession. Contains no production secret."
}
]
}
}
1 change: 1 addition & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
Version 1.25.1
=============
- Add SN/I certificate support over mTLS Proof-of-Possession (PoP) for confidential clients, presenting the certificate as the client TLS cert to obtain an mTLS-bound token. The response token_type is validated, so a request that is not honored as mtls_pop fails closed rather than surfacing an unbound token

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changes in this PR definitely weren't released a month ago.

- Add claimsFromClient API for client-originated claims on confidential-client flows (#1039)
- Migrate region discovery to IMDS /compute JSON endpoint (#1038)
- Bump Azure Arc HIMDS api-version from 2019-11-01 to 2020-06-01 (#1045)
Expand Down
74 changes: 74 additions & 0 deletions msal4j-sdk/docs/mtls-pop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# SN/I certificate over mTLS Proof-of-Possession (PoP)

MSAL4J lets a confidential-client application present its **Subject-Name/Issuer (SN/I) certificate as
the client TLS certificate** during the mutual-TLS handshake to the token endpoint. Entra ID (ESTS)
then returns an access token that is **cryptographically bound to that certificate**
(`token_type=mtls_pop`, bound via `cnf` / `x5t#S256`), instead of a plain Bearer token.

The credential is exactly the same certificate you already use for SN/I authentication — only the
mechanism changes: instead of signing a `private_key_jwt` (x5c) client assertion, the certificate is
presented on the TLS handshake.

> mTLS PoP is a **confidential-client** feature. It is distinct from the broker-based Signed-HTTP-Request
> (SHR) PoP used by public clients (`token_type=pop`), which is unaffected.

## Enabling it

Opt in per request with `ClientCredentialParameters.Builder.mtlsProofOfPossession()`:

```java
ConfidentialClientApplication cca =
ConfidentialClientApplication.builder(CLIENT_ID, sniCertificate)
.authority("https://login.microsoftonline.com/<tenant>/") // tenanted authority required
// .azureRegion("westus") // OPTIONAL — omit for the global mtlsauth.microsoft.com endpoint
.build();

ClientCredentialParameters parameters =
ClientCredentialParameters
.builder(Collections.singleton("https://graph.microsoft.com/.default"))
.mtlsProofOfPossession() // request an mTLS-bound PoP token
.build();

IAuthenticationResult result = cca.acquireToken(parameters).join();

result.metadata().tokenType(); // TokenType.MTLS_POP
result.metadata().bindingCertificate(); // public material only: x5c chain + x5t#S256 thumbprint
```

The resulting token is bound to the certificate. To call a protected resource, present the **same
certificate** on the TLS handshake and send the token with the `mtls_pop` authorization scheme:

```
Authorization: mtls_pop <access_token>
```

`BindingCertificate` exposes public material only (the `x5c` chain and the SHA-256 thumbprint); the
private key is never exported — it is used in place through its own provider.

## Bearer vs. mTLS PoP

Without `mtlsProofOfPossession()`, the SN/I certificate behaves exactly as before: it **signs and sends
a `private_key_jwt` client assertion**, and the result is a `TokenType.BEARER` token with no binding
certificate. Omitting the client assertion (and presenting the certificate on the TLS handshake
instead) is **specific to the mTLS PoP path**; the legacy SNI + Bearer flow is unchanged.

Bearer and mTLS-PoP tokens for the same scope are cached separately (keyed on
`{token_type + certificate key id}`), so enabling PoP never aliases an existing Bearer entry.

## Requirements and notes

- **Tenanted authority is required.** `/common` and `/organizations` are rejected on the mTLS PoP path.
- **Region is optional.** Omit `azureRegion(...)` to use the global `mtlsauth.microsoft.com` endpoint
(production-ready via ESTS-R regional failover), or set a region to target
`<region>.mtlsauth.microsoft.com` (recommended when you know your region).
- **Resource audience must be allow-listed.** ESTS gates mTLS PoP on the **final resource audience**,
which must be an ESTS mTLS-PoP allow-listed resource (for example Azure Key Vault or Microsoft Graph),
not the client app itself.
- **Cloud support.** Azure Public, Azure Government, and the current national clouds are supported (the
`login.*` authority host is rewritten to the matching `mtlsauth.*` host). Two legacy sovereign hosts
that have no `mtlsauth.*` endpoint — `login.usgovcloudapi.net` and `login.chinacloudapi.cn` — are not
supported and fail fast.

See also the runnable sample:
[`src/samples/confidential-client/ClientCredentialMtlsProofOfPossession.java`](../src/samples/confidential-client/ClientCredentialMtlsProofOfPossession.java),
and https://aka.ms/msal4j-pop.
Loading
Loading