diff --git a/msal4j-brokers/pom.xml b/msal4j-brokers/pom.xml index 060d756e..4e2140ce 100644 --- a/msal4j-brokers/pom.xml +++ b/msal4j-brokers/pom.xml @@ -5,7 +5,7 @@ 4.0.0 com.microsoft.azure msal4j-brokers - 0.0.1 + 1.0.0-beta jar msal4j-brokers @@ -26,11 +26,15 @@ UTF-8 - com.microsoft.azure msal4j - 1.13.2 + 1.13.4 + + + com.microsoft.azure + javamsalruntime + 0.13.4 org.projectlombok @@ -38,6 +42,23 @@ 1.18.6 provided + + org.testng + testng + 7.1.0 + test + + + org.slf4j + slf4j-api + 1.7.36 + + + ch.qos.logback + logback-classic + 1.2.3 + test + @@ -60,7 +81,6 @@ - ${project.build.directory}/delombok org.projectlombok diff --git a/msal4j-brokers/src/main/java/com/microsoft/aad/msal4jbrokers/MSALRuntimeBroker.java b/msal4j-brokers/src/main/java/com/microsoft/aad/msal4jbrokers/MSALRuntimeBroker.java deleted file mode 100644 index 598b83ac..00000000 --- a/msal4j-brokers/src/main/java/com/microsoft/aad/msal4jbrokers/MSALRuntimeBroker.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.microsoft.aad.msal4jbrokers; - -import com.microsoft.aad.msal4j.*; -import lombok.extern.slf4j.Slf4j; - -import java.util.concurrent.CompletableFuture; - -@Slf4j -public class MSALRuntimeBroker implements IBroker { - - @Override - public IAuthenticationResult acquireToken(PublicClientApplication application, SilentParameters requestParameters) { - log.debug("Should not call this API if msal runtime init failed"); - throw new MsalClientException("Broker implementation missing", "missing_broker"); - } - - @Override - public IAuthenticationResult acquireToken(PublicClientApplication application, InteractiveRequestParameters requestParameters) { - throw new MsalClientException("Broker implementation missing", "missing_broker"); - } - - @Override - public IAuthenticationResult acquireToken(PublicClientApplication application, UserNamePasswordParameters requestParameters) { - throw new MsalClientException("Broker implementation missing", "missing_broker"); - } - - @Override - public CompletableFuture removeAccount(IAccount account) { - throw new MsalClientException("Broker implementation missing", "missing_broker"); - } -} diff --git a/msal4j-brokers/src/main/java/com/microsoft/aad/msal4jbrokers/MsalRuntimeBroker.java b/msal4j-brokers/src/main/java/com/microsoft/aad/msal4jbrokers/MsalRuntimeBroker.java new file mode 100644 index 00000000..68000997 --- /dev/null +++ b/msal4j-brokers/src/main/java/com/microsoft/aad/msal4jbrokers/MsalRuntimeBroker.java @@ -0,0 +1,179 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.aad.msal4jbrokers; + +import com.microsoft.aad.msal4j.IAuthenticationResult; +import com.microsoft.aad.msal4j.IBroker; +import com.microsoft.aad.msal4j.InteractiveRequestParameters; +import com.microsoft.aad.msal4j.PublicClientApplication; +import com.microsoft.aad.msal4j.SilentParameters; +import com.microsoft.aad.msal4j.UserNamePasswordParameters; +import com.microsoft.aad.msal4j.MsalClientException; +import com.microsoft.aad.msal4j.AuthenticationErrorCode; +import com.microsoft.aad.msal4j.IAccount; +import com.microsoft.azure.javamsalruntime.Account; +import com.microsoft.azure.javamsalruntime.AuthParameters; +import com.microsoft.azure.javamsalruntime.AuthResult; +import com.microsoft.azure.javamsalruntime.MsalInteropException; +import com.microsoft.azure.javamsalruntime.MsalRuntimeInterop; +import com.microsoft.azure.javamsalruntime.ReadAccountResult; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +public class MsalRuntimeBroker implements IBroker { + private static final Logger LOG = LoggerFactory.getLogger(MsalRuntimeBroker.class); + + private static MsalRuntimeInterop interop; + + static { + try { + //MsalRuntimeInterop performs various initialization steps in a similar static block, + // so when an MsalRuntimeBroker is created this will cause the interop layer to initialize + interop = new MsalRuntimeInterop(); + } catch (MsalInteropException e) { + throw new MsalClientException(String.format("Could not initialize MSALRuntime: %s", e.getErrorMessage()), AuthenticationErrorCode.MSALRUNTIME_INTEROP_ERROR); + } + } + + @Override + public CompletableFuture acquireToken(PublicClientApplication application, SilentParameters parameters) { + Account accountResult = null; + + //If request has an account ID, MSALRuntime likely has data cached for that account that we can retrieve + if (parameters.account() != null) { + try { + accountResult = ((ReadAccountResult) interop.readAccountById(parameters.account().homeAccountId(), application.correlationId()).get()).getAccount(); + } catch (InterruptedException | ExecutionException ex) { + throw new MsalClientException(String.format("MSALRuntime async operation interrupted when waiting for result: %s", ex.getMessage()), AuthenticationErrorCode.MSALRUNTIME_INTEROP_ERROR); + } + } + + try { + AuthParameters authParameters = new AuthParameters + .AuthParametersBuilder(application.clientId(), + application.authority(), + String.join(" ", parameters.scopes())) + .build(); + + if (accountResult == null) { + return interop.signInSilently(authParameters, application.correlationId()) + .thenCompose(acctResult -> interop.acquireTokenSilently(authParameters, application.correlationId(), ((AuthResult) acctResult).getAccount())) + .thenApply(authResult -> parseBrokerAuthResult( + application.authority(), + ((AuthResult) authResult).getIdToken(), + ((AuthResult) authResult).getAccessToken(), + ((AuthResult) authResult).getAccount().getAccountId(), + ((AuthResult) authResult).getAccount().getClientInfo(), + ((AuthResult) authResult).getAccessTokenExpirationTime())); + } else { + return interop.acquireTokenSilently(authParameters, application.correlationId(), accountResult) + .thenApply(authResult -> parseBrokerAuthResult(application.authority(), + ((AuthResult) authResult).getIdToken(), + ((AuthResult) authResult).getAccessToken(), + ((AuthResult) authResult).getAccount().getAccountId(), + ((AuthResult) authResult).getAccount().getClientInfo(), + ((AuthResult) authResult).getAccessTokenExpirationTime()) + + ); + } + } catch (MsalInteropException interopException) { + throw new MsalClientException(interopException.getErrorMessage(), AuthenticationErrorCode.MSALRUNTIME_INTEROP_ERROR); + } + } + + @Override + public CompletableFuture acquireToken(PublicClientApplication application, InteractiveRequestParameters parameters) { + try { + AuthParameters authParameters = new AuthParameters + .AuthParametersBuilder(application.clientId(), + application.authority(), + String.join(" ", parameters.scopes())) + .build(); + + return interop.signInInteractively(parameters.windowHandle(), authParameters, application.correlationId(), parameters.loginHint()) + .thenCompose(acctResult -> interop.acquireTokenInteractively(parameters.windowHandle(), authParameters, application.correlationId(), ((AuthResult) acctResult).getAccount())) + .thenApply(authResult -> parseBrokerAuthResult( + application.authority(), + ((AuthResult) authResult).getIdToken(), + ((AuthResult) authResult).getAccessToken(), + ((AuthResult) authResult).getAccount().getAccountId(), + ((AuthResult) authResult).getAccount().getClientInfo(), + ((AuthResult) authResult).getAccessTokenExpirationTime()) + ); + } catch (MsalInteropException interopException) { + throw new MsalClientException(interopException.getErrorMessage(), AuthenticationErrorCode.MSALRUNTIME_INTEROP_ERROR); + } + } + + /** + * @deprecated + */ + @Deprecated + @Override + public CompletableFuture acquireToken(PublicClientApplication application, UserNamePasswordParameters parameters) { + try { + AuthParameters authParameters = + new AuthParameters + .AuthParametersBuilder(application.clientId(), + application.authority(), + String.join(" ", parameters.scopes())) + .build(); + + authParameters.setUsernamePassword(parameters.username(), new String(parameters.password())); + + return interop.signInSilently(authParameters, application.correlationId()) + .thenCompose(acctResult -> interop.acquireTokenSilently(authParameters, application.correlationId(), ((AuthResult) acctResult).getAccount())) + .thenApply(authResult -> parseBrokerAuthResult( + application.authority(), + ((AuthResult) authResult).getIdToken(), + ((AuthResult) authResult).getAccessToken(), + ((AuthResult) authResult).getAccount().getAccountId(), + ((AuthResult) authResult).getAccount().getClientInfo(), + ((AuthResult) authResult).getAccessTokenExpirationTime())); + } catch (MsalInteropException interopException) { + throw new MsalClientException(interopException.getErrorMessage(), AuthenticationErrorCode.MSALRUNTIME_INTEROP_ERROR); + } + } + + @Override + public void removeAccount(PublicClientApplication application, IAccount msalJavaAccount) { + try { + Account msalRuntimeAccount = ((ReadAccountResult) interop.readAccountById(msalJavaAccount.homeAccountId(), application.correlationId()).get()).getAccount(); + + if (msalRuntimeAccount != null) { + interop.signOutSilently(application.clientId(), application.correlationId(), msalRuntimeAccount); + } + } catch (MsalInteropException interopException) { + throw new MsalClientException(interopException.getErrorMessage(), AuthenticationErrorCode.MSALRUNTIME_INTEROP_ERROR); + } catch (InterruptedException | ExecutionException ex) { + throw new MsalClientException(String.format("MSALRuntime async operation interrupted when waiting for result: %s", ex.getMessage()), AuthenticationErrorCode.MSALRUNTIME_INTEROP_ERROR); + } + } + + /** + * Calls MSALRuntime's startup API. If MSALRuntime started successfully, we can assume that the broker is available for use. + * + * If an exception is thrown when trying to start MSALRuntime, we assume that we cannot use the broker and will not make any more attempts to do so. + * + * @return boolean representing whether or not MSALRuntime started successfully + */ + @Override + public boolean isBrokerAvailable() { + try { + interop.startupMsalRuntime(); + + LOG.info("MSALRuntime started successfully. MSAL Java will use MSALRuntime in all supported broker flows."); + + return true; + } catch (MsalInteropException e) { + LOG.warn("Exception thrown when trying to start MSALRuntime: {}", e.getErrorMessage()); + LOG.warn("MSALRuntime could not be started. MSAL Java will fall back to non-broker flows."); + + return false; + } + } +} diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AuthenticationErrorCode.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AuthenticationErrorCode.java index 78f5260c..1ea0232e 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AuthenticationErrorCode.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AuthenticationErrorCode.java @@ -115,9 +115,22 @@ public class AuthenticationErrorCode { * A JWT parsing failure, indicating the JWT provided to MSAL is of invalid format. */ public final static String INVALID_JWT = "invalid_jwt"; + /** * Indicates that a Broker implementation is missing from the device, such as when an app developer * does not include one of our broker packages as a dependency in their project, or otherwise cannot - * be accessed by MSAL Java*/ + * be accessed by MSAL Java + */ public final static String MISSING_BROKER = "missing_broker"; + + /** + * Indicates an error from the MSAL Java/MSALRuntime interop layer used by the Java Brokers package, + * and will generally just be forwarding an error message from the interop layer or MSALRuntime itself + */ + public final static String MSALRUNTIME_INTEROP_ERROR = "interop_package_error"; + + /** + * Indicates an error in the MSAL Java Brokers package + */ + public final static String MSALJAVA_BROKERS_ERROR = "brokers_package_error"; } diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/IBroker.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/IBroker.java index 919a8092..69906319 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/IBroker.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/IBroker.java @@ -3,58 +3,80 @@ package com.microsoft.aad.msal4j; -import java.util.Set; +import com.nimbusds.jwt.JWTParser; + +import java.net.URL; import java.util.concurrent.CompletableFuture; /** * Used to define the basic set of methods that all Brokers must implement * - * All methods are so they can be referenced by MSAL Java without an implementation, and by default simply throw an - * exception saying that a broker implementation is missing + * All methods are marked as default so they can be referenced by MSAL Java without an implementation, + * and most will simply throw an exception if not overridden by an IBroker implementation */ public interface IBroker { - /** - * checks if a IBroker implementation exists - */ - - default boolean isAvailable(){ - return false; - } /** * Acquire a token silently, i.e. without direct user interaction * * This may be accomplished by returning tokens from a token cache, using cached refresh tokens to get new tokens, * or via any authentication flow where a user is not prompted to enter credentials - * - * @param requestParameters MsalRequest object which contains everything needed for the broker implementation to make a request - * @return IBroker implementations will return an AuthenticationResult object */ - default IAuthenticationResult acquireToken(PublicClientApplication application, SilentParameters requestParameters) { + default CompletableFuture acquireToken(PublicClientApplication application, SilentParameters requestParameters) { throw new MsalClientException("Broker implementation missing", AuthenticationErrorCode.MISSING_BROKER); } /** * Acquire a token interactively, by prompting users to enter their credentials in some way - * - * @param requestParameters MsalRequest object which contains everything needed for the broker implementation to make a request - * @return IBroker implementations will return an AuthenticationResult object */ - default IAuthenticationResult acquireToken(PublicClientApplication application, InteractiveRequestParameters requestParameters) { + default CompletableFuture acquireToken(PublicClientApplication application, InteractiveRequestParameters parameters) { throw new MsalClientException("Broker implementation missing", AuthenticationErrorCode.MISSING_BROKER); } /** * Acquire a token silently, i.e. without direct user interaction, using username/password authentication - * - * @param requestParameters MsalRequest object which contains everything needed for the broker implementation to make a request - * @return IBroker implementations will return an AuthenticationResult object */ - default IAuthenticationResult acquireToken(PublicClientApplication application, UserNamePasswordParameters requestParameters) { + default CompletableFuture acquireToken(PublicClientApplication application, UserNamePasswordParameters parameters) { throw new MsalClientException("Broker implementation missing", AuthenticationErrorCode.MISSING_BROKER); } - default CompletableFuture removeAccount(IAccount account) { + default void removeAccount(PublicClientApplication application, IAccount account) throws MsalClientException { throw new MsalClientException("Broker implementation missing", AuthenticationErrorCode.MISSING_BROKER); } + + default boolean isBrokerAvailable() { + throw new MsalClientException("Broker implementation missing", AuthenticationErrorCode.MISSING_BROKER); + } + + /** + * MSAL Java's AuthenticationResult requires several package-private classes that a broker implementation can't access, + * so this helper method can be used to create AuthenticationResults from within the MSAL Java package + */ + default IAuthenticationResult parseBrokerAuthResult(String authority, String idToken, String accessToken, + String accountId, String clientInfo, + long accessTokenExpirationTime) { + + AuthenticationResult.AuthenticationResultBuilder builder = AuthenticationResult.builder(); + + try { + if (idToken != null) { + builder.idToken(idToken); + if (accountId!= null) { + String idTokenJson = + JWTParser.parse(idToken).getParsedParts()[1].decodeToString(); + //TODO: need to figure out if 'policy' field is relevant for brokers + builder.accountCacheEntity(AccountCacheEntity.create(clientInfo, + Authority.createAuthority(new URL(authority)), JsonHelper.convertJsonToObject(idTokenJson, + IdToken.class), null)); + } + } + if (accessToken != null) { + builder.accessToken(accessToken); + builder.expiresOn(accessTokenExpirationTime); + } + } catch (Exception e) { + throw new MsalClientException(String.format("Exception when converting broker result to MSAL Java AuthenticationResult: %s", e.getMessage()), AuthenticationErrorCode.MSALJAVA_BROKERS_ERROR); + } + return builder.build(); + } } \ No newline at end of file diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/InteractiveRequestParameters.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/InteractiveRequestParameters.java index acdb638a..a41d1832 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/InteractiveRequestParameters.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/InteractiveRequestParameters.java @@ -100,6 +100,18 @@ public class InteractiveRequestParameters implements IAcquireTokenParameters { */ private boolean instanceAware; + /** + * The parent window handle used to open UI elements with the correct parent + * + * + * For browser scenarios and Windows console applications, this value should not need to be set + * + * For Windows console applications, MSAL Java will attempt to discover the console's window handle if this parameter is not set + * + * For scenarios where MSAL Java is responsible for opening UI elements (such as when using MSALRuntime), this parameter is required and an exception will be thrown if not set + */ + private long windowHandle; + private static InteractiveRequestParametersBuilder builder() { return new InteractiveRequestParametersBuilder(); } diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/PublicClientApplication.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/PublicClientApplication.java index a7f18dda..11b19604 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/PublicClientApplication.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/PublicClientApplication.java @@ -8,6 +8,7 @@ import com.nimbusds.oauth2.sdk.id.ClientID; import org.slf4j.LoggerFactory; +import java.net.MalformedURLException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicReference; @@ -23,6 +24,8 @@ public class PublicClientApplication extends AbstractClientApplicationBase implements IPublicClientApplication { private final ClientAuthenticationPost clientAuthentication; + private IBroker broker; + private boolean brokerEnabled; @Override public CompletableFuture acquireToken(UserNamePasswordParameters parameters) { @@ -35,12 +38,20 @@ public CompletableFuture acquireToken(UserNamePasswordPar parameters, UserIdentifier.fromUpn(parameters.username())); - UserNamePasswordRequest userNamePasswordRequest = - new UserNamePasswordRequest(parameters, - this, - context); + CompletableFuture future; - return this.executeRequest(userNamePasswordRequest); + if (brokerEnabled) { + future = broker.acquireToken(this, parameters); + } else { + UserNamePasswordRequest userNamePasswordRequest = + new UserNamePasswordRequest(parameters, + this, + context); + + future = this.executeRequest(userNamePasswordRequest); + } + + return future; } @Override @@ -112,17 +123,49 @@ public CompletableFuture acquireToken(InteractiveRequestP this, context); - CompletableFuture future = executeRequest(interactiveRequest); + CompletableFuture future; + + if (brokerEnabled) { + future = broker.acquireToken(this, parameters); + } else { + future = executeRequest(interactiveRequest); + } + futureReference.set(future); + return future; } + @Override + public CompletableFuture acquireTokenSilently(SilentParameters parameters) throws MalformedURLException { + CompletableFuture future; + + if (brokerEnabled) { + future = broker.acquireToken(this, parameters); + } else { + future = super.acquireTokenSilently(parameters); + } + + return future; + } + + @Override + public CompletableFuture removeAccount(IAccount account) { + if (brokerEnabled) { + broker.removeAccount(this, account); + } + + return super.removeAccount(account); + } + private PublicClientApplication(Builder builder) { super(builder); validateNotBlank("clientId", clientId()); log = LoggerFactory.getLogger(PublicClientApplication.class); this.clientAuthentication = new ClientAuthenticationPost(ClientAuthenticationMethod.NONE, new ClientID(clientId())); + this.broker = builder.broker; + this.brokerEnabled = builder.brokerEnabled; } @Override @@ -146,6 +189,22 @@ private Builder(String clientId) { super(clientId); } + private IBroker broker = null; + private boolean brokerEnabled = false; + + /** + * Implementation of IBroker that will be used to retrieve tokens + *

+ * Setting this will cause MSAL Java to use the given broker implementation to retrieve tokens from a broker (such as WAM/MSALRuntime) in flows that support it + */ + public PublicClientApplication.Builder broker(IBroker val) { + this.broker = val; + + this.brokerEnabled = this.broker.isBrokerAvailable(); + + return self(); + } + @Override public PublicClientApplication build() {