33
44package com .microsoft .aad .msal4j ;
55
6- import com .nimbusds .oauth2 .sdk .auth .PrivateKeyJWT ;
76import com .nimbusds .jwt .SignedJWT ;
87import org .junit .jupiter .api .Test ;
98import org .junit .jupiter .api .TestInstance ;
109
1110import static org .junit .jupiter .api .Assertions .assertEquals ;
11+ import static org .junit .jupiter .api .Assertions .assertNotEquals ;
1212import static org .junit .jupiter .api .Assertions .assertNotNull ;
1313import static org .junit .jupiter .api .Assertions .assertNull ;
1414import static org .junit .jupiter .api .Assertions .assertThrows ;
15- import static org .junit .jupiter .api .Assertions .assertTrue ;
1615import static org .mockito .ArgumentMatchers .any ;
1716import static org .mockito .Mockito .*;
1817
1918import java .math .BigInteger ;
2019import java .security .*;
2120import java .security .cert .CertificateException ;
2221import java .security .interfaces .RSAPrivateKey ;
23- import java .text .ParseException ;
2422import java .util .*;
23+ import java .net .URLDecoder ;
24+ import java .nio .charset .StandardCharsets ;
2525
2626@ TestInstance (TestInstance .Lifecycle .PER_CLASS )
2727class ClientCertificateTest {
@@ -77,12 +77,17 @@ void testIClientCertificateInterface_CredentialFactoryUsesSha256() throws Except
7777 HttpRequest request = parameters .getArgument (0 );
7878 String requestBody = request .body ();
7979
80- SignedJWT signedJWT = SignedJWT . parse ( cca . getAssertionString () );
80+ String clientAssertion = extractClientAssertion ( requestBody );
8181
82- if (requestBody .contains (cca .getAssertionString ())
83- && signedJWT .getHeader ().toJSONObject ().containsKey ("x5t#S256" )) {
84- return TestHelper .expectedResponse (200 , TestHelper .getSuccessfulTokenResponse (tokenResponseValues ));
82+ if (clientAssertion != null ) {
83+ SignedJWT signedJWT = SignedJWT .parse (clientAssertion );
84+ if (signedJWT .getHeader ().toJSONObject ().containsKey ("x5t#S256" )) {
85+ return TestHelper .expectedResponse (200 , TestHelper .getSuccessfulTokenResponse (tokenResponseValues ));
86+ }
8587 }
88+
89+ //If the client assertion is null or does not contain the x5t#S256 header,
90+ // that indicates a problem in assertion generation and this test should fail.
8691 return null ;
8792 });
8893
@@ -94,6 +99,86 @@ void testIClientCertificateInterface_CredentialFactoryUsesSha256() throws Except
9499 assertEquals ("accessTokenSha256" , result .accessToken ());
95100 }
96101
102+ @ Test
103+ void testClientCertificate_GeneratesNewAssertionEachTime () throws Exception {
104+ DefaultHttpClient httpClientMock = mock (DefaultHttpClient .class );
105+ List <String > capturedAssertions = new ArrayList <>();
106+
107+ ConfidentialClientApplication cca =
108+ ConfidentialClientApplication .builder ("clientId" , ClientCredentialFactory .createFromCertificate (TestHelper .getPrivateKey (), TestHelper .getX509Cert ()))
109+ .authority ("https://login.microsoftonline.com/tenant" )
110+ .instanceDiscovery (false )
111+ .validateAuthority (false )
112+ .httpClient (httpClientMock )
113+ .build ();
114+
115+ // Mock the HTTP client to capture assertions from each request
116+ when (httpClientMock .send (any (HttpRequest .class ))).thenAnswer (parameters -> {
117+ HttpRequest request = parameters .getArgument (0 );
118+ String requestBody = request .body ();
119+
120+ String clientAssertion = extractClientAssertion (requestBody );
121+ if (clientAssertion != null ) {
122+ capturedAssertions .add (clientAssertion );
123+
124+ // Verify it's a valid JWT with proper headers
125+ SignedJWT signedJWT = SignedJWT .parse (clientAssertion );
126+ if (signedJWT .getHeader ().toJSONObject ().containsKey ("x5t#S256" )) {
127+ HashMap <String , String > tokenResponseValues = new HashMap <>();
128+ tokenResponseValues .put ("access_token" , "access_token_" + capturedAssertions .size ());
129+ return TestHelper .expectedResponse (200 , TestHelper .getSuccessfulTokenResponse (tokenResponseValues ));
130+ }
131+ }
132+ return null ;
133+ });
134+
135+ ClientCredentialParameters parameters = ClientCredentialParameters .builder (Collections .singleton ("scopes" )).skipCache (true ).build ();
136+
137+ // Make two token requests
138+ IAuthenticationResult result1 = cca .acquireToken (parameters ).get ();
139+ IAuthenticationResult result2 = cca .acquireToken (parameters ).get ();
140+
141+ // Verify two unique assertions were generated
142+ assertEquals (2 , capturedAssertions .size (), "Two assertions should have been generated" );
143+ assertNotEquals (capturedAssertions .get (0 ), capturedAssertions .get (1 ),
144+ "Each token request should generate a unique assertion" );
145+
146+ // Optional: Parse and verify JWT properties if needed
147+ SignedJWT jwt1 = SignedJWT .parse (capturedAssertions .get (0 ));
148+ SignedJWT jwt2 = SignedJWT .parse (capturedAssertions .get (1 ));
149+
150+ // Different JTI (JWT ID) should be used for each assertion
151+ assertNotEquals (jwt1 .getJWTClaimsSet ().getJWTID (), jwt2 .getJWTClaimsSet ().getJWTID (),
152+ "Each assertion should have a unique JTI claim" );
153+
154+ // Verify the tokens are different
155+ assertNotEquals (result1 .accessToken (), result2 .accessToken (),
156+ "The access tokens from each request should be different" );
157+ }
158+
159+ /**
160+ * Extracts the client_assertion value from a URL-encoded request body
161+ * @param requestBody The request body string
162+ * @return The extracted client assertion or null if not found
163+ */
164+ private String extractClientAssertion (String requestBody ) {
165+ try {
166+ // Split the request body into key-value pairs
167+ String [] pairs = requestBody .split ("&" );
168+ for (String pair : pairs ) {
169+ // Find the client_assertion parameter
170+ if (pair .startsWith ("client_assertion=" )) {
171+ // Extract and URL-decode the value
172+ return URLDecoder .decode (pair .substring ("client_assertion=" .length ()), StandardCharsets .UTF_8 .toString ());
173+ }
174+ }
175+ } catch (Exception e ) {
176+ // In case of any parsing errors
177+ System .err .println ("Error extracting client assertion: " + e .getMessage ());
178+ }
179+ return null ;
180+ }
181+
97182 class TestClientCredential implements IClientCertificate {
98183 @ Override
99184 public PrivateKey privateKey () {
0 commit comments