Skip to content

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

Open
Robbie-Microsoft wants to merge 11 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 11 commits into
devfrom
rginsburg/sni-mtls-pop

Conversation

@Robbie-Microsoft

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

Copy link
Copy Markdown
Contributor

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.

2-leg FIC over mTLS PoP is split into stacked follow-up #1041 per review feedback.

Public API

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) and BindingCertificate (public material only); AuthenticationResultMetadata.tokenType() / bindingCertificate().

Internal changes

  • New: MtlsClientCertificateHelper (mTLS socket factory, thumbprint, binding-cert resolution) and MtlsEndpointHelper (login.*mtlsauth.* host rewrite; region optional → global mtlsauth.microsoft.com; tenanted authority required).
  • Cloud eligibility (matches MSAL.NET's denylist): only the two legacy sovereign hosts with no mtlsauth.* endpoint — login.usgovcloudapi.net and login.chinacloudapi.cn — fail fast. Azure Government and the current national clouds are supported via a domain-preserving rewrite.
  • Request building (TokenRequestExecutor): the direct-cert path omits client_assertion and presents the certificate on the TLS handshake; the result populates tokenType/bindingCertificate.
  • Transport: the mTLS socket factory is threaded through OAuthHttpRequest / HttpHelper via an injected mTLS DefaultHttpClient (MSAL owns the mTLS transport).
  • Cache isolation: the access-token cache is keyed on {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

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

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
Comment thread msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenRequestExecutor.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/TokenRequestExecutor.java Outdated
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 6 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>
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>

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 23 out of 24 changed files in this pull request and generated 2 comments.

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>
// 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 =

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.

we have so much reliance on this MSI team's test tenant. Can we have our own SNI test setup in IDLABS1?

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.

3 participants