2121import java .util .Collections ;
2222import java .util .concurrent .ExecutionException ;
2323
24+ import static com .microsoft .aad .msal4j .TestConstants .AGENTIC_GRAPH_SCOPE ;
2425import static com .microsoft .aad .msal4j .TestConstants .KEYVAULT_DEFAULT_SCOPE ;
2526import static org .junit .jupiter .api .Assertions .assertEquals ;
2627import static org .junit .jupiter .api .Assertions .assertFalse ;
2728import static org .junit .jupiter .api .Assertions .assertNotEquals ;
2829import static org .junit .jupiter .api .Assertions .assertNotNull ;
30+ import static org .junit .jupiter .api .Assertions .assertNull ;
2931
3032/**
3133 * End-to-end integration tests for SN/I certificate over mTLS Proof-of-Possession (PoP).
@@ -66,6 +68,11 @@ class MtlsPopIT {
6668 "https://login.microsoftonline.com/bea21ebe-8b64-4d06-9f6d-6a889b120a7c" ;
6769 private static final String TEST_SLICE_REGION = "westus3" ;
6870
71+ // mTLS-enabled MS Graph host (NOT plain graph.microsoft.com, which does not perform the client-cert
72+ // handshake). A token bound to the presented certificate is accepted here with HTTP 200.
73+ private static final String MTLS_GRAPH_RESOURCE =
74+ "https://mtlstb.graph.microsoft.com/v1.0/applications?$top=1" ;
75+
6976 private PrivateKey privateKey ;
7077 private X509Certificate publicCertificate ;
7178 private IClientCertificate certificate ;
@@ -79,39 +86,79 @@ void init() throws KeyStoreException, NoSuchProviderException, IOException,
7986 privateKey = (PrivateKey ) keystore .getKey (KeyVaultSecretsProvider .CERTIFICATE_ALIAS , null );
8087 publicCertificate = (X509Certificate ) keystore .getCertificate (KeyVaultSecretsProvider .CERTIFICATE_ALIAS );
8188
82- assertNotNull (privateKey , "Lab private key not found. Ensure the lab cert is installed." );
83- assertNotNull (publicCertificate , "Lab certificate not found. Ensure the lab cert is installed." );
89+ // These are live-lab E2E tests (like the other *IT classes). Off-CI the lab SN/I cert is absent
90+ // from the OS keystore, so SKIP the whole class rather than hard-failing it: a missing lab cert
91+ // is an environment condition, not a product defect.
92+ Assumptions .assumeTrue (privateKey != null && publicCertificate != null ,
93+ "Lab SN/I certificate not available (alias '" + KeyVaultSecretsProvider .CERTIFICATE_ALIAS
94+ + "'); skipping mTLS PoP E2E. Expected off-CI." );
8495
8596 certificate = ClientCredentialFactory .createFromCertificate (privateKey , publicCertificate );
8697 }
8798
8899 /**
89- * Direct SNI cert → mTLS PoP with <b>no region</b> (exercises the global
90- * {@code mtlsauth.microsoft.com} endpoint). The lab cert is presented as the client TLS certificate;
91- * the request carries {@code token_type=mtls_pop} and <b>no</b> client assertion. Requests an
92- * allow-listed resource (Key Vault) so ESTS issues the bound token.
100+ * <b>X509 SNI cert → mTLS PoP, proven end to end.</b> Canonical matrix cell
101+ * {@code Credential_X509_Output_Pop}: with <b>no region</b> configured (global
102+ * {@code mtlsauth.microsoft.com} endpoint), the lab cert is presented as the client TLS certificate
103+ * and the request carries {@code token_type=mtls_pop} with <b>no</b> client assertion.
104+ *
105+ * <p>Beyond asserting the token is issued and bound to the cert, this proves the bound token is
106+ * <b>actually usable</b>: it is presented (with the binding cert on the TLS handshake) to an
107+ * mTLS-enabled resource, which must return HTTP 200. A 401/403 would mean the certificate was not
108+ * presented or the {@code mtls_pop} scheme was wrong. The MS Graph scope is used because Graph is an
109+ * ESTS mTLS-PoP allow-listed resource whose {@code mtlstb.graph.microsoft.com} host performs the
110+ * client-cert handshake (the app must be granted Graph {@code Application.Read.All}).
93111 */
94112 @ Test
95- void acquireTokenClientCredentials_Certificate_MtlsPop () throws Exception {
113+ void Credential_X509_Output_Pop () throws Exception {
96114 ConfidentialClientApplication cca = ConfidentialClientApplication .builder (SNI_ALLOWLISTED_APP_ID , certificate )
97115 .authority (SNI_ALLOWLISTED_AUTHORITY ) // tenanted authority (required for mTLS PoP)
98116 .build ();
99117
100118 IAuthenticationResult result = acquireMtlsPopOrSkipOnDowngrade (cca , ClientCredentialParameters
101- .builder (Collections .singleton (KEYVAULT_DEFAULT_SCOPE ))
119+ .builder (Collections .singleton (AGENTIC_GRAPH_SCOPE ))
102120 .mtlsProofOfPossession ()
103121 .build ());
104122
105123 assertMtlsPopResult (result , expectedLabThumbprint ());
124+
125+ int status = MtlsResourceCaller .callResourceWithMtlsToken (
126+ MTLS_GRAPH_RESOURCE , result .accessToken (), certificate );
127+ assertEquals (200 , status ,
128+ "mTLS-enabled resource must accept the bound PoP token (HTTP 200); 401/403 means the "
129+ + "binding certificate was not presented on the handshake or the mtls_pop scheme was wrong" );
130+ }
131+
132+ /**
133+ * Canonical matrix cell {@code Credential_X509_Output_Bearer}: the same SN/I cert <b>without</b>
134+ * {@code mtlsProofOfPossession()} yields the existing {@code Bearer} token (the cert signs a
135+ * {@code private_key_jwt} client assertion) and exposes <b>no</b> binding certificate. This anchors
136+ * that opting out of mTLS PoP leaves the legacy SNI+Bearer behaviour intact.
137+ */
138+ @ Test
139+ void Credential_X509_Output_Bearer () throws Exception {
140+ ConfidentialClientApplication cca = ConfidentialClientApplication .builder (SNI_ALLOWLISTED_APP_ID , certificate )
141+ .authority (SNI_ALLOWLISTED_AUTHORITY )
142+ .build ();
143+
144+ IAuthenticationResult result = cca .acquireToken (ClientCredentialParameters
145+ .builder (Collections .singleton (KEYVAULT_DEFAULT_SCOPE ))
146+ .build ()) // no mtlsProofOfPossession() -> Bearer
147+ .get ();
148+
149+ assertNotNull (result .accessToken (), "Access token should not be null" );
150+ assertEquals (TokenType .BEARER , result .metadata ().tokenType (), "Result token type should be BEARER" );
151+ assertNull (result .metadata ().bindingCertificate (),
152+ "Bearer result must not expose a binding certificate" );
106153 }
107154
108155 /**
109- * Direct SNI cert → mTLS PoP with a region configured (exercises the regional
110- * {@code <region>.mtlsauth.microsoft.com} endpoint) , and verifies the bound token is cached and
111- * retrieved on a second call.
156+ * {@code Credential_X509_Output_Pop} over the regional endpoint: with a region configured the
157+ * request targets {@code <region>.mtlsauth.microsoft.com}, and the bound token is cached under
158+ * {@code {token_type + cert KeyId}} and returned on a second call.
112159 */
113160 @ Test
114- void acquireTokenClientCredentials_Certificate_MtlsPop_Regional () throws Exception {
161+ void Credential_X509_Output_Pop_Regional () throws Exception {
115162 ConfidentialClientApplication cca = ConfidentialClientApplication .builder (SNI_ALLOWLISTED_APP_ID , certificate )
116163 .authority (SNI_ALLOWLISTED_AUTHORITY )
117164 .azureRegion (TEST_SLICE_REGION )
@@ -140,7 +187,7 @@ void acquireTokenClientCredentials_Certificate_MtlsPop_Regional() throws Excepti
140187 * aliases the existing SNI+Bearer path.
141188 */
142189 @ Test
143- void acquireTokenClientCredentials_BearerAndMtlsPop_AreCacheIsolated () throws Exception {
190+ void Credential_X509_Output_Pop_And_Bearer_CacheIsolated () throws Exception {
144191 ConfidentialClientApplication cca = ConfidentialClientApplication .builder (SNI_ALLOWLISTED_APP_ID , certificate )
145192 .authority (SNI_ALLOWLISTED_AUTHORITY )
146193 .build ();
@@ -193,7 +240,6 @@ private void assertMtlsPopResult(IAuthenticationResult result, String expectedTh
193240
194241 BindingCertificate binding = result .metadata ().bindingCertificate ();
195242 assertNotNull (binding , "mTLS-PoP result must expose a binding certificate" );
196- assertNotNull (binding .thumbprintSha256 (), "Binding certificate must expose its SHA-256 thumbprint" );
197243 assertFalse (binding .certificateChain ().isEmpty (), "Binding certificate must expose its x5c chain" );
198244 assertEquals (expectedThumbprint , binding .thumbprintSha256 (),
199245 "Binding certificate thumbprint must match the lab SNI cert (x5t#S256)" );
0 commit comments