Skip to content

Add SN/I certificate support over mTLS Proof-of-Possession (PoP)#1040

Open
Robbie-Microsoft wants to merge 6 commits into
devfrom
rginsburg/sni-mtls-pop
Open

Add SN/I certificate support over mTLS Proof-of-Possession (PoP)#1040
Robbie-Microsoft wants to merge 6 commits into
devfrom
rginsburg/sni-mtls-pop

Conversation

@Robbie-Microsoft

@Robbie-Microsoft Robbie-Microsoft commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

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).

Follow-up (stacked): the 2-leg Federated Identity Credential (FIC) over mTLS PoP has been split into its own PR — #1041 — per review feedback. This PR is now the core direct SN/I → mTLS PoP path (including FIC Leg 1, which reuses the same certificate code path).

Public API

// Direct SN/I cert -> mTLS-bound PoP token
IAuthenticationResult result = cca.acquireToken(
        ClientCredentialParameters.builder(Set.of("https://vault.azure.net/.default"))
                .mtlsProofOfPossession()          // opt in
                .build())
        .get();

result.metadata().tokenType();            // TokenType.MTLS_POP
result.metadata().bindingCertificate();   // public material only: x5c chain + x5t#S256
  • ClientCredentialParameters.Builder.mtlsProofOfPossession()
  • New public types: TokenType (BEARER / MTLS_POP), BindingCertificate (public material only), plus AuthenticationResultMetadata.tokenType() / bindingCertificate().

Internal changes

  • New: TokenType, BindingCertificate, MtlsClientCertificateHelper (mTLS socket factory, thumbprint, binding-cert resolution), MtlsEndpointHelper (login.*mtlsauth.* host rewrite; region optional → global mtlsauth.microsoft.com; tenanted-authority validation; sovereign-cloud fail-fast).
  • Request building (TokenRequestExecutor): the direct cert path omits client_assertion and presents the certificate on the TLS handshake; the result populates tokenType/bindingCertificate.
  • Transport: mTLS socket factory threaded through OAuthHttpRequest / HttpHelper / IHttpHelper via an injected mTLS DefaultHttpClient (MSAL owns the mTLS transport).
  • Cache isolation: access-token cache keyed on {token_type + cert KeyId} so Bearer/PoP (and different certs) never alias.
  • Telemetry: isolated 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

  • Unit: 3 new classes (MtlsEndpointHelperTest, MtlsClientCertificateHelperTest, MtlsProofOfPossessionTest) + a hermetic PKCS12 resource. Unit suite green (367 tests, 0 failures, 0 errors).
  • E2E: 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

  • New sample confidential-client/ClientCredentialMtlsProofOfPossession.java
  • changelog.txt and .github/copilot-instructions.md updated
  • Javadoc on all new public API

Out of scope / follow-ups

Risks / open questions

  • The telemetry wire header intentionally does not yet carry token type (would require an ESTS-coordinated schema bump).

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>
@Robbie-Microsoft Robbie-Microsoft requested a review from a team as a code owner July 1, 2026 19:41
@Robbie-Microsoft Robbie-Microsoft marked this pull request as draft July 1, 2026 20:51
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>

Copilot AI left a comment

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.

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 of client_assertion for 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.

Comment thread .github/copilot-instructions.md Outdated
Comment thread msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ClientCredentialParameters.java Outdated
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>

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 22 out of 23 changed files in this pull request and generated 3 comments.

Comment thread msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ClientCredentialParameters.java Outdated
- 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>
Robbie-Microsoft and others added 2 commits July 7, 2026 18:39
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants