Skip to content

Commit aacb439

Browse files
authored
Add IBroker implementation for MSALRuntime (#563)
* Add IBroker implementation for MSALRuntime * Remove dll used during testing * Integrate broker steps to relevant flows in PublicClientApplication * Add logic to cancel MsalRuntimeFutures * Expand javadocs and exception handling * Address code review comments * Simplify future chaining, address code review comments * Reorganize future chaining, fix testing issues * Adjust how broker availability is checked * Create automated test * Adjust startup logic * Correct version number for interop * Correct broker versioning * Move broker tests to MSAL Java package * Remove usage of msal4j-brokers from msal4j * Add missing SLFJ dependency * Use newest msal4j * Bump javamsalruntime version number
1 parent 0b81ab6 commit aacb439

File tree

7 files changed

+339
-65
lines changed

7 files changed

+339
-65
lines changed

msal4j-brokers/pom.xml

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<modelVersion>4.0.0</modelVersion>
66
<groupId>com.microsoft.azure</groupId>
77
<artifactId>msal4j-brokers</artifactId>
8-
<version>0.0.1</version>
8+
<version>1.0.0-beta</version>
99
<packaging>jar</packaging>
1010
<name>msal4j-brokers</name>
1111
<description>
@@ -26,18 +26,39 @@
2626
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
2727
</properties>
2828
<dependencies>
29-
<!-- https://mvnrepository.com/artifact/com.microsoft.azure/msal4j -->
3029
<dependency>
3130
<groupId>com.microsoft.azure</groupId>
3231
<artifactId>msal4j</artifactId>
33-
<version>1.13.2</version>
32+
<version>1.13.4</version>
33+
</dependency>
34+
<dependency>
35+
<groupId>com.microsoft.azure</groupId>
36+
<artifactId>javamsalruntime</artifactId>
37+
<version>0.13.4</version>
3438
</dependency>
3539
<dependency>
3640
<groupId>org.projectlombok</groupId>
3741
<artifactId>lombok</artifactId>
3842
<version>1.18.6</version>
3943
<scope>provided</scope>
4044
</dependency>
45+
<dependency>
46+
<groupId>org.testng</groupId>
47+
<artifactId>testng</artifactId>
48+
<version>7.1.0</version>
49+
<scope>test</scope>
50+
</dependency>
51+
<dependency>
52+
<groupId>org.slf4j</groupId>
53+
<artifactId>slf4j-api</artifactId>
54+
<version>1.7.36</version>
55+
</dependency>
56+
<dependency>
57+
<groupId>ch.qos.logback</groupId>
58+
<artifactId>logback-classic</artifactId>
59+
<version>1.2.3</version>
60+
<scope>test</scope>
61+
</dependency>
4162
</dependencies>
4263

4364
<!-- force https -->
@@ -60,7 +81,6 @@
6081
</pluginRepository>
6182
</pluginRepositories>
6283
<build>
63-
<sourceDirectory>${project.build.directory}/delombok</sourceDirectory>
6484
<plugins>
6585
<plugin>
6686
<groupId>org.projectlombok</groupId>

msal4j-brokers/src/main/java/com/microsoft/aad/msal4jbrokers/MSALRuntimeBroker.java

Lines changed: 0 additions & 31 deletions
This file was deleted.
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package com.microsoft.aad.msal4jbrokers;
5+
6+
import com.microsoft.aad.msal4j.IAuthenticationResult;
7+
import com.microsoft.aad.msal4j.IBroker;
8+
import com.microsoft.aad.msal4j.InteractiveRequestParameters;
9+
import com.microsoft.aad.msal4j.PublicClientApplication;
10+
import com.microsoft.aad.msal4j.SilentParameters;
11+
import com.microsoft.aad.msal4j.UserNamePasswordParameters;
12+
import com.microsoft.aad.msal4j.MsalClientException;
13+
import com.microsoft.aad.msal4j.AuthenticationErrorCode;
14+
import com.microsoft.aad.msal4j.IAccount;
15+
import com.microsoft.azure.javamsalruntime.Account;
16+
import com.microsoft.azure.javamsalruntime.AuthParameters;
17+
import com.microsoft.azure.javamsalruntime.AuthResult;
18+
import com.microsoft.azure.javamsalruntime.MsalInteropException;
19+
import com.microsoft.azure.javamsalruntime.MsalRuntimeInterop;
20+
import com.microsoft.azure.javamsalruntime.ReadAccountResult;
21+
import org.slf4j.Logger;
22+
import org.slf4j.LoggerFactory;
23+
24+
import java.util.concurrent.CompletableFuture;
25+
import java.util.concurrent.ExecutionException;
26+
27+
public class MsalRuntimeBroker implements IBroker {
28+
private static final Logger LOG = LoggerFactory.getLogger(MsalRuntimeBroker.class);
29+
30+
private static MsalRuntimeInterop interop;
31+
32+
static {
33+
try {
34+
//MsalRuntimeInterop performs various initialization steps in a similar static block,
35+
// so when an MsalRuntimeBroker is created this will cause the interop layer to initialize
36+
interop = new MsalRuntimeInterop();
37+
} catch (MsalInteropException e) {
38+
throw new MsalClientException(String.format("Could not initialize MSALRuntime: %s", e.getErrorMessage()), AuthenticationErrorCode.MSALRUNTIME_INTEROP_ERROR);
39+
}
40+
}
41+
42+
@Override
43+
public CompletableFuture<IAuthenticationResult> acquireToken(PublicClientApplication application, SilentParameters parameters) {
44+
Account accountResult = null;
45+
46+
//If request has an account ID, MSALRuntime likely has data cached for that account that we can retrieve
47+
if (parameters.account() != null) {
48+
try {
49+
accountResult = ((ReadAccountResult) interop.readAccountById(parameters.account().homeAccountId(), application.correlationId()).get()).getAccount();
50+
} catch (InterruptedException | ExecutionException ex) {
51+
throw new MsalClientException(String.format("MSALRuntime async operation interrupted when waiting for result: %s", ex.getMessage()), AuthenticationErrorCode.MSALRUNTIME_INTEROP_ERROR);
52+
}
53+
}
54+
55+
try {
56+
AuthParameters authParameters = new AuthParameters
57+
.AuthParametersBuilder(application.clientId(),
58+
application.authority(),
59+
String.join(" ", parameters.scopes()))
60+
.build();
61+
62+
if (accountResult == null) {
63+
return interop.signInSilently(authParameters, application.correlationId())
64+
.thenCompose(acctResult -> interop.acquireTokenSilently(authParameters, application.correlationId(), ((AuthResult) acctResult).getAccount()))
65+
.thenApply(authResult -> parseBrokerAuthResult(
66+
application.authority(),
67+
((AuthResult) authResult).getIdToken(),
68+
((AuthResult) authResult).getAccessToken(),
69+
((AuthResult) authResult).getAccount().getAccountId(),
70+
((AuthResult) authResult).getAccount().getClientInfo(),
71+
((AuthResult) authResult).getAccessTokenExpirationTime()));
72+
} else {
73+
return interop.acquireTokenSilently(authParameters, application.correlationId(), accountResult)
74+
.thenApply(authResult -> parseBrokerAuthResult(application.authority(),
75+
((AuthResult) authResult).getIdToken(),
76+
((AuthResult) authResult).getAccessToken(),
77+
((AuthResult) authResult).getAccount().getAccountId(),
78+
((AuthResult) authResult).getAccount().getClientInfo(),
79+
((AuthResult) authResult).getAccessTokenExpirationTime())
80+
81+
);
82+
}
83+
} catch (MsalInteropException interopException) {
84+
throw new MsalClientException(interopException.getErrorMessage(), AuthenticationErrorCode.MSALRUNTIME_INTEROP_ERROR);
85+
}
86+
}
87+
88+
@Override
89+
public CompletableFuture<IAuthenticationResult> acquireToken(PublicClientApplication application, InteractiveRequestParameters parameters) {
90+
try {
91+
AuthParameters authParameters = new AuthParameters
92+
.AuthParametersBuilder(application.clientId(),
93+
application.authority(),
94+
String.join(" ", parameters.scopes()))
95+
.build();
96+
97+
return interop.signInInteractively(parameters.windowHandle(), authParameters, application.correlationId(), parameters.loginHint())
98+
.thenCompose(acctResult -> interop.acquireTokenInteractively(parameters.windowHandle(), authParameters, application.correlationId(), ((AuthResult) acctResult).getAccount()))
99+
.thenApply(authResult -> parseBrokerAuthResult(
100+
application.authority(),
101+
((AuthResult) authResult).getIdToken(),
102+
((AuthResult) authResult).getAccessToken(),
103+
((AuthResult) authResult).getAccount().getAccountId(),
104+
((AuthResult) authResult).getAccount().getClientInfo(),
105+
((AuthResult) authResult).getAccessTokenExpirationTime())
106+
);
107+
} catch (MsalInteropException interopException) {
108+
throw new MsalClientException(interopException.getErrorMessage(), AuthenticationErrorCode.MSALRUNTIME_INTEROP_ERROR);
109+
}
110+
}
111+
112+
/**
113+
* @deprecated
114+
*/
115+
@Deprecated
116+
@Override
117+
public CompletableFuture<IAuthenticationResult> acquireToken(PublicClientApplication application, UserNamePasswordParameters parameters) {
118+
try {
119+
AuthParameters authParameters =
120+
new AuthParameters
121+
.AuthParametersBuilder(application.clientId(),
122+
application.authority(),
123+
String.join(" ", parameters.scopes()))
124+
.build();
125+
126+
authParameters.setUsernamePassword(parameters.username(), new String(parameters.password()));
127+
128+
return interop.signInSilently(authParameters, application.correlationId())
129+
.thenCompose(acctResult -> interop.acquireTokenSilently(authParameters, application.correlationId(), ((AuthResult) acctResult).getAccount()))
130+
.thenApply(authResult -> parseBrokerAuthResult(
131+
application.authority(),
132+
((AuthResult) authResult).getIdToken(),
133+
((AuthResult) authResult).getAccessToken(),
134+
((AuthResult) authResult).getAccount().getAccountId(),
135+
((AuthResult) authResult).getAccount().getClientInfo(),
136+
((AuthResult) authResult).getAccessTokenExpirationTime()));
137+
} catch (MsalInteropException interopException) {
138+
throw new MsalClientException(interopException.getErrorMessage(), AuthenticationErrorCode.MSALRUNTIME_INTEROP_ERROR);
139+
}
140+
}
141+
142+
@Override
143+
public void removeAccount(PublicClientApplication application, IAccount msalJavaAccount) {
144+
try {
145+
Account msalRuntimeAccount = ((ReadAccountResult) interop.readAccountById(msalJavaAccount.homeAccountId(), application.correlationId()).get()).getAccount();
146+
147+
if (msalRuntimeAccount != null) {
148+
interop.signOutSilently(application.clientId(), application.correlationId(), msalRuntimeAccount);
149+
}
150+
} catch (MsalInteropException interopException) {
151+
throw new MsalClientException(interopException.getErrorMessage(), AuthenticationErrorCode.MSALRUNTIME_INTEROP_ERROR);
152+
} catch (InterruptedException | ExecutionException ex) {
153+
throw new MsalClientException(String.format("MSALRuntime async operation interrupted when waiting for result: %s", ex.getMessage()), AuthenticationErrorCode.MSALRUNTIME_INTEROP_ERROR);
154+
}
155+
}
156+
157+
/**
158+
* Calls MSALRuntime's startup API. If MSALRuntime started successfully, we can assume that the broker is available for use.
159+
*
160+
* 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.
161+
*
162+
* @return boolean representing whether or not MSALRuntime started successfully
163+
*/
164+
@Override
165+
public boolean isBrokerAvailable() {
166+
try {
167+
interop.startupMsalRuntime();
168+
169+
LOG.info("MSALRuntime started successfully. MSAL Java will use MSALRuntime in all supported broker flows.");
170+
171+
return true;
172+
} catch (MsalInteropException e) {
173+
LOG.warn("Exception thrown when trying to start MSALRuntime: {}", e.getErrorMessage());
174+
LOG.warn("MSALRuntime could not be started. MSAL Java will fall back to non-broker flows.");
175+
176+
return false;
177+
}
178+
}
179+
}

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,9 +115,22 @@ public class AuthenticationErrorCode {
115115
* A JWT parsing failure, indicating the JWT provided to MSAL is of invalid format.
116116
*/
117117
public final static String INVALID_JWT = "invalid_jwt";
118+
118119
/**
119120
* Indicates that a Broker implementation is missing from the device, such as when an app developer
120121
* does not include one of our broker packages as a dependency in their project, or otherwise cannot
121-
* be accessed by MSAL Java*/
122+
* be accessed by MSAL Java
123+
*/
122124
public final static String MISSING_BROKER = "missing_broker";
125+
126+
/**
127+
* Indicates an error from the MSAL Java/MSALRuntime interop layer used by the Java Brokers package,
128+
* and will generally just be forwarding an error message from the interop layer or MSALRuntime itself
129+
*/
130+
public final static String MSALRUNTIME_INTEROP_ERROR = "interop_package_error";
131+
132+
/**
133+
* Indicates an error in the MSAL Java Brokers package
134+
*/
135+
public final static String MSALJAVA_BROKERS_ERROR = "brokers_package_error";
123136
}

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

Lines changed: 45 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,58 +3,80 @@
33

44
package com.microsoft.aad.msal4j;
55

6-
import java.util.Set;
6+
import com.nimbusds.jwt.JWTParser;
7+
8+
import java.net.URL;
79
import java.util.concurrent.CompletableFuture;
810

911
/**
1012
* Used to define the basic set of methods that all Brokers must implement
1113
*
12-
* All methods are so they can be referenced by MSAL Java without an implementation, and by default simply throw an
13-
* exception saying that a broker implementation is missing
14+
* All methods are marked as default so they can be referenced by MSAL Java without an implementation,
15+
* and most will simply throw an exception if not overridden by an IBroker implementation
1416
*/
1517
public interface IBroker {
1618

17-
/**
18-
* checks if a IBroker implementation exists
19-
*/
20-
21-
default boolean isAvailable(){
22-
return false;
23-
}
2419
/**
2520
* Acquire a token silently, i.e. without direct user interaction
2621
*
2722
* This may be accomplished by returning tokens from a token cache, using cached refresh tokens to get new tokens,
2823
* or via any authentication flow where a user is not prompted to enter credentials
29-
*
30-
* @param requestParameters MsalRequest object which contains everything needed for the broker implementation to make a request
31-
* @return IBroker implementations will return an AuthenticationResult object
3224
*/
33-
default IAuthenticationResult acquireToken(PublicClientApplication application, SilentParameters requestParameters) {
25+
default CompletableFuture<IAuthenticationResult> acquireToken(PublicClientApplication application, SilentParameters requestParameters) {
3426
throw new MsalClientException("Broker implementation missing", AuthenticationErrorCode.MISSING_BROKER);
3527
}
3628

3729
/**
3830
* Acquire a token interactively, by prompting users to enter their credentials in some way
39-
*
40-
* @param requestParameters MsalRequest object which contains everything needed for the broker implementation to make a request
41-
* @return IBroker implementations will return an AuthenticationResult object
4231
*/
43-
default IAuthenticationResult acquireToken(PublicClientApplication application, InteractiveRequestParameters requestParameters) {
32+
default CompletableFuture<IAuthenticationResult> acquireToken(PublicClientApplication application, InteractiveRequestParameters parameters) {
4433
throw new MsalClientException("Broker implementation missing", AuthenticationErrorCode.MISSING_BROKER);
4534
}
4635

4736
/**
4837
* Acquire a token silently, i.e. without direct user interaction, using username/password authentication
49-
*
50-
* @param requestParameters MsalRequest object which contains everything needed for the broker implementation to make a request
51-
* @return IBroker implementations will return an AuthenticationResult object
5238
*/
53-
default IAuthenticationResult acquireToken(PublicClientApplication application, UserNamePasswordParameters requestParameters) {
39+
default CompletableFuture<IAuthenticationResult> acquireToken(PublicClientApplication application, UserNamePasswordParameters parameters) {
5440
throw new MsalClientException("Broker implementation missing", AuthenticationErrorCode.MISSING_BROKER);
5541
}
5642

57-
default CompletableFuture removeAccount(IAccount account) {
43+
default void removeAccount(PublicClientApplication application, IAccount account) throws MsalClientException {
5844
throw new MsalClientException("Broker implementation missing", AuthenticationErrorCode.MISSING_BROKER);
5945
}
46+
47+
default boolean isBrokerAvailable() {
48+
throw new MsalClientException("Broker implementation missing", AuthenticationErrorCode.MISSING_BROKER);
49+
}
50+
51+
/**
52+
* MSAL Java's AuthenticationResult requires several package-private classes that a broker implementation can't access,
53+
* so this helper method can be used to create AuthenticationResults from within the MSAL Java package
54+
*/
55+
default IAuthenticationResult parseBrokerAuthResult(String authority, String idToken, String accessToken,
56+
String accountId, String clientInfo,
57+
long accessTokenExpirationTime) {
58+
59+
AuthenticationResult.AuthenticationResultBuilder builder = AuthenticationResult.builder();
60+
61+
try {
62+
if (idToken != null) {
63+
builder.idToken(idToken);
64+
if (accountId!= null) {
65+
String idTokenJson =
66+
JWTParser.parse(idToken).getParsedParts()[1].decodeToString();
67+
//TODO: need to figure out if 'policy' field is relevant for brokers
68+
builder.accountCacheEntity(AccountCacheEntity.create(clientInfo,
69+
Authority.createAuthority(new URL(authority)), JsonHelper.convertJsonToObject(idTokenJson,
70+
IdToken.class), null));
71+
}
72+
}
73+
if (accessToken != null) {
74+
builder.accessToken(accessToken);
75+
builder.expiresOn(accessTokenExpirationTime);
76+
}
77+
} catch (Exception e) {
78+
throw new MsalClientException(String.format("Exception when converting broker result to MSAL Java AuthenticationResult: %s", e.getMessage()), AuthenticationErrorCode.MSALJAVA_BROKERS_ERROR);
79+
}
80+
return builder.build();
81+
}
6082
}

0 commit comments

Comments
 (0)