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 22e8299f..0d3f231a 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
@@ -130,4 +130,15 @@ public class AuthenticationErrorCode {
* slow response, and this may be resolvable by increasing timeouts. For more details, see https://aka.ms/msal4j-http-client
*/
public final static String HTTP_TIMEOUT = "http_timeout";
+
+ /**
+ * 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 related to 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/AuthenticationResult.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AuthenticationResult.java
index dcef23f5..1338467b 100644
--- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AuthenticationResult.java
+++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AuthenticationResult.java
@@ -90,4 +90,7 @@ private ITenantProfile getTenantProfile() {
private final Date expiresOnDate = new Date(expiresOn * 1000);
private final String scopes;
+
+ @Getter(value = AccessLevel.PACKAGE)
+ private final Boolean isPopAuthorization;
}
diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/HttpMethod.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/HttpMethod.java
index 64af4605..9497ec3c 100644
--- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/HttpMethod.java
+++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/HttpMethod.java
@@ -4,17 +4,53 @@
package com.microsoft.aad.msal4j;
/**
- * Http request method.
+ * An enumerator representing common HTTP request methods.
*/
public enum HttpMethod {
+ /**
+ * The HTTP CONNECT method.
+ */
+ CONNECT("CONNECT"),
+
+ /**
+ * The HTTP DELETE method.
+ */
+ DELETE("DELETE"),
+
/**
* The HTTP GET method.
*/
- GET,
+ GET("GET"),
+
+ /**
+ * The HTTP HEAD method.
+ */
+ HEAD("HEAD"),
+
+ /**
+ * The HTTP OPTIONS method.
+ */
+ OPTIONS("OPTIONS"),
/**
* The HTTP POST method.
*/
- POST
+ POST("POST"),
+
+ /**
+ * The HTTP PUT method.
+ */
+ PUT("PUT"),
+
+ /**
+ * The HTTP TRACE method.
+ */
+ TRACE("TRACE");
+
+ public final String methodName;
+
+ HttpMethod(String methodName) {
+ this.methodName = methodName;
+ }
}
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..ab3f0ce0 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,87 @@
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);
+ }
+
+ /**
+ * Returns whether a broker is available and ready to use on this machine, allowing the use of the methods
+ * in this interface and other broker-only features in MSAL Java
+ */
+ 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,
+ boolean isPopAuthorization) {
+
+ AuthenticationResult.AuthenticationResultBuilder builder = AuthenticationResult.builder();
+
+ try {
+ if (idToken != null) {
+ builder.idToken(idToken);
+ if (accountId != null) {
+ String idTokenJson =
+ JWTParser.parse(idToken).getParsedParts()[1].decodeToString();
+ 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);
+ }
+
+ builder.isPopAuthorization(isPopAuthorization);
+
+ } 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 33e89eab..567cb280 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
@@ -105,6 +105,20 @@ 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 PopParameters proofOfPossession;
+
private static InteractiveRequestParametersBuilder builder() {
return new InteractiveRequestParametersBuilder();
}
@@ -116,4 +130,23 @@ public static InteractiveRequestParametersBuilder builder(URI redirectUri) {
return builder()
.redirectUri(redirectUri);
}
+
+ //This Builder class is used to override Lombok's default setter behavior for any fields defined in it
+ public static class InteractiveRequestParametersBuilder {
+
+ /**
+ * Sets the PopParameters for this request, allowing the request to retrieve proof-of-possession tokens rather than bearer tokens
+ *
+ * For more information, see {@link PopParameters} and https://aka.ms/msal4j-pop
+ *
+ * @param httpMethod a valid HTTP method, such as "GET" or "POST"
+ * @param uri the URI on the downstream protected API which the application is trying to access, e.g. https://graph.microsoft.com/beta/me/profile
+ * @param nonce a string obtained by calling the resource (e.g. Microsoft Graph) un-authenticated and parsing the WWW-Authenticate header associated with pop authentication scheme and extracting the nonce parameter, or, on subsequent calls, by parsing the Autheticate-Info header and extracting the nextnonce parameter.
+ */
+ public InteractiveRequestParametersBuilder proofOfPossession(HttpMethod httpMethod, URI uri, String nonce) {
+ this.proofOfPossession = new PopParameters(httpMethod, uri, nonce);
+
+ return this;
+ }
+ }
}
diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/PopParameters.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/PopParameters.java
new file mode 100644
index 00000000..d72ab5a5
--- /dev/null
+++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/PopParameters.java
@@ -0,0 +1,44 @@
+package com.microsoft.aad.msal4j;
+
+import java.net.URI;
+
+/**
+ * Contains parameters used to request a Proof of Possession (PoP) token in supported flows
+ */
+public class PopParameters {
+
+ HttpMethod httpMethod;
+ URI uri;
+ String nonce;
+
+ public HttpMethod getHttpMethod() {
+ return httpMethod;
+ }
+
+ public URI getUri() {
+ return uri;
+ }
+
+ public String getNonce() {
+ return nonce;
+ }
+
+ PopParameters(HttpMethod httpMethod, URI uri, String nonce) {
+ validatePopAuthScheme(httpMethod, uri);
+
+ this.httpMethod = httpMethod;
+ this.uri = uri;
+ this.nonce = nonce;
+ }
+
+ /**
+ * Performs any minimum validation to confirm this auth scheme could be valid for a POP request
+ */
+ void validatePopAuthScheme(HttpMethod httpMethod, URI uri) {
+ //At a minimum HTTP method and host must be non-null
+ if (httpMethod == null || uri == null || uri.getHost() == null) {
+ throw new MsalClientException(
+ "HTTP method and URI host must be non-null", AuthenticationErrorCode.MSALJAVA_BROKERS_ERROR);
+ }
+ }
+}
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 80fa1c31..e03b9c17 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;
+
+ if (validateBrokerUsage(parameters)) {
+ future = broker.acquireToken(this, parameters);
+ } else {
+ UserNamePasswordRequest userNamePasswordRequest =
+ new UserNamePasswordRequest(parameters,
+ this,
+ context);
- return this.executeRequest(userNamePasswordRequest);
+ future = this.executeRequest(userNamePasswordRequest);
+ }
+
+ return future;
}
@Override
@@ -111,17 +122,49 @@ public CompletableFuture acquireToken(InteractiveRequestP
this,
context);
- CompletableFuture future = executeRequest(interactiveRequest);
+ CompletableFuture future;
+
+ if (validateBrokerUsage(parameters)) {
+ 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 (validateBrokerUsage(parameters)) {
+ 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
@@ -145,6 +188,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() {
@@ -156,4 +215,61 @@ protected Builder self() {
return this;
}
}
+
+ /**
+ * Used to determine whether to call into an IBroker instance instead of standard MSAL Java's normal interactive flow,
+ * and may throw exceptions or log messages if broker-only parameters are used when a broker is not enabled/available
+ */
+ private boolean validateBrokerUsage(InteractiveRequestParameters parameters) {
+
+ //Check if broker-only parameters are being used when a broker is not enabled. If they are, either throw an
+ // exception saying a broker is required, or provide a clear log message saying the parameter will be ignored
+ if (!brokerEnabled) {
+ if (parameters.proofOfPossession() != null) {
+ throw new MsalClientException(
+ "InteractiveRequestParameters.proofOfPossession should not be used when broker is not available, see https://aka.ms/msal4j-pop for more information",
+ AuthenticationErrorCode.MSALJAVA_BROKERS_ERROR );
+ }
+ }
+
+ return brokerEnabled;
+ }
+
+ /**
+ * Used to determine whether to call into an IBroker instance instead of standard MSAL Java's normal username/password flow,
+ * and may throw exceptions or log messages if broker-only parameters are used when a broker is not enabled/available
+ */
+ private boolean validateBrokerUsage(UserNamePasswordParameters parameters) {
+
+ //Check if broker-only parameters are being used when a broker is not enabled. If they are, either throw an
+ // exception saying a broker is required, or provide a clear log message saying the parameter will be ignored
+ if (!brokerEnabled) {
+ if (parameters.proofOfPossession() != null) {
+ throw new MsalClientException(
+ "UserNamePasswordParameters.proofOfPossession should not be used when broker is not available, see https://aka.ms/msal4j-pop for more information",
+ AuthenticationErrorCode.MSALJAVA_BROKERS_ERROR );
+ }
+ }
+
+ return brokerEnabled;
+ }
+
+ /**
+ * Used to determine whether to call into an IBroker instance instead of standard MSAL Java's normal silent flow,
+ * and may throw exceptions or log messages if broker-only parameters are used when a broker is not enabled/available
+ */
+ private boolean validateBrokerUsage(SilentParameters parameters) {
+
+ //Check if broker-only parameters are being used when a broker is not enabled. If they are, either throw an
+ // exception saying a broker is required, or provide a clear log message saying the parameter will be ignored
+ if (!brokerEnabled) {
+ if (parameters.proofOfPossession() != null) {
+ throw new MsalClientException(
+ "UserNamePasswordParameters.proofOfPossession should not be used when broker is not available, see https://aka.ms/msal4j-pop for more information",
+ AuthenticationErrorCode.MSALJAVA_BROKERS_ERROR );
+ }
+ }
+
+ return brokerEnabled;
+ }
}
\ No newline at end of file
diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/SilentParameters.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/SilentParameters.java
index 429c5dbb..c4c4e9fa 100644
--- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/SilentParameters.java
+++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/SilentParameters.java
@@ -6,6 +6,7 @@
import lombok.*;
import lombok.experimental.Accessors;
+import java.net.URI;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
@@ -64,6 +65,8 @@ public class SilentParameters implements IAcquireTokenParameters {
*/
private String tenant;
+ private PopParameters proofOfPossession;
+
private static SilentParametersBuilder builder() {
return new SilentParametersBuilder();
@@ -114,4 +117,23 @@ private static Set removeEmptyScope(Set scopes){
}
return updatedScopes;
}
+
+ //This Builder class is used to override Lombok's default setter behavior for any fields defined in it
+ public static class SilentParametersBuilder {
+
+ /**
+ * Sets the PopParameters for this request, allowing the request to retrieve proof-of-possession tokens rather than bearer tokens
+ *
+ * For more information, see {@link PopParameters} and https://aka.ms/msal4j-pop
+ *
+ * @param httpMethod a valid HTTP method, such as "GET" or "POST"
+ * @param uri URI to associate with the token
+ * @param nonce optional nonce value for the token, can be empty or null
+ */
+ public SilentParametersBuilder proofOfPossession(HttpMethod httpMethod, URI uri, String nonce) {
+ this.proofOfPossession = new PopParameters(httpMethod, uri, nonce);
+
+ return this;
+ }
+ }
}
diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/UserNamePasswordParameters.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/UserNamePasswordParameters.java
index cc4dab0c..f9df4454 100644
--- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/UserNamePasswordParameters.java
+++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/UserNamePasswordParameters.java
@@ -6,6 +6,7 @@
import lombok.*;
import lombok.experimental.Accessors;
+import java.net.URI;
import java.util.Map;
import java.util.Set;
@@ -63,6 +64,8 @@ public class UserNamePasswordParameters implements IAcquireTokenParameters {
*/
private String tenant;
+ private PopParameters proofOfPossession;
+
public char[] password() {
return password.clone();
}
@@ -98,5 +101,20 @@ public UserNamePasswordParametersBuilder password(char[] password) {
this.password = password.clone();
return this;
}
+
+ /**
+ * Sets the PopParameters for this request, allowing the request to retrieve proof-of-possession tokens rather than bearer tokens
+ *
+ * For more information, see {@link PopParameters} and https://aka.ms/msal4j-pop
+ *
+ * @param httpMethod a valid HTTP method, such as "GET" or "POST"
+ * @param uri URI to associate with the token
+ * @param nonce optional nonce value for the token, can be empty or null
+ */
+ public UserNamePasswordParametersBuilder proofOfPossession(HttpMethod httpMethod, URI uri, String nonce) {
+ this.proofOfPossession = new PopParameters(httpMethod, uri, nonce);
+
+ return this;
+ }
}
}