Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,7 @@ AuthenticationResult execute() throws Exception {
requestAuthority = clientApplication.authenticationAuthority;
}

if (requestAuthority.authorityType == AuthorityType.AAD) {
requestAuthority = getAuthorityWithPrefNetworkHost(requestAuthority.authority());
}
requestAuthority = getAuthorityWithPrefNetworkHost(requestAuthority.authority());

try {
return clientApplication.acquireTokenCommon(msalRequest, requestAuthority);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,7 @@ class AcquireTokenSilentSupplier extends AuthenticationResultSupplier {
@Override
AuthenticationResult execute() throws Exception {
boolean shouldRefresh;
Authority requestAuthority = silentRequest.requestAuthority();
if (requestAuthority.authorityType != AuthorityType.B2C) {
requestAuthority =
getAuthorityWithPrefNetworkHost(silentRequest.requestAuthority().authority());
}
Authority requestAuthority = getAuthorityWithPrefNetworkHost(silentRequest.requestAuthority().authority());

AuthenticationResult res;
if (silentRequest.parameters().account() == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,21 @@ static void validateAuthority(URL authorityUrl) {
}
}

/**
* Creates a new Authority instance with a different tenant.
* This is useful when overriding the tenant at request level.
*
* @param originalAuthority The original authority to base the new one on
* @param newTenant The new tenant to use in the authority URL
* @return A new Authority instance with the specified tenant
*/
static Authority replaceTenant(Authority originalAuthority, String newTenant) throws MalformedURLException {
String authorityString = originalAuthority.canonicalAuthorityUrl().toString();
authorityString = authorityString.replace(originalAuthority.tenant, newTenant);

return createAuthority(new URL(authorityString));
}

static String getTenant(URL authorityUrl, AuthorityType authorityType) {
String[] segments = authorityUrl.getPath().substring(1).split("/");
if (authorityType == AuthorityType.B2C) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,71 @@
package com.microsoft.aad.msal4j;

import java.util.Objects;
import java.util.concurrent.Callable;

final class ClientAssertion implements IClientAssertion {

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

/**
* Constructor that accepts a static assertion string
*
* @param assertion The JWT assertion string to use
* @throws NullPointerException if assertion is null or empty
*/
ClientAssertion(final String assertion) {
if (StringHelper.isBlank(assertion)) {
throw new NullPointerException("assertion");
}

this.assertion = assertion;
this.assertionProvider = null;
}

/**
* Constructor that accepts a callable that provides the assertion string
*
* @param assertionProvider A callable that returns a JWT assertion string
* @throws NullPointerException if assertionProvider is null
*/
ClientAssertion(final Callable<String> assertionProvider) {
if (assertionProvider == null) {
throw new NullPointerException("assertionProvider");
}

this.assertion = null;
this.assertionProvider = assertionProvider;
}

/**
* Gets the JWT assertion for client authentication.
* If this ClientAssertion was created with a Callable, the callable will be
* invoked each time this method is called to generate a fresh assertion.
*
* @return A JWT assertion string
* @throws MsalClientException if the assertion provider returns null/empty or throws an exception
*/
public String assertion() {
if (assertionProvider != null) {
try {
String generatedAssertion = assertionProvider.call();

if (StringHelper.isBlank(generatedAssertion)) {
throw new MsalClientException(
"Assertion provider returned null or empty assertion",
AuthenticationErrorCode.INVALID_JWT);
}

return generatedAssertion;
} catch (MsalClientException ex) {
throw ex;
} catch (Exception ex) {
throw new MsalClientException(ex);
}
}

return this.assertion;
}

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

ClientAssertion other = (ClientAssertion) o;

// For assertion providers, we consider them equal if they're the same object
if (this.assertionProvider != null && other.assertionProvider != null) {
return this.assertionProvider == other.assertionProvider;
}

// For static assertions, compare the assertion strings
return Objects.equals(assertion(), other.assertion());
}

@Override
public int hashCode() {
// For assertion providers, use the provider's identity hash code
if (assertionProvider != null) {
return System.identityHashCode(assertionProvider);
}

// For static assertions, hash the assertion string
int result = 1;
result = result * 59 + (this.assertion == null ? 43 : this.assertion.hashCode());
return result;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,33 @@ public List<String> getEncodedPublicKeyCertificateChain() throws CertificateEnco
return result;
}

/**
* Gets a newly created JWT assertion using the certificate.
* <p>
* This method creates a fresh JWT assertion on each call, which prevents issues
* with token expiration and ensures each request has a unique assertion.
*
* @param authority The authority for which the assertion is being created, must not be null
* @param clientId The client ID of the application, used as the subject of the JWT
* @param sendX5c Whether to include the x5c claim (certificate chain) in the JWT
* @return A JWT assertion for client authentication
* @throws NullPointerException if authority is null
*/
public String getAssertion(Authority authority, String clientId, boolean sendX5c) {
if (authority == null) {
throw new NullPointerException("Authority cannot be null");
}

boolean useSha1 = Authority.detectAuthorityType(authority.canonicalAuthorityUrl()) == AuthorityType.ADFS;

return JwtHelper.buildJwt(
clientId,
this,
authority.selfSignedJwtAudience(),
sendX5c,
useSha1).assertion();
}

static ClientCertificate create(InputStream pkcs12Certificate, String password)
throws KeyStoreException, NoSuchProviderException, NoSuchAlgorithmException,
CertificateException, IOException, UnrecoverableKeyException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,15 +91,18 @@ public static IClientAssertion createFromClientAssertion(String clientAssertion)

/**
* Static method to create a {@link ClientAssertion} instance from a provided Callable.
* The callable will be invoked each time the assertion is needed, allowing for dynamic
* generation of assertions.
*
* @param callable Callable that produces a JWT token encoded as a base64 URL encoded string
* @return {@link ClientAssertion}
* @return {@link ClientAssertion} that will invoke the callable each time assertion() is called
* @throws NullPointerException if callable is null
*/
public static IClientAssertion createFromCallback(Callable<String> callable) throws ExecutionException, InterruptedException {
ExecutorService executor = Executors.newSingleThreadExecutor();

Future<String> future = executor.submit(callable);
public static IClientAssertion createFromCallback(Callable<String> callable) {
if (callable == null) {
throw new NullPointerException("callable");
}

return new ClientAssertion(future.get());
return new ClientAssertion(callable);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,14 @@
*/
public class ConfidentialClientApplication extends AbstractClientApplicationBase implements IConfidentialClientApplication {

private ClientCertificate clientCertificate;
String assertion;
String secret;
IClientCredential clientCredential;
private boolean sendX5c;

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

private boolean sendX5c;

@Override
public CompletableFuture<IAuthenticationResult> acquireToken(ClientCredentialParameters parameters) {
validateNotNull("parameters", parameters);
Expand Down Expand Up @@ -73,43 +70,11 @@ private ConfidentialClientApplication(Builder builder) {

log = LoggerFactory.getLogger(ConfidentialClientApplication.class);

initClientAuthentication(builder.clientCredential);
this.clientCredential = builder.clientCredential;

this.tenant = this.authenticationAuthority.tenant;
}

private void initClientAuthentication(IClientCredential clientCredential) {
validateNotNull("clientCredential", clientCredential);

if (clientCredential instanceof ClientSecret) {
this.secret = ((ClientSecret) clientCredential).clientSecret();
} else if (clientCredential instanceof ClientCertificate) {
this.clientCertificate = (ClientCertificate) clientCredential;
this.assertion = getAssertionString(clientCredential);
} else if (clientCredential instanceof ClientAssertion) {
this.assertion = getAssertionString(clientCredential);
} else {
throw new IllegalArgumentException("Unsupported client credential");
}
}

String getAssertionString(IClientCredential clientCredential) {
if (clientCredential instanceof ClientCertificate) {
boolean useSha1 = Authority.detectAuthorityType(this.authenticationAuthority.canonicalAuthorityUrl()) == AuthorityType.ADFS;

return JwtHelper.buildJwt(
clientId(),
clientCertificate,
this.authenticationAuthority.selfSignedJwtAudience(),
sendX5c,
useSha1).assertion();
} else if (clientCredential instanceof ClientAssertion) {
return ((ClientAssertion) clientCredential).assertion();
} else {
throw new IllegalArgumentException("Unsupported client credential");
}
}

/**
* Creates instance of Builder of ConfidentialClientApplication
*
Expand Down Expand Up @@ -137,6 +102,9 @@ public static class Builder extends AbstractClientApplicationBase.Builder<Builde

private Builder(String clientId, IClientCredential clientCredential) {
super(clientId);

validateNotNull("clientCredential", clientCredential);

this.clientCredential = clientCredential;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,25 +86,75 @@ private void addQueryParameters(OAuthHttpRequest oauthHttpRequest) {
String clientID = msalRequest.application().clientId();
queryParameters.put("client_id", clientID);

// If the client application has a client assertion to apply to the request, check if a new client assertion
// was supplied as a request parameter. If so, use the request's assertion instead of the application's
// Add client authentication parameters if this is a confidential client
if (msalRequest.application() instanceof ConfidentialClientApplication) {
if (msalRequest instanceof ClientCredentialRequest && ((ClientCredentialRequest) msalRequest).parameters.clientCredential() != null) {
IClientCredential credential = ((ClientCredentialRequest) msalRequest).parameters.clientCredential();
addJWTBearerAssertionParams(queryParameters, ((ConfidentialClientApplication) msalRequest.application()).getAssertionString(credential));
} else {
if (((ConfidentialClientApplication) msalRequest.application()).assertion != null) {
addJWTBearerAssertionParams(queryParameters, ((ConfidentialClientApplication) msalRequest.application()).assertion);
} else if (((ConfidentialClientApplication) msalRequest.application()).secret != null) {
// Client secrets have a different parameter than bearer assertions
queryParameters.put("client_secret", ((ConfidentialClientApplication) msalRequest.application()).secret);
ConfidentialClientApplication application = (ConfidentialClientApplication) msalRequest.application();

// Consolidated credential and tenant override handling
addCredentialToRequest(queryParameters, application);
}

oauthHttpRequest.setQuery(StringHelper.serializeQueryParameters(queryParameters));
}

/**
* Adds the appropriate authentication parameters to the request based on credential type.
* Handles different credential types (secret, assertion, certificate) by adding the appropriate
* parameters to the request.
*
* @param queryParameters The map of query parameters to add to
* @param application The confidential client application
*/
private void addCredentialToRequest(Map<String, String> queryParameters,
ConfidentialClientApplication application) {
IClientCredential credentialToUse = application.clientCredential;
Authority authorityToUse = application.authenticationAuthority;

// A ClientCredentialRequest may have parameters which override the credentials used to build the application.
if (msalRequest instanceof ClientCredentialRequest) {
ClientCredentialParameters parameters = ((ClientCredentialRequest) msalRequest).parameters;

if (parameters.clientCredential() != null) {
credentialToUse = parameters.clientCredential();
}

if (parameters.tenant() != null) {
try {
authorityToUse = Authority.replaceTenant(authorityToUse, parameters.tenant());
} catch (MalformedURLException e) {
log.warn("Could not create authority with tenant override: {}", e.getMessage());
}
}
}

oauthHttpRequest.setQuery(StringHelper.serializeQueryParameters(queryParameters));
// Quick return if no credential is provided
if (credentialToUse == null) {
return;
}

if (credentialToUse instanceof ClientSecret) {
// For client secret, add client_secret parameter
queryParameters.put("client_secret", ((ClientSecret) credentialToUse).clientSecret());
} else if (credentialToUse instanceof ClientAssertion) {
// For client assertion, add client_assertion and client_assertion_type parameters
addJWTBearerAssertionParams(queryParameters, ((ClientAssertion) credentialToUse).assertion());
} else if (credentialToUse instanceof ClientCertificate) {
// For client certificate, generate a new assertion and add it to the request
ClientCertificate certificate = (ClientCertificate) credentialToUse;
String assertion = certificate.getAssertion(
authorityToUse,
application.clientId(),
application.sendX5c());
addJWTBearerAssertionParams(queryParameters, assertion);
}
}

/**
* Adds the JWT bearer token assertion parameters to the request
*
* @param queryParameters The map of query parameters to add to
* @param assertion The JWT assertion string
*/
private void addJWTBearerAssertionParams(Map<String, String> queryParameters, String assertion) {
queryParameters.put("client_assertion", assertion);
queryParameters.put("client_assertion_type", ClientAssertion.ASSERTION_TYPE_JWT_BEARER);
Expand Down
Loading
Loading