Add SN/I certificate support over mTLS Proof-of-Possession (PoP)#1040
Open
Robbie-Microsoft wants to merge 6 commits into
Open
Add SN/I certificate support over mTLS Proof-of-Possession (PoP)#1040Robbie-Microsoft wants to merge 6 commits into
Robbie-Microsoft wants to merge 6 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>
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
Adds support for using a Subject-Name/Issuer (SN/I) certificate as the first-leg credential over mTLS Proof-of-Possession (PoP).
Today an SN/I cert is used to sign a
private_key_jwt(x5c) client assertion, which yields a Bearer token (SNI + Bearer unchanged). This PR lets a confidential-client app present that same certificate as the client TLS certificate in the mutual-TLS handshake to the token endpoint, so Entra ID (ESTS) returns a token that is cryptographically bound to the cert (token_type=mtls_pop,cnf/x5t#S256). The credential is identical; only the mechanism changes (assertion-signer → TLS client cert).Public API
ClientCredentialParameters.Builder.mtlsProofOfPossession()TokenType(BEARER/MTLS_POP),BindingCertificate(public material only), plusAuthenticationResultMetadata.tokenType()/bindingCertificate().Internal changes
TokenType,BindingCertificate,MtlsClientCertificateHelper(mTLS socket factory, thumbprint, binding-cert resolution),MtlsEndpointHelper(login.*→mtlsauth.*host rewrite; region optional → globalmtlsauth.microsoft.com; tenanted-authority validation; sovereign-cloud fail-fast).TokenRequestExecutor): the direct cert path omitsclient_assertionand presents the certificate on the TLS handshake; the result populatestokenType/bindingCertificate.OAuthHttpRequest/HttpHelper/IHttpHelpervia an injected mTLSDefaultHttpClient(MSAL owns the mTLS transport).{token_type + cert KeyId}so Bearer/PoP (and different certs) never alias.TokenType.telemetryValue()(BEARER=2, MTLS_POP=6) for cross-SDK parity; the locked v5 wire header is intentionally left unchanged.Backward compatibility
The existing SNI + Bearer (assertion) and broker SHR-PoP flows are unchanged and remain byte-for-byte identical — validated by the full existing unit suite.
Testing
MtlsEndpointHelperTest,MtlsClientCertificateHelperTest,MtlsProofOfPossessionTest) + a hermetic PKCS12 resource. Unit suite green (367 tests, 0 failures, 0 errors).MtlsPopIT— direct SNI→PoP (global + regional) and Bearer/PoP cache isolation. CI-only: requires the lab SN/I cert (non-CNG) + an ESTS allow-listed resource; ESTS gates mTLS PoP on the final resource audience.Docs / samples
confidential-client/ClientCredentialMtlsProofOfPossession.javachangelog.txtand.github/copilot-instructions.mdupdatedOut of scope / follow-ups
user_fic) FIC over mTLS PoP.Risks / open questions