Skip to content

Commit 9c03bc3

Browse files
authored
Merge pull request #986 from AzureAD/avdunn/fix-assertion-refresh
Refactor client credential behavior to be per-request
2 parents 5a4f9fc + cf22677 commit 9c03bc3

10 files changed

+530
-71
lines changed

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,7 @@ AuthenticationResult execute() throws Exception {
5252
requestAuthority = clientApplication.authenticationAuthority;
5353
}
5454

55-
if (requestAuthority.authorityType == AuthorityType.AAD) {
56-
requestAuthority = getAuthorityWithPrefNetworkHost(requestAuthority.authority());
57-
}
55+
requestAuthority = getAuthorityWithPrefNetworkHost(requestAuthority.authority());
5856

5957
try {
6058
return clientApplication.acquireTokenCommon(msalRequest, requestAuthority);

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

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,7 @@ class AcquireTokenSilentSupplier extends AuthenticationResultSupplier {
2424
@Override
2525
AuthenticationResult execute() throws Exception {
2626
boolean shouldRefresh;
27-
Authority requestAuthority = silentRequest.requestAuthority();
28-
if (requestAuthority.authorityType != AuthorityType.B2C) {
29-
requestAuthority =
30-
getAuthorityWithPrefNetworkHost(silentRequest.requestAuthority().authority());
31-
}
27+
Authority requestAuthority = getAuthorityWithPrefNetworkHost(silentRequest.requestAuthority().authority());
3228

3329
AuthenticationResult res;
3430
if (silentRequest.parameters().account() == null) {

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,21 @@ static void validateAuthority(URL authorityUrl) {
131131
}
132132
}
133133

134+
/**
135+
* Creates a new Authority instance with a different tenant.
136+
* This is useful when overriding the tenant at request level.
137+
*
138+
* @param originalAuthority The original authority to base the new one on
139+
* @param newTenant The new tenant to use in the authority URL
140+
* @return A new Authority instance with the specified tenant
141+
*/
142+
static Authority replaceTenant(Authority originalAuthority, String newTenant) throws MalformedURLException {
143+
String authorityString = originalAuthority.canonicalAuthorityUrl().toString();
144+
authorityString = authorityString.replace(originalAuthority.tenant, newTenant);
145+
146+
return createAuthority(new URL(authorityString));
147+
}
148+
134149
static String getTenant(URL authorityUrl, AuthorityType authorityType) {
135150
String[] segments = authorityUrl.getPath().substring(1).split("/");
136151
if (authorityType == AuthorityType.B2C) {

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

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,71 @@
44
package com.microsoft.aad.msal4j;
55

66
import java.util.Objects;
7+
import java.util.concurrent.Callable;
78

89
final class ClientAssertion implements IClientAssertion {
910

1011
static final String ASSERTION_TYPE_JWT_BEARER = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
1112
private final String assertion;
13+
private final Callable<String> assertionProvider;
1214

15+
/**
16+
* Constructor that accepts a static assertion string
17+
*
18+
* @param assertion The JWT assertion string to use
19+
* @throws NullPointerException if assertion is null or empty
20+
*/
1321
ClientAssertion(final String assertion) {
1422
if (StringHelper.isBlank(assertion)) {
1523
throw new NullPointerException("assertion");
1624
}
1725

1826
this.assertion = assertion;
27+
this.assertionProvider = null;
1928
}
2029

30+
/**
31+
* Constructor that accepts a callable that provides the assertion string
32+
*
33+
* @param assertionProvider A callable that returns a JWT assertion string
34+
* @throws NullPointerException if assertionProvider is null
35+
*/
36+
ClientAssertion(final Callable<String> assertionProvider) {
37+
if (assertionProvider == null) {
38+
throw new NullPointerException("assertionProvider");
39+
}
40+
41+
this.assertion = null;
42+
this.assertionProvider = assertionProvider;
43+
}
44+
45+
/**
46+
* Gets the JWT assertion for client authentication.
47+
* If this ClientAssertion was created with a Callable, the callable will be
48+
* invoked each time this method is called to generate a fresh assertion.
49+
*
50+
* @return A JWT assertion string
51+
* @throws MsalClientException if the assertion provider returns null/empty or throws an exception
52+
*/
2153
public String assertion() {
54+
if (assertionProvider != null) {
55+
try {
56+
String generatedAssertion = assertionProvider.call();
57+
58+
if (StringHelper.isBlank(generatedAssertion)) {
59+
throw new MsalClientException(
60+
"Assertion provider returned null or empty assertion",
61+
AuthenticationErrorCode.INVALID_JWT);
62+
}
63+
64+
return generatedAssertion;
65+
} catch (MsalClientException ex) {
66+
throw ex;
67+
} catch (Exception ex) {
68+
throw new MsalClientException(ex);
69+
}
70+
}
71+
2272
return this.assertion;
2373
}
2474

@@ -30,11 +80,24 @@ public boolean equals(Object o) {
3080
if (!(o instanceof ClientAssertion)) return false;
3181

3282
ClientAssertion other = (ClientAssertion) o;
83+
84+
// For assertion providers, we consider them equal if they're the same object
85+
if (this.assertionProvider != null && other.assertionProvider != null) {
86+
return this.assertionProvider == other.assertionProvider;
87+
}
88+
89+
// For static assertions, compare the assertion strings
3390
return Objects.equals(assertion(), other.assertion());
3491
}
3592

3693
@Override
3794
public int hashCode() {
95+
// For assertion providers, use the provider's identity hash code
96+
if (assertionProvider != null) {
97+
return System.identityHashCode(assertionProvider);
98+
}
99+
100+
// For static assertions, hash the assertion string
38101
int result = 1;
39102
result = result * 59 + (this.assertion == null ? 43 : this.assertion.hashCode());
40103
return result;

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,33 @@ public List<String> getEncodedPublicKeyCertificateChain() throws CertificateEnco
5555
return result;
5656
}
5757

58+
/**
59+
* Gets a newly created JWT assertion using the certificate.
60+
* <p>
61+
* This method creates a fresh JWT assertion on each call, which prevents issues
62+
* with token expiration and ensures each request has a unique assertion.
63+
*
64+
* @param authority The authority for which the assertion is being created, must not be null
65+
* @param clientId The client ID of the application, used as the subject of the JWT
66+
* @param sendX5c Whether to include the x5c claim (certificate chain) in the JWT
67+
* @return A JWT assertion for client authentication
68+
* @throws NullPointerException if authority is null
69+
*/
70+
public String getAssertion(Authority authority, String clientId, boolean sendX5c) {
71+
if (authority == null) {
72+
throw new NullPointerException("Authority cannot be null");
73+
}
74+
75+
boolean useSha1 = Authority.detectAuthorityType(authority.canonicalAuthorityUrl()) == AuthorityType.ADFS;
76+
77+
return JwtHelper.buildJwt(
78+
clientId,
79+
this,
80+
authority.selfSignedJwtAudience(),
81+
sendX5c,
82+
useSha1).assertion();
83+
}
84+
5885
static ClientCertificate create(InputStream pkcs12Certificate, String password)
5986
throws KeyStoreException, NoSuchProviderException, NoSuchAlgorithmException,
6087
CertificateException, IOException, UnrecoverableKeyException {

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

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -91,15 +91,18 @@ public static IClientAssertion createFromClientAssertion(String clientAssertion)
9191

9292
/**
9393
* Static method to create a {@link ClientAssertion} instance from a provided Callable.
94+
* The callable will be invoked each time the assertion is needed, allowing for dynamic
95+
* generation of assertions.
9496
*
9597
* @param callable Callable that produces a JWT token encoded as a base64 URL encoded string
96-
* @return {@link ClientAssertion}
98+
* @return {@link ClientAssertion} that will invoke the callable each time assertion() is called
99+
* @throws NullPointerException if callable is null
97100
*/
98-
public static IClientAssertion createFromCallback(Callable<String> callable) throws ExecutionException, InterruptedException {
99-
ExecutorService executor = Executors.newSingleThreadExecutor();
100-
101-
Future<String> future = executor.submit(callable);
101+
public static IClientAssertion createFromCallback(Callable<String> callable) {
102+
if (callable == null) {
103+
throw new NullPointerException("callable");
104+
}
102105

103-
return new ClientAssertion(future.get());
106+
return new ClientAssertion(callable);
104107
}
105108
}

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

Lines changed: 6 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,14 @@
1919
*/
2020
public class ConfidentialClientApplication extends AbstractClientApplicationBase implements IConfidentialClientApplication {
2121

22-
private ClientCertificate clientCertificate;
23-
String assertion;
24-
String secret;
22+
IClientCredential clientCredential;
23+
private boolean sendX5c;
2524

2625
/** AppTokenProvider creates a Credential from a function that provides access tokens. The function
2726
must be concurrency safe. This is intended only to allow the Azure SDK to cache MSI tokens. It isn't
2827
useful to applications in general because the token provider must implement all authentication logic. */
2928
public Function<AppTokenProviderParameters, CompletableFuture<TokenProviderResult>> appTokenProvider;
3029

31-
private boolean sendX5c;
32-
3330
@Override
3431
public CompletableFuture<IAuthenticationResult> acquireToken(ClientCredentialParameters parameters) {
3532
validateNotNull("parameters", parameters);
@@ -73,43 +70,11 @@ private ConfidentialClientApplication(Builder builder) {
7370

7471
log = LoggerFactory.getLogger(ConfidentialClientApplication.class);
7572

76-
initClientAuthentication(builder.clientCredential);
73+
this.clientCredential = builder.clientCredential;
7774

7875
this.tenant = this.authenticationAuthority.tenant;
7976
}
8077

81-
private void initClientAuthentication(IClientCredential clientCredential) {
82-
validateNotNull("clientCredential", clientCredential);
83-
84-
if (clientCredential instanceof ClientSecret) {
85-
this.secret = ((ClientSecret) clientCredential).clientSecret();
86-
} else if (clientCredential instanceof ClientCertificate) {
87-
this.clientCertificate = (ClientCertificate) clientCredential;
88-
this.assertion = getAssertionString(clientCredential);
89-
} else if (clientCredential instanceof ClientAssertion) {
90-
this.assertion = getAssertionString(clientCredential);
91-
} else {
92-
throw new IllegalArgumentException("Unsupported client credential");
93-
}
94-
}
95-
96-
String getAssertionString(IClientCredential clientCredential) {
97-
if (clientCredential instanceof ClientCertificate) {
98-
boolean useSha1 = Authority.detectAuthorityType(this.authenticationAuthority.canonicalAuthorityUrl()) == AuthorityType.ADFS;
99-
100-
return JwtHelper.buildJwt(
101-
clientId(),
102-
clientCertificate,
103-
this.authenticationAuthority.selfSignedJwtAudience(),
104-
sendX5c,
105-
useSha1).assertion();
106-
} else if (clientCredential instanceof ClientAssertion) {
107-
return ((ClientAssertion) clientCredential).assertion();
108-
} else {
109-
throw new IllegalArgumentException("Unsupported client credential");
110-
}
111-
}
112-
11378
/**
11479
* Creates instance of Builder of ConfidentialClientApplication
11580
*
@@ -137,6 +102,9 @@ public static class Builder extends AbstractClientApplicationBase.Builder<Builde
137102

138103
private Builder(String clientId, IClientCredential clientCredential) {
139104
super(clientId);
105+
106+
validateNotNull("clientCredential", clientCredential);
107+
140108
this.clientCredential = clientCredential;
141109
}
142110

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

Lines changed: 62 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -86,25 +86,75 @@ private void addQueryParameters(OAuthHttpRequest oauthHttpRequest) {
8686
String clientID = msalRequest.application().clientId();
8787
queryParameters.put("client_id", clientID);
8888

89-
// If the client application has a client assertion to apply to the request, check if a new client assertion
90-
// was supplied as a request parameter. If so, use the request's assertion instead of the application's
89+
// Add client authentication parameters if this is a confidential client
9190
if (msalRequest.application() instanceof ConfidentialClientApplication) {
92-
if (msalRequest instanceof ClientCredentialRequest && ((ClientCredentialRequest) msalRequest).parameters.clientCredential() != null) {
93-
IClientCredential credential = ((ClientCredentialRequest) msalRequest).parameters.clientCredential();
94-
addJWTBearerAssertionParams(queryParameters, ((ConfidentialClientApplication) msalRequest.application()).getAssertionString(credential));
95-
} else {
96-
if (((ConfidentialClientApplication) msalRequest.application()).assertion != null) {
97-
addJWTBearerAssertionParams(queryParameters, ((ConfidentialClientApplication) msalRequest.application()).assertion);
98-
} else if (((ConfidentialClientApplication) msalRequest.application()).secret != null) {
99-
// Client secrets have a different parameter than bearer assertions
100-
queryParameters.put("client_secret", ((ConfidentialClientApplication) msalRequest.application()).secret);
91+
ConfidentialClientApplication application = (ConfidentialClientApplication) msalRequest.application();
92+
93+
// Consolidated credential and tenant override handling
94+
addCredentialToRequest(queryParameters, application);
95+
}
96+
97+
oauthHttpRequest.setQuery(StringHelper.serializeQueryParameters(queryParameters));
98+
}
99+
100+
/**
101+
* Adds the appropriate authentication parameters to the request based on credential type.
102+
* Handles different credential types (secret, assertion, certificate) by adding the appropriate
103+
* parameters to the request.
104+
*
105+
* @param queryParameters The map of query parameters to add to
106+
* @param application The confidential client application
107+
*/
108+
private void addCredentialToRequest(Map<String, String> queryParameters,
109+
ConfidentialClientApplication application) {
110+
IClientCredential credentialToUse = application.clientCredential;
111+
Authority authorityToUse = application.authenticationAuthority;
112+
113+
// A ClientCredentialRequest may have parameters which override the credentials used to build the application.
114+
if (msalRequest instanceof ClientCredentialRequest) {
115+
ClientCredentialParameters parameters = ((ClientCredentialRequest) msalRequest).parameters;
116+
117+
if (parameters.clientCredential() != null) {
118+
credentialToUse = parameters.clientCredential();
119+
}
120+
121+
if (parameters.tenant() != null) {
122+
try {
123+
authorityToUse = Authority.replaceTenant(authorityToUse, parameters.tenant());
124+
} catch (MalformedURLException e) {
125+
log.warn("Could not create authority with tenant override: {}", e.getMessage());
101126
}
102127
}
103128
}
104129

105-
oauthHttpRequest.setQuery(StringHelper.serializeQueryParameters(queryParameters));
130+
// Quick return if no credential is provided
131+
if (credentialToUse == null) {
132+
return;
133+
}
134+
135+
if (credentialToUse instanceof ClientSecret) {
136+
// For client secret, add client_secret parameter
137+
queryParameters.put("client_secret", ((ClientSecret) credentialToUse).clientSecret());
138+
} else if (credentialToUse instanceof ClientAssertion) {
139+
// For client assertion, add client_assertion and client_assertion_type parameters
140+
addJWTBearerAssertionParams(queryParameters, ((ClientAssertion) credentialToUse).assertion());
141+
} else if (credentialToUse instanceof ClientCertificate) {
142+
// For client certificate, generate a new assertion and add it to the request
143+
ClientCertificate certificate = (ClientCertificate) credentialToUse;
144+
String assertion = certificate.getAssertion(
145+
authorityToUse,
146+
application.clientId(),
147+
application.sendX5c());
148+
addJWTBearerAssertionParams(queryParameters, assertion);
149+
}
106150
}
107151

152+
/**
153+
* Adds the JWT bearer token assertion parameters to the request
154+
*
155+
* @param queryParameters The map of query parameters to add to
156+
* @param assertion The JWT assertion string
157+
*/
108158
private void addJWTBearerAssertionParams(Map<String, String> queryParameters, String assertion) {
109159
queryParameters.put("client_assertion", assertion);
110160
queryParameters.put("client_assertion_type", ClientAssertion.ASSERTION_TYPE_JWT_BEARER);

0 commit comments

Comments
 (0)