Skip to content

Commit a5b5b01

Browse files
committed
Improve assertion callback options
1 parent 4bf9dff commit a5b5b01

5 files changed

Lines changed: 382 additions & 6 deletions

File tree

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package com.microsoft.aad.msal4j;
5+
6+
import java.security.cert.X509Certificate;
7+
8+
/**
9+
* Container returned from context-aware assertion provider callbacks.
10+
* Allows the callback to supply both the client assertion JWT and an optional
11+
* token-binding certificate for mutual-TLS Proof-of-Possession (mTLS PoP) scenarios.
12+
*
13+
* <p>When a {@link #tokenBindingCertificate()} is provided, MSAL sets the
14+
* {@code client_assertion_type} to {@code urn:ietf:params:oauth:client-assertion-type:jwt-pop}
15+
* instead of the default {@code jwt-bearer}.</p>
16+
*
17+
* @see ClientCredentialFactory#createFromCallback(java.util.function.Function)
18+
* @see AssertionRequestOptions
19+
*/
20+
public class AssertionResponse {
21+
22+
private final String assertion;
23+
private final X509Certificate tokenBindingCertificate;
24+
25+
/**
26+
* Creates an AssertionResponse with just an assertion string (no token binding certificate).
27+
*
28+
* @param assertion the JWT assertion string
29+
*/
30+
public AssertionResponse(String assertion) {
31+
this(assertion, null);
32+
}
33+
34+
/**
35+
* Creates an AssertionResponse with an assertion string and an optional token-binding certificate.
36+
*
37+
* @param assertion the JWT assertion string
38+
* @param tokenBindingCertificate optional certificate for mTLS PoP binding, or null for standard jwt-bearer
39+
*/
40+
public AssertionResponse(String assertion, X509Certificate tokenBindingCertificate) {
41+
this.assertion = assertion;
42+
this.tokenBindingCertificate = tokenBindingCertificate;
43+
}
44+
45+
/**
46+
* Gets the JWT assertion string to use as the {@code client_assertion} parameter.
47+
*
48+
* @return the JWT assertion string
49+
*/
50+
public String assertion() {
51+
return assertion;
52+
}
53+
54+
/**
55+
* Gets the optional token-binding certificate for mutual-TLS Proof-of-Possession (mTLS PoP).
56+
* When present, MSAL uses {@code client_assertion_type=jwt-pop} instead of {@code jwt-bearer}.
57+
*
58+
* @return the binding certificate, or null if not using mTLS PoP
59+
*/
60+
public X509Certificate tokenBindingCertificate() {
61+
return tokenBindingCertificate;
62+
}
63+
}

msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ClientAssertion.java

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@
1010
final class ClientAssertion implements IClientAssertion {
1111

1212
static final String ASSERTION_TYPE_JWT_BEARER = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
13+
static final String ASSERTION_TYPE_JWT_POP = "urn:ietf:params:oauth:client-assertion-type:jwt-pop";
1314
private final String assertion;
1415
private final Callable<String> assertionProvider;
1516
private final Function<AssertionRequestOptions, String> contextAwareAssertionProvider;
17+
private final Function<AssertionRequestOptions, AssertionResponse> contextAwareResponseProvider;
1618

1719
/**
1820
* Constructor that accepts a static assertion string
@@ -28,6 +30,7 @@ final class ClientAssertion implements IClientAssertion {
2830
this.assertion = assertion;
2931
this.assertionProvider = null;
3032
this.contextAwareAssertionProvider = null;
33+
this.contextAwareResponseProvider = null;
3134
}
3235

3336
/**
@@ -44,6 +47,7 @@ final class ClientAssertion implements IClientAssertion {
4447
this.assertion = null;
4548
this.assertionProvider = assertionProvider;
4649
this.contextAwareAssertionProvider = null;
50+
this.contextAwareResponseProvider = null;
4751
}
4852

4953
/**
@@ -62,6 +66,27 @@ final class ClientAssertion implements IClientAssertion {
6266
this.assertion = null;
6367
this.assertionProvider = null;
6468
this.contextAwareAssertionProvider = contextAwareAssertionProvider;
69+
this.contextAwareResponseProvider = null;
70+
}
71+
72+
/**
73+
* Constructor that accepts a context-aware function returning an {@link AssertionResponse}.
74+
* This allows the callback to supply both the assertion JWT and an optional token-binding
75+
* certificate for mTLS PoP scenarios.
76+
*
77+
* @param contextAwareResponseProvider A function that receives context and returns an AssertionResponse
78+
* @throws NullPointerException if contextAwareResponseProvider is null
79+
*/
80+
ClientAssertion(final Function<AssertionRequestOptions, AssertionResponse> contextAwareResponseProvider,
81+
boolean responseProvider) {
82+
if (contextAwareResponseProvider == null) {
83+
throw new NullPointerException("contextAwareResponseProvider");
84+
}
85+
86+
this.assertion = null;
87+
this.assertionProvider = null;
88+
this.contextAwareAssertionProvider = null;
89+
this.contextAwareResponseProvider = contextAwareResponseProvider;
6590
}
6691

6792
/**
@@ -74,6 +99,11 @@ final class ClientAssertion implements IClientAssertion {
7499
* @throws MsalClientException if the assertion provider returns null/empty or throws an exception
75100
*/
76101
public String assertion() {
102+
if (contextAwareResponseProvider != null) {
103+
AssertionResponse response = assertionResponse(new AssertionRequestOptions(null, null, null));
104+
return response.assertion();
105+
}
106+
77107
if (contextAwareAssertionProvider != null) {
78108
return assertion(new AssertionRequestOptions(null, null, null));
79109
}
@@ -95,6 +125,11 @@ public String assertion() {
95125
* @throws MsalClientException if the assertion provider returns null/empty or throws an exception
96126
*/
97127
String assertion(AssertionRequestOptions options) {
128+
if (contextAwareResponseProvider != null) {
129+
AssertionResponse response = assertionResponse(options);
130+
return response.assertion();
131+
}
132+
98133
if (contextAwareAssertionProvider != null) {
99134
try {
100135
String generatedAssertion = contextAwareAssertionProvider.apply(options);
@@ -118,10 +153,40 @@ String assertion(AssertionRequestOptions options) {
118153
}
119154

120155
/**
121-
* Returns true if this assertion uses a context-aware provider.
156+
* Gets the full AssertionResponse from the context-aware response provider.
157+
* Returns null if this ClientAssertion does not use a response provider.
158+
*
159+
* @param options context information for the assertion request
160+
* @return An AssertionResponse, or null if not using a response provider
161+
* @throws MsalClientException if the provider returns null or throws an exception
162+
*/
163+
AssertionResponse assertionResponse(AssertionRequestOptions options) {
164+
if (contextAwareResponseProvider == null) {
165+
return null;
166+
}
167+
168+
try {
169+
AssertionResponse response = contextAwareResponseProvider.apply(options);
170+
171+
if (response == null || StringHelper.isBlank(response.assertion())) {
172+
throw new MsalClientException(
173+
"Assertion provider returned null or empty assertion",
174+
AuthenticationErrorCode.INVALID_JWT);
175+
}
176+
177+
return response;
178+
} catch (MsalClientException ex) {
179+
throw ex;
180+
} catch (Exception ex) {
181+
throw new MsalClientException(ex);
182+
}
183+
}
184+
185+
/**
186+
* Returns true if this assertion uses a context-aware provider (either string or response).
122187
*/
123188
boolean isContextAware() {
124-
return contextAwareAssertionProvider != null;
189+
return contextAwareAssertionProvider != null || contextAwareResponseProvider != null;
125190
}
126191

127192
private String invokeCallable() {
@@ -151,6 +216,11 @@ public boolean equals(Object o) {
151216

152217
ClientAssertion other = (ClientAssertion) o;
153218

219+
// For context-aware response providers, we consider them equal if they're the same object
220+
if (this.contextAwareResponseProvider != null && other.contextAwareResponseProvider != null) {
221+
return this.contextAwareResponseProvider == other.contextAwareResponseProvider;
222+
}
223+
154224
// For context-aware providers, we consider them equal if they're the same object
155225
if (this.contextAwareAssertionProvider != null && other.contextAwareAssertionProvider != null) {
156226
return this.contextAwareAssertionProvider == other.contextAwareAssertionProvider;
@@ -167,6 +237,11 @@ public boolean equals(Object o) {
167237

168238
@Override
169239
public int hashCode() {
240+
// For context-aware response providers, use the provider's identity hash code
241+
if (contextAwareResponseProvider != null) {
242+
return System.identityHashCode(contextAwareResponseProvider);
243+
}
244+
170245
// For context-aware providers, use the provider's identity hash code
171246
if (contextAwareAssertionProvider != null) {
172247
return System.identityHashCode(contextAwareAssertionProvider);

msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ClientCredentialFactory.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,4 +125,29 @@ public static IClientAssertion createFromCallback(Function<AssertionRequestOptio
125125

126126
return new ClientAssertion(assertionProvider);
127127
}
128+
129+
/**
130+
* Static method to create a {@link ClientAssertion} instance from a provided Function that
131+
* receives {@link AssertionRequestOptions} context and returns an {@link AssertionResponse}.
132+
* This overload allows the callback to supply both the assertion JWT and an optional
133+
* token-binding certificate for mTLS PoP scenarios.
134+
*
135+
* <p>When the returned {@link AssertionResponse} includes a
136+
* {@link AssertionResponse#tokenBindingCertificate()}, MSAL uses
137+
* {@code client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-pop}
138+
* instead of the default {@code jwt-bearer}.</p>
139+
*
140+
* @param assertionProvider Function that receives {@link AssertionRequestOptions} and produces
141+
* an {@link AssertionResponse} containing the assertion and optional certificate
142+
* @return {@link ClientAssertion} that will invoke the function each time assertion() is called
143+
* @throws NullPointerException if assertionProvider is null
144+
*/
145+
public static IClientAssertion createFromAssertionResponseCallback(
146+
Function<AssertionRequestOptions, AssertionResponse> assertionProvider) {
147+
if (assertionProvider == null) {
148+
throw new NullPointerException("assertionProvider");
149+
}
150+
151+
return new ClientAssertion(assertionProvider, true);
152+
}
128153
}

msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenRequestExecutor.java

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,6 @@ private void addCredentialToRequest(Map<String, String> queryParameters,
138138
} else if (credentialToUse instanceof ClientAssertion) {
139139
// For client assertion, add client_assertion and client_assertion_type parameters
140140
ClientAssertion clientAssertion = (ClientAssertion) credentialToUse;
141-
String assertionValue;
142141
if (clientAssertion.isContextAware()) {
143142
// Build assertion context with fmi_path if available
144143
String fmiPath = null;
@@ -156,11 +155,17 @@ private void addCredentialToRequest(Map<String, String> queryParameters,
156155
application.clientId(),
157156
tokenEndpoint,
158157
fmiPath);
159-
assertionValue = clientAssertion.assertion(options);
158+
159+
// Try to get the full AssertionResponse first
160+
AssertionResponse response = clientAssertion.assertionResponse(options);
161+
if (response != null) {
162+
addAssertionResponseParams(queryParameters, response);
163+
} else {
164+
addJWTBearerAssertionParams(queryParameters, clientAssertion.assertion(options));
165+
}
160166
} else {
161-
assertionValue = clientAssertion.assertion();
167+
addJWTBearerAssertionParams(queryParameters, clientAssertion.assertion());
162168
}
163-
addJWTBearerAssertionParams(queryParameters, assertionValue);
164169
} else if (credentialToUse instanceof ClientCertificate) {
165170
// For client certificate, generate a new assertion and add it to the request
166171
ClientCertificate certificate = (ClientCertificate) credentialToUse;
@@ -183,6 +188,23 @@ private void addJWTBearerAssertionParams(Map<String, String> queryParameters, St
183188
queryParameters.put("client_assertion_type", ClientAssertion.ASSERTION_TYPE_JWT_BEARER);
184189
}
185190

191+
/**
192+
* Adds assertion parameters from an AssertionResponse, using jwt-pop assertion type
193+
* when a token-binding certificate is present, or jwt-bearer otherwise.
194+
*
195+
* @param queryParameters The map of query parameters to add to
196+
* @param response The AssertionResponse containing the assertion and optional certificate
197+
*/
198+
private void addAssertionResponseParams(Map<String, String> queryParameters, AssertionResponse response) {
199+
queryParameters.put("client_assertion", response.assertion());
200+
201+
if (response.tokenBindingCertificate() != null) {
202+
queryParameters.put("client_assertion_type", ClientAssertion.ASSERTION_TYPE_JWT_POP);
203+
} else {
204+
queryParameters.put("client_assertion_type", ClientAssertion.ASSERTION_TYPE_JWT_BEARER);
205+
}
206+
}
207+
186208
private AuthenticationResult createAuthenticationResultFromOauthHttpResponse(HttpResponse oauthHttpResponse) {
187209
AuthenticationResult result;
188210

0 commit comments

Comments
 (0)