Add SN/I certificate support over mTLS Proof-of-Possession (PoP)#1040
Open
Robbie-Microsoft wants to merge 11 commits into
Open
Add SN/I certificate support over mTLS Proof-of-Possession (PoP)#1040Robbie-Microsoft wants to merge 11 commits into
Robbie-Microsoft wants to merge 11 commits into
Conversation
Allow a confidential-client app configured with a Subject-Name/Issuer (SN/I) certificate to obtain an mTLS-bound PoP access token from Entra ID by presenting that same certificate as the client TLS certificate in the mutual-TLS handshake to the token endpoint, instead of signing a private_key_jwt (x5c) client assertion.
Opt in via ClientCredentialParameters.builder(...).mtlsProofOfPossession(). Adds TokenType, BindingCertificate, MtlsClientCertificateHelper, and MtlsEndpointHelper; threads the mTLS socket factory through the HTTP layer; derives the mtlsauth.* endpoint (region optional); builds the mtls_pop request (direct cert -> no assertion; FIC Leg 2 -> jwt-pop with mtlsBindingCertificate); parses token_type and surfaces the public binding certificate; and isolates the cache on {token_type + cert KeyId}. Also covers the 2-leg FIC over mTLS PoP where both legs are cert-bound.
The existing SNI+Bearer (assertion) and broker SHR-PoP flows are unchanged. Includes unit tests, an E2E integration test (MtlsPopIT), a sample, and docs/changelog updates.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Per review feedback, FIC Leg 2 (binding an assertion-authenticated app to a certificate via mtlsBindingCertificate + client_assertion_type=jwt-pop) is moved to a separate stacked follow-up PR. This PR retains the core direct SN/I -> mTLS PoP path and FIC Leg 1. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Adds confidential-client support for acquiring mTLS Proof-of-Possession (PoP) tokens using an SN/I certificate presented on the TLS handshake (instead of signing a private_key_jwt), and surfaces token-binding metadata to callers.
Changes:
- Introduces new public result metadata (
TokenType,BindingCertificate,AuthenticationResultMetadata.tokenType()/bindingCertificate()). - Implements mTLS PoP request execution:
login.*→mtlsauth.*endpoint rewrite, mTLS socket factory transport injection, and omission ofclient_assertionfor direct certificate auth. - Adds cache-isolation dimensions and unit/integration tests, plus docs/sample updates.
Reviewed changes
Copilot reviewed 21 out of 22 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByClientCredentialSupplier.java | Stamps cert KeyId for cache isolation when mTLS PoP is requested. |
| msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AuthenticationErrorCode.java | Adds MTLS_POP_ERROR for mTLS PoP configuration/runtime failures. |
| msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AuthenticationResultMetadata.java | Adds token type + binding certificate fields and builder support. |
| msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/BindingCertificate.java | New public type exposing x5c chain + x5t#S256 (public material only). |
| msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ClientCredentialParameters.java | Adds mtlsProofOfPossession() opt-in and cache-key component support. |
| msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ClientCredentialRequest.java | Adds token_type=mtls_pop parameter when mTLS PoP is requested. |
| msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/HttpHelper.java | Exposes request-level HTTP execution overload used by mTLS path. |
| msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/IHttpHelper.java | Adds overload to execute requests with an explicit HTTP client + telemetry. |
| msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/MtlsClientCertificateHelper.java | Builds mTLS socket factory and derives binding-certificate public metadata. |
| msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/MtlsEndpointHelper.java | Rewrites standard token endpoint host to mtlsauth.* and validates tenant/cloud. |
| msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/OAuthHttpRequest.java | Routes token requests through an injected mTLS HTTP client when configured. |
| msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenRequestExecutor.java | Creates mTLS transport/endpoint and populates result metadata for mTLS PoP. |
| msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenResponse.java | Captures token_type from token responses. |
| msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenType.java | New public enum for BEARER vs MTLS_POP token types. |
| msal4j-sdk/src/integrationtest/java/com/microsoft/aad/msal4j/MtlsPopIT.java | CI-only E2E validation for direct SNI→mTLS PoP and cache isolation. |
| msal4j-sdk/src/samples/confidential-client/ClientCredentialMtlsProofOfPossession.java | New sample showing direct SN/I cert → mTLS PoP token acquisition. |
| msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/MtlsClientCertificateHelperTest.java | Unit tests for socket factory, thumbprint computation, and binding cert behavior. |
| msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/MtlsEndpointHelperTest.java | Unit tests for endpoint derivation and cloud/tenant validation. |
| msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/MtlsProofOfPossessionTest.java | Unit tests verifying mTLS request shaping and cache-key isolation behavior. |
| changelog.txt | Documents the new mTLS PoP SN/I confidential-client support. |
| .github/copilot-instructions.md | Updates repo guidance to include the new client-credentials mTLS PoP flow. |
The Unit Tests build's Credential Scanner (Guardian) gate flags the checked-in PKCS12 test cert (mtls_test_cert.p12), failing the build. It is a self-signed, test-only certificate (CN=msal4j-mtls-test) with no production secret, so allowlist it in build/credscan-exclude.json. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
- MtlsClientCertificateHelper: use a non-empty transient in-memory keystore password (JDK 8 PKCS12 setKeyEntry throws 'Key protection algorithm not found' NPE with an empty password) and guard against a null private key. Fixes the MtlsPopIT failures seen only in CI (JDK 8). - ClientCredentialParameters: make the cache-key fields volatile and rewrite bindingCertificateKeyId as synchronized copy-on-write so a reused params instance can't observe a partially-updated map/hash; correct the stale 'computed once / immutable' comments. - copilot-instructions.md: split the inline '- Key Classes' segment into its own nested bullet so it renders as a list item. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Robbie-Microsoft
added a commit
that referenced
this pull request
Jul 7, 2026
Pulls in the mTLS PoP socket-factory JDK 8 fix and the ClientCredentialParameters thread-safety/comment fixes from #1040. Resolved copilot-instructions.md to keep the FIC Leg 2 sentence alongside the Key Classes markdown split. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…portable keys) createMtlsSocketFactory imported the certificate's private key into an in-memory PKCS12 KeyStore via setKeyEntry. That serializes the key (PrivateKey.getEncoded()), which returns null for non-exportable OS/HSM key handles. On the Windows CI agent the lab cert is loaded from Windows-MY via SunMSCAPI, so all three MtlsPopIT tests failed with 'Key protection algorithm not found: java.lang.NullPointerException'. The earlier password change was a misdiagnosis; the password is irrelevant to this failure. Present the certificate through a custom X509ExtendedKeyManager that holds the live PrivateKey and chain, so the TLS handshake signs through the key's own provider (as JWT client-assertion signing already does). Works for both non-exportable and ordinary exportable keys. Add a regression unit test using a private key whose getEncoded()/getFormat() return null. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
ESTS only issues mTLS PoP tokens for the SN/I-allow-listed app in the MSI team tenant. MtlsPopIT authenticated as LabVaultAppID against microsoft.onmicrosoft.com, which is valid for Bearer client-credentials but not allow-listed for mTLS PoP, so ESTS rejected the PoP requests with AADSTS700025 (Client is public). Mirror MSAL .NET's ClientCredentialsMtlsPopTests: use the SN/I-allow-listed app (163ffef9-...), the MSI team tenant authority (bea21ebe-...), and region westus3. App/tenant IDs are public identifiers. The lab cert and Key Vault scope are unchanged. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Validate the identity provider's response token_type instead of assuming MTLS_POP from the request. When an mTLS Proof-of-Possession token is requested but ESTS returns a different token_type (e.g. a Bearer downgrade), the access token is not certificate-bound; MSAL now throws MsalClientException(token_type_mismatch) rather than mislabeling an unbound token as MTLS_POP. The binding certificate is only surfaced after the token type is validated, and the result token type is derived from the validated response so Bearer flows keep reporting Bearer. Adds AuthenticationErrorCode.TOKEN_TYPE_MISMATCH, a fail-closed unit test, a token_type-overridable test mock, and a skip-on-downgrade accommodation in MtlsPopIT (mirrors MSAL .NET's ExecuteOrInconclusiveOnTokenTypeMismatchAsync since the AAD test slice is a known intermittent downgrader). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The per-constant telemetry value was never wired into telemetry emission, so drop the field, constructor parameter, and accessor. It can be reintroduced if/when token_type telemetry is actually plumbed through. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…exact token_type Fix three divergences from the MSAL.NET reference in the direct SN/I -> mTLS Proof-of-Possession path: - MtlsEndpointHelper: enforce cloud boundaries so sovereign login.* hosts fail fast (via the authoritative TRUSTED_SOVEREIGN_HOSTS_SET, including regional forms) instead of being rewritten to the public mtlsauth.microsoft.com and having the client certificate presented cross-cloud. Other login.* hosts are rewritten domain-preserving (login -> mtlsauth) and non-login hosts are rejected. - AcquireTokenByClientCredentialSupplier: restore tokenType(MTLS_POP) and the binding certificate on cache hits, which previously defaulted to BEARER/null because only the network path stamped them. - TokenType.fromString: map only the exact (case-insensitive) mtls_pop token_type to MTLS_POP, so a non-certificate-bound "pop" response is correctly rejected by the mTLS PoP fail-closed check instead of masquerading as MTLS_POP. Adds regression tests for all three (378 msal4j-sdk unit tests green). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…rent national clouds Relax the mTLS PoP sovereign-cloud guardrail to match MSAL.NET's RegionAndMtlsDiscoveryProvider. isMtlsPoPUnsupportedCloud now denies only the two legacy sovereign hosts that have no mtlsauth.* endpoint (login.usgovcloudapi.net, login.chinacloudapi.cn). Every other login.* host -- including Azure Government and the current national clouds -- is allowed and rewritten domain-preserving (login -> mtlsauth), keeping each request within its own cloud boundary. This keeps the earlier domain-preserving rewrite (the actual cross-cloud leak fix) while removing the over-broad blanket sovereign rejection. Non-login. hosts remain rejected by deriveMtlsHost. MtlsEndpointHelperTest updated: the Azure Gov / current China / German / France cases now assert domain-preserving rewrite, and two legacy hosts assert fail-fast. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
decodeCertificateChain could throw a raw IllegalArgumentException when an x5c entry was not valid base64, bypassing the MTLS_POP_ERROR surface that callers expect. Catch it and rethrow as CertificateException so buildBindingCertificate/computeCertificateKeyId/createMtlsSocketFactory all report MsalClientException(MTLS_POP_ERROR). Adds regression tests. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
gladjohn
reviewed
Jul 10, 2026
| // SN/I-allow-listed app and MSI team tenant. mTLS PoP only works on this app/tenant pair; mirrors | ||
| // MSAL .NET's ClientCredentialsMtlsPopTests. App and tenant IDs are public identifiers, not secrets. | ||
| private static final String SNI_ALLOWLISTED_APP_ID = "163ffef9-a313-45b4-ab2f-c7e2f5e0e23e"; | ||
| private static final String SNI_ALLOWLISTED_AUTHORITY = |
Contributor
There was a problem hiding this comment.
we have so much reliance on this MSI team's test tenant. Can we have our own SNI test setup in IDLABS1?
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Lets a confidential-client app present its SN/I certificate as the client TLS certificate in the mTLS handshake to the token endpoint, so Entra ID (ESTS) returns a certificate-bound token (
token_type=mtls_pop,cnf/x5t#S256) instead of a Bearer token. The credential is identical to today's SNI + Bearer assertion flow — only the mechanism changes (assertion-signer → TLS client cert). Also covers FIC Leg 1, which reuses the same certificate path.Public API
ClientCredentialParameters.Builder.mtlsProofOfPossession()TokenType(BEARER/MTLS_POP) andBindingCertificate(public material only);AuthenticationResultMetadata.tokenType()/bindingCertificate().Internal changes
MtlsClientCertificateHelper(mTLS socket factory, thumbprint, binding-cert resolution) andMtlsEndpointHelper(login.*→mtlsauth.*host rewrite; region optional → globalmtlsauth.microsoft.com; tenanted authority required).mtlsauth.*endpoint —login.usgovcloudapi.netandlogin.chinacloudapi.cn— fail fast. Azure Government and the current national clouds are supported via a domain-preserving rewrite.TokenRequestExecutor): the direct-cert path omitsclient_assertionand presents the certificate on the TLS handshake; the result populatestokenType/bindingCertificate.OAuthHttpRequest/HttpHelpervia an injected mTLSDefaultHttpClient(MSAL owns the mTLS transport).{token_type + cert KeyId}so Bearer/PoP (and different certs) never alias.Backward compatibility
Existing SNI + Bearer (assertion) and broker SHR-PoP flows are unchanged, validated by the full unit suite.
Testing
MtlsEndpointHelperTest,MtlsClientCertificateHelperTest,MtlsProofOfPossessionTest) + a hermetic PKCS12 resource. Suite green (382 tests).MtlsPopIT— direct SNI→PoP (global + regional) and Bearer/PoP cache isolation. CI-only (needs the lab SN/I cert + an ESTS allow-listed resource).Docs
New sample
confidential-client/ClientCredentialMtlsProofOfPossession.java;changelog.txt,.github/copilot-instructions.md, and Javadoc on all new public API updated.Out of scope / follow-ups
user_fic) FIC over mTLS PoP.