From eb748c0dcb20979a61293027908fe754f754bd3b Mon Sep 17 00:00:00 2001 From: sgonzalezMSFT Date: Tue, 23 Jul 2019 09:22:02 -0700 Subject: [PATCH 01/24] setting skeleton --- pom.xml | 2 +- .../AuthorizationCodeIT.java | 6 +- .../DeviceCodeIT.java | 2 +- ...AcquireTokenByInteractiveFlowSupplier.java | 122 ++++++++++++++++++ .../aad/msal4j/AuthenticationErrorCode.java | 12 ++ .../aad/msal4j/IPublicClientApplication.java | 3 + .../aad/msal4j/InteractiveRequest.java | 89 +++++++++++++ .../msal4j/InteractiveRequestParameters.java | 37 ++++++ .../aad/msal4j/OpenBrowserAction.java | 12 ++ .../aad/msal4j/PublicClientApplication.java | 16 +++ .../aad/msal4j/SystemBrowserOptions.java | 23 ++++ .../microsoft/aad/msal4j}/TcpListener.java | 17 ++- 12 files changed, 330 insertions(+), 11 deletions(-) create mode 100644 src/main/java/com/microsoft/aad/msal4j/AcquireTokenByInteractiveFlowSupplier.java create mode 100644 src/main/java/com/microsoft/aad/msal4j/InteractiveRequest.java create mode 100644 src/main/java/com/microsoft/aad/msal4j/InteractiveRequestParameters.java create mode 100644 src/main/java/com/microsoft/aad/msal4j/OpenBrowserAction.java create mode 100644 src/main/java/com/microsoft/aad/msal4j/SystemBrowserOptions.java rename src/{integrationtest/java/infrastructure => main/java/com/microsoft/aad/msal4j}/TcpListener.java (84%) diff --git a/pom.xml b/pom.xml index 665a29f9..1fb6be59 100644 --- a/pom.xml +++ b/pom.xml @@ -98,7 +98,7 @@ org.apache.httpcomponents httpclient - 4.5 + 4.5.9 com.microsoft.azure diff --git a/src/integrationtest/java/com.microsoft.aad.msal4j/AuthorizationCodeIT.java b/src/integrationtest/java/com.microsoft.aad.msal4j/AuthorizationCodeIT.java index deb80d9b..d696ad34 100644 --- a/src/integrationtest/java/com.microsoft.aad.msal4j/AuthorizationCodeIT.java +++ b/src/integrationtest/java/com.microsoft.aad.msal4j/AuthorizationCodeIT.java @@ -4,7 +4,6 @@ package com.microsoft.aad.msal4j; import infrastructure.SeleniumExtensions; -import infrastructure.TcpListener; import labapi.B2CIdentityProvider; import labapi.FederationProvider; import labapi.LabResponse; @@ -320,7 +319,10 @@ private void runSeleniumAutomatedLogin(LabResponse labUserData, AuthorityType au private void startTcpListener(BlockingQueue tcpStartUpNotifierQueue){ AuthorizationCodeQueue = new LinkedBlockingQueue<>(); tcpListener = new TcpListener(AuthorizationCodeQueue, tcpStartUpNotifierQueue); - tcpListener.startServer(); + + // these are the ports that are registered for B2C tests + int[] ports = { 3843,4584, 4843, 60000 }; + tcpListener.startServer(ports); } private String getResponseFromTcpListener(){ diff --git a/src/integrationtest/java/com.microsoft.aad.msal4j/DeviceCodeIT.java b/src/integrationtest/java/com.microsoft.aad.msal4j/DeviceCodeIT.java index fda7dc4d..04cc23b2 100644 --- a/src/integrationtest/java/com.microsoft.aad.msal4j/DeviceCodeIT.java +++ b/src/integrationtest/java/com.microsoft.aad.msal4j/DeviceCodeIT.java @@ -45,7 +45,7 @@ public void DeviceCodeFlowTest() throws Exception { PublicClientApplication pca = new PublicClientApplication.Builder( labResponse.getAppId()). authority(TestConstants.ORGANIZATIONS_AUTHORITY). - build(); + build(); Consumer deviceCodeConsumer = (DeviceCode deviceCode) -> { runAutomatedDeviceCodeFlow(deviceCode, labResponse.getUser()); diff --git a/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByInteractiveFlowSupplier.java b/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByInteractiveFlowSupplier.java new file mode 100644 index 00000000..104cd0f0 --- /dev/null +++ b/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByInteractiveFlowSupplier.java @@ -0,0 +1,122 @@ +package com.microsoft.aad.msal4j; + +import java.awt.*; +import java.net.URI; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class AcquireTokenByInteractiveFlowSupplier extends AuthenticationResultSupplier { + + private BlockingQueue AuthorizationCodeQueue; + private TcpListener tcpListener; + + private PublicClientApplication clientApplication; + private InteractiveRequest interactiveRequest; + + AcquireTokenByInteractiveFlowSupplier(PublicClientApplication clientApplication, + InteractiveRequest request){ + super(clientApplication, request); + this.clientApplication = clientApplication; + this.interactiveRequest = request; + } + + // This where the high level logic for the flow will go + AuthenticationResult execute() { + String authorizationCode = getAuthorizationCode(); + acquireTokenWithAuthorizationCode(authorizationCode); + + } + + private String getAuthorizationCode(){ + + BlockingQueue tcpStartUpNotificationQueue = new LinkedBlockingQueue<>(); + startTcpListener(tcpStartUpNotificationQueue); + + String authServerResponse; + try{ + + Boolean tcpListenerStarted = tcpStartUpNotificationQueue.poll( + 30, + TimeUnit.SECONDS); + if (tcpListenerStarted == null || !tcpListenerStarted){ + throw new RuntimeException("Could not start TCP listener"); + } + + if(interactiveRequest.interactiveRequestParameters.systemBrowserOptions().openBrowserAction() != null){ + interactiveRequest.interactiveRequestParameters.systemBrowserOptions().openBrowserAction().openBrowser( + interactiveRequest.authorizationURI); + } else { + openDefaultSystemBrowser(interactiveRequest.authorizationURI); + } + + authServerResponse = getResponseFromTcpListener(); + } catch(Exception e){ + throw new MsalClientException("Error", AuthenticationErrorCode.AUTHORIZATION_CODE_BLANK); + } + + return parseServerResponse(authServerResponse, clientApplication.authenticationAuthority.authorityType); + } + + private AuthenticationResult acquireTokenWithAuthorizationCode(String authorizationCode){ + + } + + private void startTcpListener(BlockingQueue tcpStartUpNotifierQueue){ + AuthorizationCodeQueue = new LinkedBlockingQueue<>(); + + tcpListener = new TcpListener( + AuthorizationCodeQueue, + tcpStartUpNotifierQueue); + + // if port is unspecified, set to 0, which will cause socket to find a free port + int port = interactiveRequest.interactiveRequestParameters.redirectUri().getPort() == -1 ? + 0 : + interactiveRequest.interactiveRequestParameters.redirectUri().getPort(); + tcpListener.startServer(new int[] {port}); + } + + private void openDefaultSystemBrowser(URI uri){ + + if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) { + Desktop.getDesktop().browse(uri); + } + } + + private String getResponseFromTcpListener(){ + String response; + try { + response = AuthorizationCodeQueue.poll(300, TimeUnit.SECONDS); + if (StringHelper.isBlank(response)){ + throw new MsalClientException("No Authorization code was returned from the server", + AuthenticationErrorCode.AUTHORIZATION_CODE_BLANK); + } + } catch(Exception e){ + throw new MsalClientException(e); + } + return response; + } + + //TODO: Bring up difference in between auth code response and ask B2C team if it's possible to standardize + private String parseServerResponse(String serverResponse, AuthorityType authorityType){ + // Response will be a GET request with query parameter ?code=authCode + String regexp; + if(authorityType == AuthorityType.B2C){ + regexp = "(?<=code=)(?:(?! HTTP).)*"; + } else { + regexp = "(?<=code=)(?:(?!&).)*"; + } + + Pattern pattern = Pattern.compile(regexp); + Matcher matcher = pattern.matcher(serverResponse); + + if(!matcher.find()){ + throw new MsalClientException("No authorization code in server response", + AuthenticationErrorCode.AUTHORIZATION_CODE_BLANK); + } + return matcher.group(0); + } + +} \ No newline at end of file diff --git a/src/main/java/com/microsoft/aad/msal4j/AuthenticationErrorCode.java b/src/main/java/com/microsoft/aad/msal4j/AuthenticationErrorCode.java index 1df4944c..225a87ba 100644 --- a/src/main/java/com/microsoft/aad/msal4j/AuthenticationErrorCode.java +++ b/src/main/java/com/microsoft/aad/msal4j/AuthenticationErrorCode.java @@ -45,5 +45,17 @@ public class AuthenticationErrorCode { * Unknown error occurred */ public final static String UNKNOWN = "unknown"; + + /** + * The curretn redirect URL is not a loopback URL. To use OS browser, a loopback URL must be + * configured both during app registration as well as when initializing the InteractiveRequestParameters + * object + */ + public final static String LOOPBACK_REDIRECT_URI = "loopback_redirect_uri"; + + + public final static String PORT_BLOCKED = "port_blocked"; + + public final static String AUTHORIZATION_CODE_BLANK = "authorization_code_blank"; } diff --git a/src/main/java/com/microsoft/aad/msal4j/IPublicClientApplication.java b/src/main/java/com/microsoft/aad/msal4j/IPublicClientApplication.java index 88bed012..28410f08 100644 --- a/src/main/java/com/microsoft/aad/msal4j/IPublicClientApplication.java +++ b/src/main/java/com/microsoft/aad/msal4j/IPublicClientApplication.java @@ -44,4 +44,7 @@ public interface IPublicClientApplication extends IClientApplicationBase { * SHOULD wait between polling requests to the token endpoint */ CompletableFuture acquireToken(DeviceCodeFlowParameters parameters); + + //TODO fill in JavaDoc + CompletableFuture acquireToken(InteractiveRequestParameters parameters); } diff --git a/src/main/java/com/microsoft/aad/msal4j/InteractiveRequest.java b/src/main/java/com/microsoft/aad/msal4j/InteractiveRequest.java new file mode 100644 index 00000000..3d855898 --- /dev/null +++ b/src/main/java/com/microsoft/aad/msal4j/InteractiveRequest.java @@ -0,0 +1,89 @@ +package com.microsoft.aad.msal4j; + +import com.nimbusds.oauth2.sdk.util.URLUtils; + +import java.net.InetAddress; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.UnknownHostException; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +class InteractiveRequest extends MsalRequest{ + + URI authorizationURI; + InteractiveRequestParameters interactiveRequestParameters; + private PublicClientApplication publicClientApplication; + + InteractiveRequest(InteractiveRequestParameters parameters, + PublicClientApplication publicClientApplication, + RequestContext requestContext){ + + super(publicClientApplication, null, requestContext); + + validateRedirectURI(parameters.redirectUri()); + this.authorizationURI = createAuthorizationURI(); + this.interactiveRequestParameters = parameters; + this.publicClientApplication = publicClientApplication; + } + + private void validateRedirectURI(URI redirectURI) { + try { + if (!InetAddress.getByName(redirectURI.getHost()).isLoopbackAddress()) { + throw new MsalClientException(String.format( + "Only loopback redirect uri is supported, but %s was found " + + "Configure http://localhost or http://localhost:port both during app registration" + + "and when you create the create the InteractiveRequestParameters object", redirectURI.getHost()), + AuthenticationErrorCode.LOOPBACK_REDIRECT_URI); + } + + if (redirectURI.getScheme() != null) { + throw new MsalClientException(String.format( + "Only http uri scheme is supported but %s was found. Configure http://localhost" + + "or http://localhost:port both during app registration and when you create" + + " the create the InteractiveRequestParameters object", redirectURI.toString()), + AuthenticationErrorCode.LOOPBACK_REDIRECT_URI); + } + } catch (UnknownHostException exception){ + throw new MsalClientException(exception); + } + } + + private URI createAuthorizationURI() { + + URI uri; + try{ + Map> authorizationRequestParameters = createAuthorizationRequestParameters(); + String authorizationCodeEndpoint = publicClientApplication.authority() + "oauth2/v2.0/authorize"; + String uriString = authorizationCodeEndpoint + "?" + URLUtils.serializeParameters(authorizationRequestParameters); + + uri = new URI(uriString); + } catch (URISyntaxException exception) { + throw new MsalClientException(exception); + } + + return uri; + } + + private Map> createAuthorizationRequestParameters(){ + + Map> requestParameters = new HashMap<>(); + + String scopesParam = AbstractMsalAuthorizationGrant.COMMON_SCOPES_PARAM + + AbstractMsalAuthorizationGrant.SCOPES_DELIMITER + interactiveRequestParameters.scopes(); + + requestParameters.put("scope", Collections.singletonList(scopesParam)); + requestParameters.put("response_type", Collections.singletonList("code")); + requestParameters.put("response_mode", Collections.singletonList("query")); + requestParameters.put("client_id", Collections.singletonList( + publicClientApplication.clientId())); + requestParameters.put("redirect_uri", Collections.singletonList( + interactiveRequestParameters.redirectUri().toString())); + requestParameters.put("correlation_id", Collections.singletonList( + publicClientApplication.correlationId())); + + return requestParameters; + } +} diff --git a/src/main/java/com/microsoft/aad/msal4j/InteractiveRequestParameters.java b/src/main/java/com/microsoft/aad/msal4j/InteractiveRequestParameters.java new file mode 100644 index 00000000..837a0d97 --- /dev/null +++ b/src/main/java/com/microsoft/aad/msal4j/InteractiveRequestParameters.java @@ -0,0 +1,37 @@ +package com.microsoft.aad.msal4j; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NonNull; +import lombok.experimental.Accessors; + +import java.net.URI; +import java.util.Set; + +/** + * Object containing parameters for interactive requests. Can be used as parameter to + * {@link PublicClientApplication#acquireToken(InteractiveRequestParameters)} + */ +@Builder +@Accessors(fluent = true) +@Getter +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class InteractiveRequestParameters { + + @NonNull + private Set scopes; + + @NonNull + private URI redirectUri; + + /** + * Sets system browser options to be used by the PublicClientApplication + * @param systemBrowserOptions System browser options when using acquireTokenInteractiveRequest + * @return instance of the Builder on which method was called + */ + private SystemBrowserOptions systemBrowserOptions; + + +} diff --git a/src/main/java/com/microsoft/aad/msal4j/OpenBrowserAction.java b/src/main/java/com/microsoft/aad/msal4j/OpenBrowserAction.java new file mode 100644 index 00000000..8a65c707 --- /dev/null +++ b/src/main/java/com/microsoft/aad/msal4j/OpenBrowserAction.java @@ -0,0 +1,12 @@ +package com.microsoft.aad.msal4j; + +import java.net.URI; + +/** + * Interface to be implemented to override system browser initialization logic. Otherwise, + * PublicClientApplication defaults to using default system browser + */ +public interface OpenBrowserAction { + void openBrowser(URI uri); +} + diff --git a/src/main/java/com/microsoft/aad/msal4j/PublicClientApplication.java b/src/main/java/com/microsoft/aad/msal4j/PublicClientApplication.java index d34ae186..a59ecabb 100644 --- a/src/main/java/com/microsoft/aad/msal4j/PublicClientApplication.java +++ b/src/main/java/com/microsoft/aad/msal4j/PublicClientApplication.java @@ -5,6 +5,9 @@ import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod; import com.nimbusds.oauth2.sdk.id.ClientID; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.experimental.Accessors; import org.slf4j.LoggerFactory; import java.util.concurrent.CompletableFuture; @@ -70,6 +73,19 @@ public CompletableFuture acquireToken(DeviceCodeFlowParam return future; } + @Override + public CompletableFuture acquireToken(InteractiveRequestParameters parameters){ + + validateNotNull("parameters", parameters); + + InteractiveRequest interactiveRequest = new InteractiveRequest( + parameters, + this, + createRequestContext(PublicApi.ACQUIRE_TOKEN_BY_AUTHORIZATION_CODE)); + + return executeRequest(interactiveRequest); + } + private PublicClientApplication(Builder builder) { super(builder); diff --git a/src/main/java/com/microsoft/aad/msal4j/SystemBrowserOptions.java b/src/main/java/com/microsoft/aad/msal4j/SystemBrowserOptions.java new file mode 100644 index 00000000..495ef9af --- /dev/null +++ b/src/main/java/com/microsoft/aad/msal4j/SystemBrowserOptions.java @@ -0,0 +1,23 @@ +package com.microsoft.aad.msal4j; + +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + +import java.net.URI; + +@Accessors(fluent = true) +@Getter +@Setter +public class SystemBrowserOptions { + + private String htmlMessageSuccess; + + private String htmlMessageError; + + private URI browserRedirectSuccess; + + private URI browserRedirectError; + + private OpenBrowserAction openBrowserAction; +} diff --git a/src/integrationtest/java/infrastructure/TcpListener.java b/src/main/java/com/microsoft/aad/msal4j/TcpListener.java similarity index 84% rename from src/integrationtest/java/infrastructure/TcpListener.java rename to src/main/java/com/microsoft/aad/msal4j/TcpListener.java index fba18a54..e61670c1 100644 --- a/src/integrationtest/java/infrastructure/TcpListener.java +++ b/src/main/java/com/microsoft/aad/msal4j/TcpListener.java @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package infrastructure; +package com.microsoft.aad.msal4j; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -15,7 +15,7 @@ public class TcpListener implements AutoCloseable{ - private final static Logger LOG = LoggerFactory.getLogger(SeleniumExtensions.class); + private final static Logger LOG = LoggerFactory.getLogger(TcpListener.class); private BlockingQueue authorizationCodeQueue; private BlockingQueue tcpStartUpNotificationQueue; @@ -24,13 +24,14 @@ public class TcpListener implements AutoCloseable{ public TcpListener(BlockingQueue authorizationCodeQueue, BlockingQueue tcpStartUpNotificationQueue){ + this.authorizationCodeQueue = authorizationCodeQueue; this.tcpStartUpNotificationQueue = tcpStartUpNotificationQueue; } - public void startServer(){ + public void startServer(int[] ports){ Runnable serverTask = () -> { - try(ServerSocket serverSocket = createSocket()) { + try(ServerSocket serverSocket = createSocket(ports)) { port = serverSocket.getLocalPort(); tcpStartUpNotificationQueue.put(Boolean.TRUE); Socket clientSocket = serverSocket.accept(); @@ -76,8 +77,8 @@ public void run(){ } } - public ServerSocket createSocket() throws IOException { - int[] ports = { 3843,4584, 4843, 60000 }; + public ServerSocket createSocket(int[] ports) throws IOException { + for (int port : ports) { try { return new ServerSocket(port); @@ -85,7 +86,9 @@ public ServerSocket createSocket() throws IOException { LOG.warn("Port: " + port + "is blocked"); } } - throw new IOException("no free port found"); + throw new MsalClientException(String.format( + "Unable to open port specified in redirect URI. Make sure port %s is not being used" + + "by another process", ports.toString()), AuthenticationErrorCode.PORT_BLOCKED); } public int getPort() { From 29c6a81554a27a149df7759ed73abe2982f3a0a2 Mon Sep 17 00:00:00 2001 From: sgonzalezMSFT Date: Mon, 25 Nov 2019 18:23:34 -0800 Subject: [PATCH 02/24] Add server telemetry with v2 schema --- .../msal4j/AcquireTokenSilentSupplier.java | 2 + .../msal4j/AuthenticationResultSupplier.java | 17 +- .../aad/msal4j/ClientApplicationBase.java | 2 +- .../microsoft/aad/msal4j/CurrentRequest.java | 22 +++ .../com/microsoft/aad/msal4j/MsalRequest.java | 19 +- .../msal4j/MsalServiceExceptionFactory.java | 2 +- .../aad/msal4j/OAuthHttpRequest.java | 5 + .../aad/msal4j/ServerSideTelemetry.java | 116 ++++++++++++ .../microsoft/aad/msal4j/ServiceBundle.java | 7 + .../microsoft/aad/msal4j/SilentRequest.java | 3 + .../microsoft/aad/msal4j/StringHelper.java | 2 + .../aad/msal4j/ServerTelemetryTests.java | 167 ++++++++++++++++++ 12 files changed, 357 insertions(+), 7 deletions(-) create mode 100644 src/main/java/com/microsoft/aad/msal4j/CurrentRequest.java create mode 100644 src/main/java/com/microsoft/aad/msal4j/ServerSideTelemetry.java create mode 100644 src/test/java/com/microsoft/aad/msal4j/ServerTelemetryTests.java diff --git a/src/main/java/com/microsoft/aad/msal4j/AcquireTokenSilentSupplier.java b/src/main/java/com/microsoft/aad/msal4j/AcquireTokenSilentSupplier.java index 9ae01494..b7481efd 100644 --- a/src/main/java/com/microsoft/aad/msal4j/AcquireTokenSilentSupplier.java +++ b/src/main/java/com/microsoft/aad/msal4j/AcquireTokenSilentSupplier.java @@ -57,6 +57,8 @@ AuthenticationResult execute() throws Exception { if(res == null || StringHelper.isBlank(res.accessToken())){ throw new MsalClientException(AuthenticationErrorMessage.NO_TOKEN_IN_CACHE, AuthenticationErrorCode.CACHE_MISS); } + + ServerSideTelemetry.incrementSilentSuccessfulCount(); return res; } } diff --git a/src/main/java/com/microsoft/aad/msal4j/AuthenticationResultSupplier.java b/src/main/java/com/microsoft/aad/msal4j/AuthenticationResultSupplier.java index 3ea34b1c..a3259e89 100644 --- a/src/main/java/com/microsoft/aad/msal4j/AuthenticationResultSupplier.java +++ b/src/main/java/com/microsoft/aad/msal4j/AuthenticationResultSupplier.java @@ -66,10 +66,23 @@ public IAuthenticationResult get() { } } catch(Exception ex) { + String error; if (ex instanceof MsalServiceException) { - apiEvent.setApiErrorCode(((MsalServiceException) ex).errorCode()); + error = ((MsalServiceException) ex).errorCode(); + apiEvent.setApiErrorCode(error); + } else { + if(ex.getCause() != null){ + error = ex.getCause().toString(); + } else { + error = StringHelper.EMPTY_STRING; + } } + ServerSideTelemetry.addFailedRequestTelemetry( + String.valueOf(msalRequest.requestContext().getAcquireTokenPublicApi().getApiId()), + msalRequest.requestContext().getCorrelationId(), + error); + clientApplication.log.error( LogHelper.createMessage( "Execution of " + this.getClass() + " failed.", @@ -81,7 +94,7 @@ public IAuthenticationResult get() { return result; } - void logResult(AuthenticationResult result, ClientDataHttpHeaders headers) + private void logResult(AuthenticationResult result, ClientDataHttpHeaders headers) { if (!StringHelper.isBlank(result.accessToken())) { diff --git a/src/main/java/com/microsoft/aad/msal4j/ClientApplicationBase.java b/src/main/java/com/microsoft/aad/msal4j/ClientApplicationBase.java index 0ed4b0a8..f0930f47 100644 --- a/src/main/java/com/microsoft/aad/msal4j/ClientApplicationBase.java +++ b/src/main/java/com/microsoft/aad/msal4j/ClientApplicationBase.java @@ -225,7 +225,7 @@ abstract static class Builder> { private String authority = DEFAULT_AUTHORITY; private Authority authenticationAuthority = createDefaultAADAuthority(); private boolean validateAuthority = true; - private String correlationId = UUID.randomUUID().toString(); + private String correlationId; private boolean logPii = false; private ExecutorService executorService; private Proxy proxy; diff --git a/src/main/java/com/microsoft/aad/msal4j/CurrentRequest.java b/src/main/java/com/microsoft/aad/msal4j/CurrentRequest.java new file mode 100644 index 00000000..a5ef5085 --- /dev/null +++ b/src/main/java/com/microsoft/aad/msal4j/CurrentRequest.java @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.aad.msal4j; + +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + +@Getter +@Accessors(fluent = true) +class CurrentRequest { + + private final PublicApi publicApi; + + @Setter + private boolean forceRefresh = false; + + CurrentRequest(PublicApi publicApi){ + this.publicApi = publicApi; + } +} \ No newline at end of file diff --git a/src/main/java/com/microsoft/aad/msal4j/MsalRequest.java b/src/main/java/com/microsoft/aad/msal4j/MsalRequest.java index 3ca12638..f3c8f01f 100644 --- a/src/main/java/com/microsoft/aad/msal4j/MsalRequest.java +++ b/src/main/java/com/microsoft/aad/msal4j/MsalRequest.java @@ -12,14 +12,27 @@ @Getter(AccessLevel.PACKAGE) @AllArgsConstructor abstract class MsalRequest { - private final ClientApplicationBase application; AbstractMsalAuthorizationGrant msalAuthorizationGrant; + private final ClientApplicationBase application; + private final RequestContext requestContext; - @Getter(value = AccessLevel.PACKAGE, lazy = true) + @Getter(lazy = true) private final ClientDataHttpHeaders headers = new ClientDataHttpHeaders(requestContext.getCorrelationId()); -} + MsalRequest(ClientApplicationBase clientApplicationBase, + AbstractMsalAuthorizationGrant abstractMsalAuthorizationGrant, + RequestContext requestContext){ + this.application = clientApplicationBase; + this.msalAuthorizationGrant = abstractMsalAuthorizationGrant; + this.requestContext = requestContext; + + CurrentRequest currentRequest = new CurrentRequest(requestContext.getAcquireTokenPublicApi()); + application.getServiceBundle().getServerSideTelemetry().setCurrentRequest(currentRequest); + } + + +} diff --git a/src/main/java/com/microsoft/aad/msal4j/MsalServiceExceptionFactory.java b/src/main/java/com/microsoft/aad/msal4j/MsalServiceExceptionFactory.java index 4c36e8cc..fcae7f0d 100644 --- a/src/main/java/com/microsoft/aad/msal4j/MsalServiceExceptionFactory.java +++ b/src/main/java/com/microsoft/aad/msal4j/MsalServiceExceptionFactory.java @@ -34,7 +34,7 @@ static MsalServiceException fromHttpResponse(HTTPResponse httpResponse){ errorResponse.error().equalsIgnoreCase(AuthenticationErrorCode.INVALID_GRANT)) { if(isInteractionRequired(errorResponse.subError)){ - throw new MsalInteractionRequiredException(errorResponse, httpResponse.getHeaderMap()); + return new MsalInteractionRequiredException(errorResponse, httpResponse.getHeaderMap()); } } diff --git a/src/main/java/com/microsoft/aad/msal4j/OAuthHttpRequest.java b/src/main/java/com/microsoft/aad/msal4j/OAuthHttpRequest.java index 192b9b58..8e81e784 100644 --- a/src/main/java/com/microsoft/aad/msal4j/OAuthHttpRequest.java +++ b/src/main/java/com/microsoft/aad/msal4j/OAuthHttpRequest.java @@ -62,6 +62,11 @@ private Map configureHttpHeaders(){ if (this.getAuthorization() != null) { httpHeaders.put("Authorization", this.getAuthorization()); } + + Map telemetryHeaders = + serviceBundle.getServerSideTelemetry().getServerTelemetryHeaderMap(); + httpHeaders.putAll(telemetryHeaders); + return httpHeaders; } diff --git a/src/main/java/com/microsoft/aad/msal4j/ServerSideTelemetry.java b/src/main/java/com/microsoft/aad/msal4j/ServerSideTelemetry.java new file mode 100644 index 00000000..653e02d5 --- /dev/null +++ b/src/main/java/com/microsoft/aad/msal4j/ServerSideTelemetry.java @@ -0,0 +1,116 @@ +package com.microsoft.aad.msal4j; + +import java.lang.reflect.Array; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicInteger; + +class ServerSideTelemetry { + + private final static String SCHEMA_VERSION = "2"; + private final static String SCHEMA_PIPE_DELIMITER = "|"; + private final static String SCHEMA_COMMA_DELIMITER = ","; + private final static String CURRENT_REQUEST_HEADER_NAME = "x-client-current-telemetry"; + private final static String LAST_REQUEST_HEADER_NAME = "x-client-last-telemetry"; + + private CurrentRequest currentRequest; + + static AtomicInteger silentSuccessfulCount = new AtomicInteger(0); + static ConcurrentMap previousRequests = new ConcurrentHashMap<>(); + + synchronized Map getServerTelemetryHeaderMap(){ + Map headerMap = new HashMap<>(); + + headerMap.put(CURRENT_REQUEST_HEADER_NAME, buildCurrentRequestHeader()); + headerMap.put(LAST_REQUEST_HEADER_NAME, buildLastRequestHeader()); + + return headerMap; + } + + static void addFailedRequestTelemetry(String publicApiId, String correlationId, String error){ + + String[] previousRequest = new String[]{publicApiId, error}; + previousRequests.put( + correlationId, + previousRequest); + } + + static void incrementSilentSuccessfulCount(){ + silentSuccessfulCount.incrementAndGet(); + } + + synchronized CurrentRequest getCurrentRequest() { + return currentRequest; + } + + synchronized void setCurrentRequest(CurrentRequest currentRequest) { + this.currentRequest = currentRequest; + } + + private synchronized String buildCurrentRequestHeader(){ + if(currentRequest == null){ + return StringHelper.EMPTY_STRING; + } + + return SCHEMA_VERSION + + SCHEMA_PIPE_DELIMITER + + currentRequest.publicApi().getApiId() + + SCHEMA_COMMA_DELIMITER + + currentRequest.forceRefresh() + + SCHEMA_PIPE_DELIMITER; + } + + private synchronized String buildLastRequestHeader() { + + // LastRequest header schema: + // schema_version|silent_successful_count|api_id1,correlation_id1|error1| + StringBuilder lastRequestBuilder = new StringBuilder(); + + lastRequestBuilder + .append(SCHEMA_VERSION) + .append(SCHEMA_PIPE_DELIMITER) + .append(silentSuccessfulCount.getAndSet(0)); + + if (previousRequests.isEmpty()) { + // Kusto queries always expect all delimiters so return + // "schema_version|silent_successful_count|||" + return lastRequestBuilder + .append(SCHEMA_PIPE_DELIMITER) + .append(SCHEMA_PIPE_DELIMITER) + .append(SCHEMA_PIPE_DELIMITER) + .toString(); + } + + StringBuilder middleSegmentBuilder = new StringBuilder(SCHEMA_PIPE_DELIMITER); + StringBuilder errorSegmentBuilder = new StringBuilder(SCHEMA_PIPE_DELIMITER); + + Iterator it = previousRequests.keySet().iterator(); + + // Total header size should be less than 8kb. At max, we will use 4kb for telemetry. + while (it.hasNext() + && (middleSegmentBuilder.length() + errorSegmentBuilder.length()) < 3800) + { + String correlationId = it.next(); + String[] previousRequest = previousRequests.get(correlationId); + String apiId = (String)Array.get(previousRequest, 0); + String error = (String)Array.get(previousRequest, 1); + + middleSegmentBuilder.append(apiId).append(SCHEMA_COMMA_DELIMITER).append(correlationId); + errorSegmentBuilder.append(error); + + if(it.hasNext()){ + middleSegmentBuilder.append(SCHEMA_COMMA_DELIMITER); + errorSegmentBuilder.append(SCHEMA_COMMA_DELIMITER); + } + + it.remove(); + } + + errorSegmentBuilder.append(SCHEMA_PIPE_DELIMITER); + + return lastRequestBuilder.append(middleSegmentBuilder).append(errorSegmentBuilder).toString(); + } +} diff --git a/src/main/java/com/microsoft/aad/msal4j/ServiceBundle.java b/src/main/java/com/microsoft/aad/msal4j/ServiceBundle.java index a7d30f3c..704544e1 100644 --- a/src/main/java/com/microsoft/aad/msal4j/ServiceBundle.java +++ b/src/main/java/com/microsoft/aad/msal4j/ServiceBundle.java @@ -10,12 +10,15 @@ class ServiceBundle { private ExecutorService executorService; private TelemetryManager telemetryManager; private IHttpClient httpClient; + private ServerSideTelemetry serverSideTelemetry; ServiceBundle(ExecutorService executorService, IHttpClient httpClient, TelemetryManager telemetryManager){ this.executorService = executorService; this.telemetryManager = telemetryManager; this.httpClient = httpClient; + + serverSideTelemetry = new ServerSideTelemetry(); } ExecutorService getExecutorService() { @@ -29,4 +32,8 @@ TelemetryManager getTelemetryManager(){ IHttpClient getHttpClient(){ return httpClient; } + + ServerSideTelemetry getServerSideTelemetry(){ + return serverSideTelemetry; + } } \ No newline at end of file diff --git a/src/main/java/com/microsoft/aad/msal4j/SilentRequest.java b/src/main/java/com/microsoft/aad/msal4j/SilentRequest.java index 371d0e67..3c43d2bc 100644 --- a/src/main/java/com/microsoft/aad/msal4j/SilentRequest.java +++ b/src/main/java/com/microsoft/aad/msal4j/SilentRequest.java @@ -27,5 +27,8 @@ class SilentRequest extends MsalRequest { this.requestAuthority = StringHelper.isBlank(parameters.authorityUrl()) ? application.authenticationAuthority : Authority.createAuthority(new URL(parameters.authorityUrl())); + + application.getServiceBundle().getServerSideTelemetry().getCurrentRequest().forceRefresh( + parameters.forceRefresh()); } } diff --git a/src/main/java/com/microsoft/aad/msal4j/StringHelper.java b/src/main/java/com/microsoft/aad/msal4j/StringHelper.java index 50d93311..fff62738 100644 --- a/src/main/java/com/microsoft/aad/msal4j/StringHelper.java +++ b/src/main/java/com/microsoft/aad/msal4j/StringHelper.java @@ -5,6 +5,8 @@ final class StringHelper { + static String EMPTY_STRING = ""; + public static boolean isBlank(final String str) { return str == null || str.trim().length() == 0; } diff --git a/src/test/java/com/microsoft/aad/msal4j/ServerTelemetryTests.java b/src/test/java/com/microsoft/aad/msal4j/ServerTelemetryTests.java new file mode 100644 index 00000000..8f71c51c --- /dev/null +++ b/src/test/java/com/microsoft/aad/msal4j/ServerTelemetryTests.java @@ -0,0 +1,167 @@ +package com.microsoft.aad.msal4j; + +import org.testng.Assert; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.Test; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicInteger; + +@Test(groups = { "checkin" }) +public class ServerTelemetryTests { + + private static final String SCHEMA_VERSION = "2"; + private final static String CURRENT_REQUEST_HEADER_NAME = "x-client-current-telemetry"; + private final static String LAST_REQUEST_HEADER_NAME = "x-client-last-telemetry"; + + private final static String PUBLIC_API_ID = String.valueOf(PublicApi.ACQUIRE_TOKEN_BY_AUTHORIZATION_CODE.getApiId()); + private final static String ERROR = "invalid_grant"; + + @AfterMethod + public void resetLastRequestHeader(){ + ServerSideTelemetry.previousRequests = new ConcurrentHashMap<>(); + ServerSideTelemetry.silentSuccessfulCount = new AtomicInteger(0); + } + + @Test + public void serverTelemetryHeaders_correctSchema(){ + + CurrentRequest currentRequest = new CurrentRequest(PublicApi.ACQUIRE_TOKEN_BY_AUTHORIZATION_CODE); + currentRequest.forceRefresh(false); + + ServerSideTelemetry serverSideTelemetry = new ServerSideTelemetry(); + serverSideTelemetry.setCurrentRequest(currentRequest); + + String correlationId = "936732c6-74b9-4783-aad9-fa205eae8763"; + ServerSideTelemetry.addFailedRequestTelemetry(PUBLIC_API_ID, correlationId, ERROR); + + Map headers = serverSideTelemetry.getServerTelemetryHeaderMap(); + + //Current request tests + List currentRequestHeader = Arrays.asList(headers.get(CURRENT_REQUEST_HEADER_NAME).split("\\|")); + + // ["2", "831,false"] + Assert.assertEquals(currentRequestHeader.size(), 2); + Assert.assertEquals(currentRequestHeader.get(0), SCHEMA_VERSION); + + // ["831", "false"] + List secondSegment = Arrays.asList(currentRequestHeader.get(1).split(",")); + Assert.assertEquals(secondSegment.get(0), String.valueOf(PublicApi.ACQUIRE_TOKEN_BY_AUTHORIZATION_CODE.getApiId())); + Assert.assertEquals(secondSegment.get(1), "false"); + + + // Previous request test + List previousRequestHeader = Arrays.asList(headers.get(LAST_REQUEST_HEADER_NAME).split("\\|")); + + // ["2","0","831,936732c6-74b9-4783-aad9-fa205eae8763","invalid_grant"] + Assert.assertEquals(previousRequestHeader.size(), 4); + Assert.assertEquals(previousRequestHeader.get(0), SCHEMA_VERSION); + Assert.assertEquals(previousRequestHeader.get(1), "0"); + Assert.assertEquals(previousRequestHeader.get(3), ERROR); + + List thirdSegment = Arrays.asList(previousRequestHeader.get(2).split(",")); + + Assert.assertEquals(thirdSegment.get(0), PUBLIC_API_ID); + Assert.assertEquals(thirdSegment.get(1), correlationId); + } + + @Test + public void serverTelemetryHeaders_previewsRequestNull(){ + + for(int i = 0; i < 3; i++){ + ServerSideTelemetry.incrementSilentSuccessfulCount(); + } + + ServerSideTelemetry serverSideTelemetry = new ServerSideTelemetry(); + Map headers = serverSideTelemetry.getServerTelemetryHeaderMap(); + + Assert.assertEquals(headers.get(LAST_REQUEST_HEADER_NAME), "2|3|||"); + } + + @Test + public void serverTelemetryHeader_testMaximumHeaderSize(){ + + ServerSideTelemetry serverSideTelemetry = new ServerSideTelemetry(); + + for(int i = 0; i <100; i++){ + String correlationId = UUID.randomUUID().toString(); + ServerSideTelemetry.addFailedRequestTelemetry(PUBLIC_API_ID, correlationId, ERROR); + } + + Map headers = serverSideTelemetry.getServerTelemetryHeaderMap(); + + String lastRequest = headers.get(LAST_REQUEST_HEADER_NAME); + + // http headers are encoded in ISO_8859_1 + byte[] lastRequestBytes = lastRequest.getBytes(StandardCharsets.ISO_8859_1); + + Assert.assertTrue(lastRequestBytes.length < 4000); + } + + @Test + public void serverTelemetryHeaders_multipleThreadsWrite(){ + + ExecutorService executor = Executors.newFixedThreadPool(10); + + try{ + for ( int i=0; i < 10; i++){ + executor.execute(new FailedRequestRunnable()); + executor.execute(new SilentSuccessfulRequestRunnable()); + } + } catch (Exception ex){ + ex.printStackTrace(); + } + executor.shutdown(); + + ServerSideTelemetry serverSideTelemetry = new ServerSideTelemetry(); + + try{ + Thread.sleep( 1000); + } catch(InterruptedException ex){ + ex.printStackTrace(); + } + + Map headers = serverSideTelemetry.getServerTelemetryHeaderMap(); + + List previousRequestHeader = Arrays.asList(headers.get(LAST_REQUEST_HEADER_NAME).split("\\|")); + + Assert.assertEquals(previousRequestHeader.get(1), "10"); + + List thirdSegment = Arrays.asList(previousRequestHeader.get(2).split(",")); + Assert.assertEquals(thirdSegment.size(), 20); + + List fourthSegment = Arrays.asList(previousRequestHeader.get(3).split(",")); + Assert.assertEquals(fourthSegment.size(), 10); + } + + class FailedRequestRunnable implements Runnable { + + public void run(){ + + Random rand = new Random(); + int n = rand.nextInt(250); + try { + Thread.sleep(n); + } catch (InterruptedException ex){ + ex.printStackTrace(); + } + String correlationId = UUID.randomUUID().toString(); + ServerSideTelemetry.addFailedRequestTelemetry(PUBLIC_API_ID, correlationId, ERROR); + } + } + + class SilentSuccessfulRequestRunnable implements Runnable { + + public void run(){ + ServerSideTelemetry.incrementSilentSuccessfulCount(); + } + } +} From 9172b8334e3ec515c53cddae3c2ce1f5c568384f Mon Sep 17 00:00:00 2001 From: Robert Munteanu Date: Thu, 12 Dec 2019 17:54:28 +0100 Subject: [PATCH 03/24] Add OSGi support via the bnd-maven-plugin --- bnd.bnd | 1 + pom.xml | 13 +++++++++++++ 2 files changed, 14 insertions(+) create mode 100644 bnd.bnd diff --git a/bnd.bnd b/bnd.bnd new file mode 100644 index 00000000..9cb45beb --- /dev/null +++ b/bnd.bnd @@ -0,0 +1 @@ +Export-Package: com.microsoft.aad.msal4j diff --git a/pom.xml b/pom.xml index 40bb2d4b..86b3d5be 100644 --- a/pom.xml +++ b/pom.xml @@ -162,6 +162,7 @@ true true + ${project.build.outputDirectory}/META-INF/MANIFEST.MF @@ -249,6 +250,18 @@ + + biz.aQute.bnd + bnd-maven-plugin + 4.3.1 + + + + bnd-process + + + + From 0eed34ad6b7065739e6e8b88d93d7c0e168c3372 Mon Sep 17 00:00:00 2001 From: Santiago Gonzalez <35743865+sangonzal@users.noreply.github.com> Date: Thu, 16 Jan 2020 17:16:31 +0000 Subject: [PATCH 04/24] Delete Releases.md Not being used. Releases are outlined in changelog.txt --- RELEASES.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 RELEASES.md diff --git a/RELEASES.md b/RELEASES.md deleted file mode 100644 index e69de29b..00000000 From 6a72fac4e12162ecf62e026809b69f25c62e2f9c Mon Sep 17 00:00:00 2001 From: sgonzalezMSFT Date: Wed, 22 Jan 2020 14:59:13 -0800 Subject: [PATCH 05/24] Add interactive flow sample. Add PKCE support --- ...AcquireTokenByInteractiveFlowSupplier.java | 59 ++++++++++++++----- .../com/microsoft/aad/msal4j/ApiEvent.java | 4 +- .../msal4j/AuthorizationCodeParameters.java | 5 ++ .../aad/msal4j/ClientApplicationBase.java | 4 ++ .../java/com/microsoft/aad/msal4j/Event.java | 12 ---- ...gratedWindowsAuthenticationParameters.java | 2 +- .../aad/msal4j/InteractiveRequest.java | 47 +++++++++++++-- .../aad/msal4j/PublicClientApplication.java | 7 ++- .../microsoft/aad/msal4j/StringHelper.java | 21 ++++++- .../com/microsoft/aad/msal4j/TcpListener.java | 2 +- .../public-client/InteractiveFlow.java | 34 +++++++++++ 11 files changed, 160 insertions(+), 37 deletions(-) create mode 100644 src/samples/public-client/InteractiveFlow.java diff --git a/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByInteractiveFlowSupplier.java b/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByInteractiveFlowSupplier.java index 104cd0f0..4b0aaf97 100644 --- a/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByInteractiveFlowSupplier.java +++ b/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByInteractiveFlowSupplier.java @@ -1,6 +1,7 @@ package com.microsoft.aad.msal4j; import java.awt.*; +import java.io.IOException; import java.net.URI; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; @@ -24,10 +25,9 @@ public class AcquireTokenByInteractiveFlowSupplier extends AuthenticationResultS } // This where the high level logic for the flow will go - AuthenticationResult execute() { + AuthenticationResult execute() throws Exception{ String authorizationCode = getAuthorizationCode(); - acquireTokenWithAuthorizationCode(authorizationCode); - + return acquireTokenWithAuthorizationCode(authorizationCode); } private String getAuthorizationCode(){ @@ -45,7 +45,8 @@ private String getAuthorizationCode(){ throw new RuntimeException("Could not start TCP listener"); } - if(interactiveRequest.interactiveRequestParameters.systemBrowserOptions().openBrowserAction() != null){ + SystemBrowserOptions options = interactiveRequest.interactiveRequestParameters.systemBrowserOptions(); + if(options != null && options.openBrowserAction() != null){ interactiveRequest.interactiveRequestParameters.systemBrowserOptions().openBrowserAction().openBrowser( interactiveRequest.authorizationURI); } else { @@ -60,10 +61,6 @@ private String getAuthorizationCode(){ return parseServerResponse(authServerResponse, clientApplication.authenticationAuthority.authorityType); } - private AuthenticationResult acquireTokenWithAuthorizationCode(String authorizationCode){ - - } - private void startTcpListener(BlockingQueue tcpStartUpNotifierQueue){ AuthorizationCodeQueue = new LinkedBlockingQueue<>(); @@ -79,20 +76,32 @@ private void startTcpListener(BlockingQueue tcpStartUpNotifierQueue){ } private void openDefaultSystemBrowser(URI uri){ - - if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) { - Desktop.getDesktop().browse(uri); + try{ + if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) { + Desktop.getDesktop().browse(uri); + } + } catch(IOException e){ + throw new MsalClientException(e); } } private String getResponseFromTcpListener(){ - String response; + + String response = null; try { - response = AuthorizationCodeQueue.poll(300, TimeUnit.SECONDS); - if (StringHelper.isBlank(response)){ + long expirationTime = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()) + 300; + + while(StringHelper.isBlank(response) && !interactiveRequest.futureReference.get().isCancelled() && + TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()) < expirationTime) { + + response = AuthorizationCodeQueue.poll(100, TimeUnit.MILLISECONDS); + } + + if (StringHelper.isBlank(response)) { throw new MsalClientException("No Authorization code was returned from the server", AuthenticationErrorCode.AUTHORIZATION_CODE_BLANK); } + } catch(Exception e){ throw new MsalClientException(e); } @@ -119,4 +128,26 @@ private String parseServerResponse(String serverResponse, AuthorityType authorit return matcher.group(0); } + private AuthenticationResult acquireTokenWithAuthorizationCode(String authorizationCode) + throws Exception{ + + AuthorizationCodeParameters parameters = AuthorizationCodeParameters + .builder(authorizationCode, interactiveRequest.interactiveRequestParameters.redirectUri()) + .scopes(interactiveRequest.interactiveRequestParameters.scopes()) + .codeVerifier(interactiveRequest.verifier()) + .build(); + + AuthorizationCodeRequest authCodeRequest = new AuthorizationCodeRequest( + parameters, + clientApplication, + clientApplication.createRequestContext(PublicApi.ACQUIRE_TOKEN_BY_AUTHORIZATION_CODE)); + + AcquireTokenByAuthorizationGrantSupplier acquireTokenByAuthorizationGrantSupplier = + new AcquireTokenByAuthorizationGrantSupplier( + clientApplication, + authCodeRequest, + clientApplication.authenticationAuthority); + + return acquireTokenByAuthorizationGrantSupplier.execute(); + } } \ No newline at end of file diff --git a/src/main/java/com/microsoft/aad/msal4j/ApiEvent.java b/src/main/java/com/microsoft/aad/msal4j/ApiEvent.java index 3946c3ff..1fab6ff7 100644 --- a/src/main/java/com/microsoft/aad/msal4j/ApiEvent.java +++ b/src/main/java/com/microsoft/aad/msal4j/ApiEvent.java @@ -41,7 +41,7 @@ public void setAuthorityType(String authorityType){ public void setTenantId(String tenantId){ if(!StringHelper.isBlank(tenantId) && logPii){ - this.put(TENANT_ID_KEY, hashPii(tenantId)); + this.put(TENANT_ID_KEY, StringHelper.createBase64EncodedSha256Hash(tenantId)); } else { this.put(TENANT_ID_KEY, null); } @@ -49,7 +49,7 @@ public void setTenantId(String tenantId){ public void setAccountId(String accountId){ if(!StringHelper.isBlank(accountId) && logPii){ - this.put(USER_ID_KEY, hashPii(accountId)); + this.put(USER_ID_KEY, StringHelper.createBase64EncodedSha256Hash(accountId)); } else { this.put(USER_ID_KEY, null); } diff --git a/src/main/java/com/microsoft/aad/msal4j/AuthorizationCodeParameters.java b/src/main/java/com/microsoft/aad/msal4j/AuthorizationCodeParameters.java index f8ecdeca..0ba4cff1 100644 --- a/src/main/java/com/microsoft/aad/msal4j/AuthorizationCodeParameters.java +++ b/src/main/java/com/microsoft/aad/msal4j/AuthorizationCodeParameters.java @@ -22,6 +22,11 @@ @AllArgsConstructor(access = AccessLevel.PRIVATE) public class AuthorizationCodeParameters { + /** + * Code verifier used for PKCE + */ + private String codeVerifier; + private Set scopes; @NonNull diff --git a/src/main/java/com/microsoft/aad/msal4j/ClientApplicationBase.java b/src/main/java/com/microsoft/aad/msal4j/ClientApplicationBase.java index 72fc5c49..2c3bcb0f 100644 --- a/src/main/java/com/microsoft/aad/msal4j/ClientApplicationBase.java +++ b/src/main/java/com/microsoft/aad/msal4j/ClientApplicationBase.java @@ -184,6 +184,10 @@ private AuthenticationResultSupplier getAuthenticationResultSupplier(MsalRequest (DeviceCodeFlowRequest) msalRequest); } else if (msalRequest instanceof SilentRequest) { supplier = new AcquireTokenSilentSupplier(this, (SilentRequest) msalRequest); + } else if(msalRequest instanceof InteractiveRequest){ + supplier = new AcquireTokenByInteractiveFlowSupplier( + (PublicClientApplication) this, + (InteractiveRequest) msalRequest); } else { supplier = new AcquireTokenByAuthorizationGrantSupplier( this, diff --git a/src/main/java/com/microsoft/aad/msal4j/Event.java b/src/main/java/com/microsoft/aad/msal4j/Event.java index 6baf2598..34ef7a24 100644 --- a/src/main/java/com/microsoft/aad/msal4j/Event.java +++ b/src/main/java/com/microsoft/aad/msal4j/Event.java @@ -65,16 +65,4 @@ static String scrubTenant(URI uri){ String scrubbedPath = String.join("/", segment); return uri.getScheme() + "://" + uri.getAuthority() + scrubbedPath; } - - static String hashPii(String stringToHash){ - String base64EncodedSha256Hash; - try { - MessageDigest digest = MessageDigest.getInstance("SHA-256"); - byte[] hashedString = digest.digest(stringToHash.getBytes(StandardCharsets.UTF_8)); - base64EncodedSha256Hash = new String(Base64.getEncoder().encode(hashedString), StandardCharsets.UTF_8); - } catch(NoSuchAlgorithmException e){ - base64EncodedSha256Hash = null; - } - return base64EncodedSha256Hash; - } } diff --git a/src/main/java/com/microsoft/aad/msal4j/IntegratedWindowsAuthenticationParameters.java b/src/main/java/com/microsoft/aad/msal4j/IntegratedWindowsAuthenticationParameters.java index df9fb12c..c1c2e5a2 100644 --- a/src/main/java/com/microsoft/aad/msal4j/IntegratedWindowsAuthenticationParameters.java +++ b/src/main/java/com/microsoft/aad/msal4j/IntegratedWindowsAuthenticationParameters.java @@ -13,7 +13,7 @@ /** * Object containing parameters for Integrated Windows Authentication. Can be used as parameter to - * {@link PublicClientApplication#acquireToken(IntegratedWindowsAuthenticationParameters)} + * {@link PublicClientApplication#acquireToken(IntegratedWindowsAuthenticationParameters)}` */ @Builder @Accessors(fluent = true) diff --git a/src/main/java/com/microsoft/aad/msal4j/InteractiveRequest.java b/src/main/java/com/microsoft/aad/msal4j/InteractiveRequest.java index 3d855898..7d58c54d 100644 --- a/src/main/java/com/microsoft/aad/msal4j/InteractiveRequest.java +++ b/src/main/java/com/microsoft/aad/msal4j/InteractiveRequest.java @@ -1,32 +1,50 @@ package com.microsoft.aad.msal4j; import com.nimbusds.oauth2.sdk.util.URLUtils; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.experimental.Accessors; import java.net.InetAddress; import java.net.URI; import java.net.URISyntaxException; import java.net.UnknownHostException; +import java.security.SecureRandom; +import java.util.Base64; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicReference; +@Getter(AccessLevel.PACKAGE) +@Accessors(fluent = true) class InteractiveRequest extends MsalRequest{ + AtomicReference> futureReference; + private PublicClientApplication publicClientApplication; + URI authorizationURI; InteractiveRequestParameters interactiveRequestParameters; - private PublicClientApplication publicClientApplication; + + private String verifier; + private String state; InteractiveRequest(InteractiveRequestParameters parameters, + AtomicReference> futureReference, PublicClientApplication publicClientApplication, RequestContext requestContext){ super(publicClientApplication, null, requestContext); - validateRedirectURI(parameters.redirectUri()); - this.authorizationURI = createAuthorizationURI(); this.interactiveRequestParameters = parameters; + this.futureReference = futureReference; this.publicClientApplication = publicClientApplication; + this.authorizationURI = createAuthorizationURI(); + validateRedirectURI(parameters.redirectUri()); + } private void validateRedirectURI(URI redirectURI) { @@ -39,7 +57,7 @@ private void validateRedirectURI(URI redirectURI) { AuthenticationErrorCode.LOOPBACK_REDIRECT_URI); } - if (redirectURI.getScheme() != null) { + if (!redirectURI.getScheme().equals("http")) { throw new MsalClientException(String.format( "Only http uri scheme is supported but %s was found. Configure http://localhost" + "or http://localhost:port both during app registration and when you create" + @@ -71,8 +89,11 @@ private Map> createAuthorizationRequestParameters(){ Map> requestParameters = new HashMap<>(); + addPkceAndState(requestParameters); + String scopesParam = AbstractMsalAuthorizationGrant.COMMON_SCOPES_PARAM + - AbstractMsalAuthorizationGrant.SCOPES_DELIMITER + interactiveRequestParameters.scopes(); + AbstractMsalAuthorizationGrant.SCOPES_DELIMITER + + String.join(" ", interactiveRequestParameters.scopes()); requestParameters.put("scope", Collections.singletonList(scopesParam)); requestParameters.put("response_type", Collections.singletonList("code")); @@ -86,4 +107,20 @@ private Map> createAuthorizationRequestParameters(){ return requestParameters; } + + private void addPkceAndState(Map> requestParameters) { + + SecureRandom secureRandom = new SecureRandom(); + byte[] randomBytes = new byte[32]; + secureRandom.nextBytes(randomBytes); + + verifier = Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes); + + requestParameters.put("code_challenge", Collections.singletonList( + StringHelper.createBase64EncodedSha256Hash(verifier))); + requestParameters.put("code_challenge_method", Collections.singletonList("S256")); + + state = UUID.randomUUID().toString() + UUID.randomUUID().toString(); + requestParameters.put("state", Collections.singletonList(state)); + } } diff --git a/src/main/java/com/microsoft/aad/msal4j/PublicClientApplication.java b/src/main/java/com/microsoft/aad/msal4j/PublicClientApplication.java index a59ecabb..465e37fe 100644 --- a/src/main/java/com/microsoft/aad/msal4j/PublicClientApplication.java +++ b/src/main/java/com/microsoft/aad/msal4j/PublicClientApplication.java @@ -78,12 +78,17 @@ public CompletableFuture acquireToken(InteractiveRequestP validateNotNull("parameters", parameters); + AtomicReference> futureReference = new AtomicReference<>(); + InteractiveRequest interactiveRequest = new InteractiveRequest( parameters, + futureReference, this, createRequestContext(PublicApi.ACQUIRE_TOKEN_BY_AUTHORIZATION_CODE)); - return executeRequest(interactiveRequest); + CompletableFuture future = executeRequest(interactiveRequest); + futureReference.set(future); + return future; } private PublicClientApplication(Builder builder) { diff --git a/src/main/java/com/microsoft/aad/msal4j/StringHelper.java b/src/main/java/com/microsoft/aad/msal4j/StringHelper.java index 50d93311..fe73cb6f 100644 --- a/src/main/java/com/microsoft/aad/msal4j/StringHelper.java +++ b/src/main/java/com/microsoft/aad/msal4j/StringHelper.java @@ -3,9 +3,28 @@ package com.microsoft.aad.msal4j; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; + final class StringHelper { - public static boolean isBlank(final String str) { + static boolean isBlank(final String str) { return str == null || str.trim().length() == 0; } + + static String createBase64EncodedSha256Hash(String stringToHash){ + String base64EncodedSha256Hash; + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hashedString = digest.digest(stringToHash.getBytes(StandardCharsets.UTF_8)); + base64EncodedSha256Hash = Base64.getUrlEncoder().withoutPadding().encodeToString(hashedString); + } catch(NoSuchAlgorithmException e){ + base64EncodedSha256Hash = null; + } + return base64EncodedSha256Hash; + } + + } diff --git a/src/main/java/com/microsoft/aad/msal4j/TcpListener.java b/src/main/java/com/microsoft/aad/msal4j/TcpListener.java index e61670c1..a4de65a6 100644 --- a/src/main/java/com/microsoft/aad/msal4j/TcpListener.java +++ b/src/main/java/com/microsoft/aad/msal4j/TcpListener.java @@ -77,7 +77,7 @@ public void run(){ } } - public ServerSocket createSocket(int[] ports) throws IOException { + public ServerSocket createSocket(int[] ports) { for (int port : ports) { try { diff --git a/src/samples/public-client/InteractiveFlow.java b/src/samples/public-client/InteractiveFlow.java new file mode 100644 index 00000000..1b18601f --- /dev/null +++ b/src/samples/public-client/InteractiveFlow.java @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import com.microsoft.aad.msal4j.IAuthenticationResult; +import com.microsoft.aad.msal4j.InteractiveRequestParameters; +import com.microsoft.aad.msal4j.PublicClientApplication; + +import java.net.URI; +import java.util.Collections; + +public class InteractiveFlow { + public static void main(String[] args) throws Exception{ + IAuthenticationResult result = getAccessTokenByInteractiveFlow(); + System.out.println(result.accessToken()); + System.out.println(result.account()); + System.out.println(result.idToken()); + } + + private static IAuthenticationResult getAccessTokenByInteractiveFlow() throws Exception { + + PublicClientApplication publicClientApplication = PublicClientApplication + .builder(TestData.PUBLIC_CLIENT_ID) + .authority(TestData.TENANT_SPECIFIC_AUTHORITY) + .build(); + + InteractiveRequestParameters parameters = InteractiveRequestParameters.builder() + .redirectUri(new URI("http://localhost:8080")) + .scopes(Collections.singleton(TestData.testScope)) + .build(); + + IAuthenticationResult result = publicClientApplication.acquireToken(parameters).join(); + return result; + } +} \ No newline at end of file From d9669823f0e45e86988e98a34c12e17e6f2bc70d Mon Sep 17 00:00:00 2001 From: SomkaPe Date: Mon, 27 Jan 2020 12:26:08 -0800 Subject: [PATCH 06/24] adding sameSite cookie attr support to samples (#166) * adding sameSite cookie attr support to samples --- .../aad/msal4j/DefaultHttpClient.java | 1 + src/samples/msal-b2c-web-sample/pom.xml | 6 ++ .../azure/msalwebsample/AuthFilter.java | 70 ++++---------- .../azure/msalwebsample/CookieHelper.java | 95 +++++++++++++++++++ .../src/main/resources/application.properties | 36 ++++--- src/samples/msal-web-sample/pom.xml | 14 ++- .../azure/msalwebsample/AuthFilter.java | 77 ++++----------- .../msalwebsample/AuthPageController.java | 2 +- .../azure/msalwebsample/CookieHelper.java | 95 +++++++++++++++++++ .../src/main/resources/application.properties | 14 ++- src/samples/spring-security-web-app/pom.xml | 7 +- .../AppConfiguration.java | 12 ++- .../src/main/resources/application.properties | 17 +++- .../aad/msal4j/DefaultHttpClientTest.java | 62 ++++++++++++ 14 files changed, 378 insertions(+), 130 deletions(-) create mode 100644 src/samples/msal-b2c-web-sample/src/main/java/com/microsoft/azure/msalwebsample/CookieHelper.java create mode 100644 src/samples/msal-web-sample/src/main/java/com/microsoft/azure/msalwebsample/CookieHelper.java create mode 100644 src/test/java/com/microsoft/aad/msal4j/DefaultHttpClientTest.java diff --git a/src/main/java/com/microsoft/aad/msal4j/DefaultHttpClient.java b/src/main/java/com/microsoft/aad/msal4j/DefaultHttpClient.java index c1932dc7..6fca83b0 100644 --- a/src/main/java/com/microsoft/aad/msal4j/DefaultHttpClient.java +++ b/src/main/java/com/microsoft/aad/msal4j/DefaultHttpClient.java @@ -99,6 +99,7 @@ private HttpResponse readResponseFromConnection(final HttpsURLConnection conn) t if (responseCode != HttpURLConnection.HTTP_OK) { is = conn.getErrorStream(); if (is != null) { + httpResponse.headers(conn.getHeaderFields()); httpResponse.body(inputStreamToString(is)); } return httpResponse; diff --git a/src/samples/msal-b2c-web-sample/pom.xml b/src/samples/msal-b2c-web-sample/pom.xml index ae544f7b..35209819 100644 --- a/src/samples/msal-b2c-web-sample/pom.xml +++ b/src/samples/msal-b2c-web-sample/pom.xml @@ -35,6 +35,12 @@ json 20090211 + + + org.apache.commons + commons-lang3 + 3.9 + org.springframework.boot diff --git a/src/samples/msal-b2c-web-sample/src/main/java/com/microsoft/azure/msalwebsample/AuthFilter.java b/src/samples/msal-b2c-web-sample/src/main/java/com/microsoft/azure/msalwebsample/AuthFilter.java index 1f69036f..c629ffa6 100644 --- a/src/samples/msal-b2c-web-sample/src/main/java/com/microsoft/azure/msalwebsample/AuthFilter.java +++ b/src/samples/msal-b2c-web-sample/src/main/java/com/microsoft/azure/msalwebsample/AuthFilter.java @@ -37,9 +37,7 @@ @Component public class AuthFilter implements Filter { - private static final String STATES = "states"; private static final String STATE = "state"; - private static final Integer STATE_TTL = 3600; private static final String FAILED_TO_VALIDATE_MESSAGE = "Failed to validate data received from Authorization service - "; private List excludedUrls = Arrays.asList("/", "/msal4jsample/"); @@ -69,6 +67,8 @@ public void doFilter(ServletRequest request, ServletResponse response, if(AuthHelper.containsAuthenticationCode(httpRequest)){ // response should have authentication code, which will be used to acquire access token processAuthenticationCodeRedirect(httpRequest, currentUri, fullUrl); + + CookieHelper.removeStateNonceCookies(httpResponse); } else { // not authenticated, redirecting to login.microsoft.com so user can authenticate sendAuthRedirect(authHelper.configuration.signUpSignInAuthority, httpRequest, httpResponse); @@ -107,7 +107,7 @@ private void processAuthenticationCodeRedirect(HttpServletRequest httpRequest, S params.put(key, Collections.singletonList(httpRequest.getParameterMap().get(key)[0])); } // validate that state in response equals to state in request - StateData stateData = validateState(httpRequest.getSession(), params.get(STATE).get(0)); + validateState(CookieHelper.getCookie(httpRequest, CookieHelper.MSAL_WEB_APP_STATE_COOKIE), params.get(STATE).get(0)); AuthenticationResponse authResponse = AuthenticationResponseParser.parse(new URI(fullUrl), params); if (AuthHelper.isAuthenticationSuccessful(authResponse)) { @@ -122,7 +122,9 @@ private void processAuthenticationCodeRedirect(HttpServletRequest httpRequest, S Collections.singleton(authHelper.configuration.apiScope)); // validate nonce to prevent reply attacks (code maybe substituted to one with broader access) - validateNonce(stateData, getNonceClaimValueFromIdToken(result.idToken())); + validateNonce(CookieHelper.getCookie(httpRequest, CookieHelper.MSAL_WEB_APP_NONCE_COOKIE), + getNonceClaimValueFromIdToken(result.idToken())); + authHelper.setSessionPrincipal(httpRequest, result); } else { AuthenticationErrorResponse oidcResponse = (AuthenticationErrorResponse) authResponse; @@ -136,31 +138,28 @@ void sendAuthRedirect(String authoriy, HttpServletRequest httpRequest, HttpServl // state parameter to validate response from Authorization server and nonce parameter to validate idToken String state = UUID.randomUUID().toString(); String nonce = UUID.randomUUID().toString(); - storeStateInSession(httpRequest.getSession(), state, nonce); + + CookieHelper.setStateNonceCookies(httpRequest, httpResponse, state, nonce); httpResponse.setStatus(302); String redirectUrl = getRedirectUrl(authoriy, httpRequest.getParameter("claims"), state, nonce); httpResponse.sendRedirect(redirectUrl); } - private void validateNonce(StateData stateData, String nonce) throws Exception { - if (StringUtils.isEmpty(nonce) || !nonce.equals(stateData.getNonce())) { - throw new Exception(FAILED_TO_VALIDATE_MESSAGE + "could not validate nonce"); - } - } - private String getNonceClaimValueFromIdToken(String idToken) throws ParseException { return (String) JWTParser.parse(idToken).getJWTClaimsSet().getClaim("nonce"); } - private StateData validateState(HttpSession session, String state) throws Exception { - if (StringUtils.isNotEmpty(state)) { - StateData stateDataInSession = removeStateFromSession(session, state); - if (stateDataInSession != null) { - return stateDataInSession; - } + private void validateState(String cookieValue, String state) throws Exception { + if (StringUtils.isEmpty(state) || !state.equals(cookieValue)) { + throw new Exception(FAILED_TO_VALIDATE_MESSAGE + "could not validate state"); + } + } + + private void validateNonce(String cookieValue, String nonce) throws Exception { + if (StringUtils.isEmpty(nonce) || !nonce.equals(cookieValue)) { + throw new Exception(FAILED_TO_VALIDATE_MESSAGE + "could not validate nonce"); } - throw new Exception(FAILED_TO_VALIDATE_MESSAGE + "could not validate state"); } private void validateAuthRespMatchesAuthCodeFlow(AuthenticationSuccessResponse oidcResponse) throws Exception { @@ -170,41 +169,6 @@ private void validateAuthRespMatchesAuthCodeFlow(AuthenticationSuccessResponse o } } - private void storeStateInSession(HttpSession session, String state, String nonce) { - if (session.getAttribute(STATES) == null) { - session.setAttribute(STATES, new HashMap()); - } - ((Map) session.getAttribute(STATES)).put(state, new StateData(nonce, new Date())); - } - - private StateData removeStateFromSession(HttpSession session, String state) { - Map states = (Map) session.getAttribute(STATES); - if (states != null) { - eliminateExpiredStates(states); - StateData stateData = states.get(state); - if (stateData != null) { - states.remove(state); - return stateData; - } - } - return null; - } - - private void eliminateExpiredStates(Map map) { - Iterator> it = map.entrySet().iterator(); - - Date currTime = new Date(); - while (it.hasNext()) { - Map.Entry entry = it.next(); - long diffInSeconds = TimeUnit.MILLISECONDS. - toSeconds(currTime.getTime() - entry.getValue().getExpirationDate().getTime()); - - if (diffInSeconds > STATE_TTL) { - it.remove(); - } - } - } - private String getRedirectUrl(String authority, String claims, String state, String nonce) throws UnsupportedEncodingException { diff --git a/src/samples/msal-b2c-web-sample/src/main/java/com/microsoft/azure/msalwebsample/CookieHelper.java b/src/samples/msal-b2c-web-sample/src/main/java/com/microsoft/azure/msalwebsample/CookieHelper.java new file mode 100644 index 00000000..76960a95 --- /dev/null +++ b/src/samples/msal-b2c-web-sample/src/main/java/com/microsoft/azure/msalwebsample/CookieHelper.java @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.msalwebsample; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class CookieHelper { + + static final String MSAL_WEB_APP_STATE_COOKIE = "msal_web_app_auth_state"; + static final String MSAL_WEB_APP_NONCE_COOKIE = "msal_web_app_auth_nonce"; + + static void setStateNonceCookies + (HttpServletRequest httpRequest, HttpServletResponse httpResponse, String state, String nonce){ + + boolean userAgentSameSiteNoneAware = + CookieHelper.isUserAgentAwareOfSameSiteNone(httpRequest.getHeader("User-Agent")); + + String sameSiteCookieAttribute = userAgentSameSiteNoneAware ? "; SameSite=none" : ""; + + httpResponse.addHeader("Set-Cookie", + MSAL_WEB_APP_STATE_COOKIE + "=" + state + "; secure; HttpOnly" + sameSiteCookieAttribute); + + httpResponse.addHeader("Set-Cookie", + MSAL_WEB_APP_NONCE_COOKIE + "=" + nonce + "; secure; HttpOnly" + sameSiteCookieAttribute); + } + + static void removeStateNonceCookies(HttpServletResponse httpResponse){ + + Cookie stateCookie = new Cookie(MSAL_WEB_APP_STATE_COOKIE, ""); + stateCookie.setMaxAge(0); + + httpResponse.addCookie(stateCookie); + + Cookie nonceCookie = new Cookie(MSAL_WEB_APP_NONCE_COOKIE, ""); + nonceCookie.setMaxAge(0); + + httpResponse.addCookie(nonceCookie); + } + + static String getCookie(HttpServletRequest httpRequest, String cookieName){ + for(Cookie cookie : httpRequest.getCookies()){ + if(cookie.getName().equals(cookieName)){ + return cookie.getValue(); + } + } + return null; + } + + /** + * Check whether user agent support "None" value of "SameSite" attribute of cookies + * + * The following code is for demonstration only: It should not be considered complete. + * It is not maintained or supported. + * + * @param userAgent + * @return true if user agent supports "None" value of "SameSite" attribute of cookies, + * false otherwise + */ + static boolean isUserAgentAwareOfSameSiteNone(String userAgent){ + + // Cover all iOS based browsers here. This includes: + // - Safari on iOS 12 for iPhone, iPod Touch, iPad + // - WkWebview on iOS 12 for iPhone, iPod Touch, iPad + // - Chrome on iOS 12 for iPhone, iPod Touch, iPad + // All of which are broken by SameSite=None, because they use the iOS networking + // stack. + if(userAgent.contains("CPU iPhone OS 12") || userAgent.contains("iPad; CPU OS 12")){ + return false; + } + + // Cover Mac OS X based browsers that use the Mac OS networking stack. + // This includes: + // - Safari on Mac OS X. + // This does not include: + // - Chrome on Mac OS X + // Because they do not use the Mac OS networking stack. + if (userAgent.contains("Macintosh; Intel Mac OS X 10_14") && + userAgent.contains("Version/") && userAgent.contains("Safari")) { + return false; + } + + // Cover Chrome 50-69, because some versions are broken by SameSite=None, + // and none in this range require it. + // Note: this covers some pre-Chromium Edge versions, + // but pre-Chromium Edge does not require SameSite=None. + if(userAgent.contains("Chrome/5") || userAgent.contains("Chrome/6")){ + return false; + } + + return true; + } +} diff --git a/src/samples/msal-b2c-web-sample/src/main/resources/application.properties b/src/samples/msal-b2c-web-sample/src/main/resources/application.properties index 6045ba67..c275bb81 100644 --- a/src/samples/msal-b2c-web-sample/src/main/resources/application.properties +++ b/src/samples/msal-b2c-web-sample/src/main/resources/application.properties @@ -1,17 +1,29 @@ -b2c.tenant= -b2c.host= - -b2c.authority.base=https://${b2c.host}/tfp/${b2c.tenant}/ +b2c.tenant=fabrikamb2c.onmicrosoft.com​ +b2c.host=fabrikamb2c.b2clogin.com​ +​ +b2c.authority.base=https://${b2c.host}/tfp/${b2c.tenant}/​ b2c.clientId= b2c.secret= -b2c.redirectUri=http://localhost:8080/msal4jsample/secure/aad +b2c.redirectUri=https://localhost:8080/msal4jsample/secure/aad​ +​ + +b2c.api=https://fabrikamb2chello.azurewebsites.net/hello​ +b2c.api-scope=https://fabrikamb2c.onmicrosoft.com/helloapi/demo.read​ +​ +policy.sign-up-sign-in=b2c_1_susi​ +policy.edit-profile=b2c_1_edit_profile​ +​ +​ +b2c.sign-up-sign-in-authority=${b2c.authority.base}${policy.sign-up-sign-in}/​ +b2c.edit-profile-authority=${b2c.authority.base}${policy.edit-profile}/​ +b2c.reset-password-authority=${b2c.authority.base}${policy.reset-password}/​ -b2c.api= -b2c.api-scope= +server.port=8080 -policy.sign-up-sign-in=b2c_1_susi -policy.edit-profile=b2c_1_edit_profile +server.servlet.session.cookie.secure=true -b2c.sign-up-sign-in-authority=${b2c.authority.base}${policy.sign-up-sign-in}/ -b2c.edit-profile-authority=${b2c.authority.base}${policy.edit-profile}/ -b2c.reset-password-authority=${b2c.authority.base}${policy.reset-password}/ \ No newline at end of file +server.ssl.key-store=classpath:Enter_Key_Store_Here +server.ssl.key-store-password=Enter_Key_Store_Password_Here +server.ssl.key-store-type=Enter_Key_Store_Type_Here +server.ssl.key-alias=Enter_Key_Alias_Here +server.ssl.key-password=Enter_Key_Password_Here \ No newline at end of file diff --git a/src/samples/msal-web-sample/pom.xml b/src/samples/msal-web-sample/pom.xml index 947e215c..5891eca4 100644 --- a/src/samples/msal-web-sample/pom.xml +++ b/src/samples/msal-web-sample/pom.xml @@ -5,7 +5,7 @@ org.springframework.boot spring-boot-starter-parent - 2.1.4.RELEASE + 2.2.2.RELEASE com.microsoft.azure @@ -35,6 +35,12 @@ json 20090211 + + org.apache.commons + commons-lang3 + 3.9 + + org.springframework.boot @@ -54,6 +60,12 @@ spring-boot-starter-test test + + + org.springframework.session + spring-session-core + + diff --git a/src/samples/msal-web-sample/src/main/java/com/microsoft/azure/msalwebsample/AuthFilter.java b/src/samples/msal-web-sample/src/main/java/com/microsoft/azure/msalwebsample/AuthFilter.java index d2dac781..2a983a4c 100644 --- a/src/samples/msal-web-sample/src/main/java/com/microsoft/azure/msalwebsample/AuthFilter.java +++ b/src/samples/msal-web-sample/src/main/java/com/microsoft/azure/msalwebsample/AuthFilter.java @@ -5,27 +5,24 @@ import java.io.IOException; import java.io.UnsupportedEncodingException; -import java.net.MalformedURLException; import java.net.URI; import java.net.URLEncoder; import java.text.ParseException; import java.util.*; import java.util.concurrent.*; -import javax.naming.ServiceUnavailableException; import javax.servlet.Filter; import javax.servlet.FilterChain; -import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; +import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import com.microsoft.aad.msal4j.*; import com.nimbusds.jwt.JWTParser; -import com.nimbusds.oauth2.sdk.AuthorizationCode; import com.nimbusds.openid.connect.sdk.AuthenticationErrorResponse; import com.nimbusds.openid.connect.sdk.AuthenticationResponse; import com.nimbusds.openid.connect.sdk.AuthenticationResponseParser; @@ -37,10 +34,10 @@ @Component public class AuthFilter implements Filter { - private static final String STATES = "states"; private static final String STATE = "state"; - private static final Integer STATE_TTL = 3600; private static final String FAILED_TO_VALIDATE_MESSAGE = "Failed to validate data received from Authorization service - "; + public static final String MSAL_WEB_APP_STATE_COOKIE = "msal_web_app_auth_state"; + public static final String MSAL_WEB_APP_NONCE_COOKIE = "msal_web_app_auth_nonce"; private List excludedUrls = Arrays.asList("/", "/msal4jsample/"); @@ -69,6 +66,8 @@ public void doFilter(ServletRequest request, ServletResponse response, if(AuthHelper.containsAuthenticationCode(httpRequest)){ // response should have authentication code, which will be used to acquire access token processAuthenticationCodeRedirect(httpRequest, currentUri, fullUrl); + + CookieHelper.removeStateNonceCookies(httpResponse); } else { // not authenticated, redirecting to login.microsoft.com so user can authenticate sendAuthRedirect(httpRequest, httpResponse); @@ -107,7 +106,7 @@ private void processAuthenticationCodeRedirect(HttpServletRequest httpRequest, S params.put(key, Collections.singletonList(httpRequest.getParameterMap().get(key)[0])); } // validate that state in response equals to state in request - StateData stateData = validateState(httpRequest.getSession(), params.get(STATE).get(0)); + validateState(CookieHelper.getCookie(httpRequest, MSAL_WEB_APP_STATE_COOKIE), params.get(STATE).get(0)); AuthenticationResponse authResponse = AuthenticationResponseParser.parse(new URI(fullUrl), params); if (AuthHelper.isAuthenticationSuccessful(authResponse)) { @@ -121,7 +120,8 @@ private void processAuthenticationCodeRedirect(HttpServletRequest httpRequest, S currentUri); // validate nonce to prevent reply attacks (code maybe substituted to one with broader access) - validateNonce(stateData, getNonceClaimValueFromIdToken(result.idToken())); + validateNonce(CookieHelper.getCookie(httpRequest, MSAL_WEB_APP_NONCE_COOKIE), getNonceClaimValueFromIdToken(result.idToken())); + authHelper.setSessionPrincipal(httpRequest, result); } else { AuthenticationErrorResponse oidcResponse = (AuthenticationErrorResponse) authResponse; @@ -135,31 +135,29 @@ private void sendAuthRedirect(HttpServletRequest httpRequest, HttpServletRespons // state parameter to validate response from Authorization server and nonce parameter to validate idToken String state = UUID.randomUUID().toString(); String nonce = UUID.randomUUID().toString(); - storeStateInSession(httpRequest.getSession(), state, nonce); + + CookieHelper.setStateNonceCookies(httpRequest, httpResponse, state, nonce); httpResponse.setStatus(302); String redirectUrl = getRedirectUrl(httpRequest.getParameter("claims"), state, nonce); - httpResponse.sendRedirect(redirectUrl); - } - private void validateNonce(StateData stateData, String nonce) throws Exception { - if (StringUtils.isEmpty(nonce) || !nonce.equals(stateData.getNonce())) { - throw new Exception(FAILED_TO_VALIDATE_MESSAGE + "could not validate nonce"); - } + httpResponse.sendRedirect(redirectUrl); } private String getNonceClaimValueFromIdToken(String idToken) throws ParseException { return (String) JWTParser.parse(idToken).getJWTClaimsSet().getClaim("nonce"); } - private StateData validateState(HttpSession session, String state) throws Exception { - if (StringUtils.isNotEmpty(state)) { - StateData stateDataInSession = removeStateFromSession(session, state); - if (stateDataInSession != null) { - return stateDataInSession; - } + private void validateState(String cookieValue, String state) throws Exception { + if (StringUtils.isEmpty(state) || !state.equals(cookieValue)) { + throw new Exception(FAILED_TO_VALIDATE_MESSAGE + "could not validate state"); + } + } + + private void validateNonce(String cookieValue, String nonce) throws Exception { + if (StringUtils.isEmpty(nonce) || !nonce.equals(cookieValue)) { + throw new Exception(FAILED_TO_VALIDATE_MESSAGE + "could not validate nonce"); } - throw new Exception(FAILED_TO_VALIDATE_MESSAGE + "could not validate state"); } private void validateAuthRespMatchesAuthCodeFlow(AuthenticationSuccessResponse oidcResponse) throws Exception { @@ -169,41 +167,6 @@ private void validateAuthRespMatchesAuthCodeFlow(AuthenticationSuccessResponse o } } - private void storeStateInSession(HttpSession session, String state, String nonce) { - if (session.getAttribute(STATES) == null) { - session.setAttribute(STATES, new HashMap()); - } - ((Map) session.getAttribute(STATES)).put(state, new StateData(nonce, new Date())); - } - - private StateData removeStateFromSession(HttpSession session, String state) { - Map states = (Map) session.getAttribute(STATES); - if (states != null) { - eliminateExpiredStates(states); - StateData stateData = states.get(state); - if (stateData != null) { - states.remove(state); - return stateData; - } - } - return null; - } - - private void eliminateExpiredStates(Map map) { - Iterator> it = map.entrySet().iterator(); - - Date currTime = new Date(); - while (it.hasNext()) { - Map.Entry entry = it.next(); - long diffInSeconds = TimeUnit.MILLISECONDS. - toSeconds(currTime.getTime() - entry.getValue().getExpirationDate().getTime()); - - if (diffInSeconds > STATE_TTL) { - it.remove(); - } - } - } - private String getRedirectUrl(String claims, String state, String nonce) throws UnsupportedEncodingException { diff --git a/src/samples/msal-web-sample/src/main/java/com/microsoft/azure/msalwebsample/AuthPageController.java b/src/samples/msal-web-sample/src/main/java/com/microsoft/azure/msalwebsample/AuthPageController.java index 00ef6d98..3d3f4962 100644 --- a/src/samples/msal-web-sample/src/main/java/com/microsoft/azure/msalwebsample/AuthPageController.java +++ b/src/samples/msal-web-sample/src/main/java/com/microsoft/azure/msalwebsample/AuthPageController.java @@ -56,7 +56,7 @@ public void signOut(HttpServletRequest httpRequest, HttpServletResponse response httpRequest.getSession().invalidate(); - String redirectUrl = "http://localhost:8080/msal4jsample/"; + String redirectUrl = "https://localhost:8080/msal4jsample/"; response.sendRedirect(AuthHelper.END_SESSION_ENDPOINT + "?post_logout_redirect_uri=" + URLEncoder.encode(redirectUrl, "UTF-8")); } diff --git a/src/samples/msal-web-sample/src/main/java/com/microsoft/azure/msalwebsample/CookieHelper.java b/src/samples/msal-web-sample/src/main/java/com/microsoft/azure/msalwebsample/CookieHelper.java new file mode 100644 index 00000000..76960a95 --- /dev/null +++ b/src/samples/msal-web-sample/src/main/java/com/microsoft/azure/msalwebsample/CookieHelper.java @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.msalwebsample; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class CookieHelper { + + static final String MSAL_WEB_APP_STATE_COOKIE = "msal_web_app_auth_state"; + static final String MSAL_WEB_APP_NONCE_COOKIE = "msal_web_app_auth_nonce"; + + static void setStateNonceCookies + (HttpServletRequest httpRequest, HttpServletResponse httpResponse, String state, String nonce){ + + boolean userAgentSameSiteNoneAware = + CookieHelper.isUserAgentAwareOfSameSiteNone(httpRequest.getHeader("User-Agent")); + + String sameSiteCookieAttribute = userAgentSameSiteNoneAware ? "; SameSite=none" : ""; + + httpResponse.addHeader("Set-Cookie", + MSAL_WEB_APP_STATE_COOKIE + "=" + state + "; secure; HttpOnly" + sameSiteCookieAttribute); + + httpResponse.addHeader("Set-Cookie", + MSAL_WEB_APP_NONCE_COOKIE + "=" + nonce + "; secure; HttpOnly" + sameSiteCookieAttribute); + } + + static void removeStateNonceCookies(HttpServletResponse httpResponse){ + + Cookie stateCookie = new Cookie(MSAL_WEB_APP_STATE_COOKIE, ""); + stateCookie.setMaxAge(0); + + httpResponse.addCookie(stateCookie); + + Cookie nonceCookie = new Cookie(MSAL_WEB_APP_NONCE_COOKIE, ""); + nonceCookie.setMaxAge(0); + + httpResponse.addCookie(nonceCookie); + } + + static String getCookie(HttpServletRequest httpRequest, String cookieName){ + for(Cookie cookie : httpRequest.getCookies()){ + if(cookie.getName().equals(cookieName)){ + return cookie.getValue(); + } + } + return null; + } + + /** + * Check whether user agent support "None" value of "SameSite" attribute of cookies + * + * The following code is for demonstration only: It should not be considered complete. + * It is not maintained or supported. + * + * @param userAgent + * @return true if user agent supports "None" value of "SameSite" attribute of cookies, + * false otherwise + */ + static boolean isUserAgentAwareOfSameSiteNone(String userAgent){ + + // Cover all iOS based browsers here. This includes: + // - Safari on iOS 12 for iPhone, iPod Touch, iPad + // - WkWebview on iOS 12 for iPhone, iPod Touch, iPad + // - Chrome on iOS 12 for iPhone, iPod Touch, iPad + // All of which are broken by SameSite=None, because they use the iOS networking + // stack. + if(userAgent.contains("CPU iPhone OS 12") || userAgent.contains("iPad; CPU OS 12")){ + return false; + } + + // Cover Mac OS X based browsers that use the Mac OS networking stack. + // This includes: + // - Safari on Mac OS X. + // This does not include: + // - Chrome on Mac OS X + // Because they do not use the Mac OS networking stack. + if (userAgent.contains("Macintosh; Intel Mac OS X 10_14") && + userAgent.contains("Version/") && userAgent.contains("Safari")) { + return false; + } + + // Cover Chrome 50-69, because some versions are broken by SameSite=None, + // and none in this range require it. + // Note: this covers some pre-Chromium Edge versions, + // but pre-Chromium Edge does not require SameSite=None. + if(userAgent.contains("Chrome/5") || userAgent.contains("Chrome/6")){ + return false; + } + + return true; + } +} diff --git a/src/samples/msal-web-sample/src/main/resources/application.properties b/src/samples/msal-web-sample/src/main/resources/application.properties index 97fd8ed5..fa02118a 100644 --- a/src/samples/msal-web-sample/src/main/resources/application.properties +++ b/src/samples/msal-web-sample/src/main/resources/application.properties @@ -1,5 +1,15 @@ aad.authority=https://login.microsoftonline.com/common/ aad.clientId= aad.secretKey= -aad.redirectUri=http://localhost:8080/msal4jsample/secure/aad -aad.oboApi=/access_as_user \ No newline at end of file +aad.redirectUri=https://localhost:8080/msal4jsample/secure/aad +aad.oboApi=https://pesomka.onmicrosoft.com/TodoListService/access_as_user + +server.port=8080 + +server.servlet.session.cookie.secure=true + +server.ssl.key-store=classpath:keystore.p12 +server.ssl.key-store-password= +server.ssl.key-store-type=pkcs12 +server.ssl.key-alias=tomcat +server.ssl.key-password= \ No newline at end of file diff --git a/src/samples/spring-security-web-app/pom.xml b/src/samples/spring-security-web-app/pom.xml index 85a4c46e..b84ec7ce 100644 --- a/src/samples/spring-security-web-app/pom.xml +++ b/src/samples/spring-security-web-app/pom.xml @@ -5,7 +5,7 @@ org.springframework.boot spring-boot-starter-parent - 2.1.8.RELEASE + 2.2.2.RELEASE com.microsoft.azure @@ -51,6 +51,11 @@ 2.1.8.RELEASE + + org.springframework.session + spring-session-core + + diff --git a/src/samples/spring-security-web-app/src/main/java/com/microsoft/azure/springsecuritywebapp/AppConfiguration.java b/src/samples/spring-security-web-app/src/main/java/com/microsoft/azure/springsecuritywebapp/AppConfiguration.java index 1a85d1c1..abfa9592 100644 --- a/src/samples/spring-security-web-app/src/main/java/com/microsoft/azure/springsecuritywebapp/AppConfiguration.java +++ b/src/samples/spring-security-web-app/src/main/java/com/microsoft/azure/springsecuritywebapp/AppConfiguration.java @@ -3,21 +3,31 @@ package com.microsoft.azure.springsecuritywebapp; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; +import org.springframework.core.env.Environment; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import java.net.URLEncoder; + @Configuration @EnableOAuth2Sso @Order(value = 0) public class AppConfiguration extends WebSecurityConfigurerAdapter { + @Autowired + private Environment env; + @Override public void configure(HttpSecurity http) throws Exception { + String logoutUrl = env.getProperty("endSessionEndpoint") + "?post_logout_redirect_uri=" + + URLEncoder.encode(env.getProperty("homePage"), "UTF-8"); + http.antMatcher("/**") .authorizeRequests() .antMatchers("/", "/login**", "/error**") @@ -28,6 +38,6 @@ public void configure(HttpSecurity http) throws Exception { .logout() .deleteCookies() .invalidateHttpSession(true) - .logoutSuccessUrl("/"); + .logoutSuccessUrl(logoutUrl); } } diff --git a/src/samples/spring-security-web-app/src/main/resources/application.properties b/src/samples/spring-security-web-app/src/main/resources/application.properties index ddc2ac01..a5d837be 100644 --- a/src/samples/spring-security-web-app/src/main/resources/application.properties +++ b/src/samples/spring-security-web-app/src/main/resources/application.properties @@ -1,4 +1,6 @@ logging.level.org.springframework.*=DEBUG +server.address=localhost +server.port=8080 ssoServiceUrl=https://login.microsoftonline.com/common @@ -8,9 +10,20 @@ security.oauth2.client.scope=openid profile security.oauth2.client.authentication-scheme=header security.oauth2.client.client-authentication-scheme=form -security.oauth2.issuer=https://login.microsoftonline.com//v2.0 +security.oauth2.issuer=https://login.microsoftonline.com/53f49862-d94b-42dc-849b-8f132ea3674d/v2.0 security.oauth2.client.access-token-uri=${ssoServiceUrl}/oauth2/v2.0/token security.oauth2.client.user-authorization-uri=${ssoServiceUrl}/oauth2/v2.0/authorize -security.oauth2.resource.user-info-uri=https://graph.microsoft.com/oidc/userinfo \ No newline at end of file +security.oauth2.resource.user-info-uri=https://graph.microsoft.com/oidc/userinfo + +server.servlet.session.cookie.secure=true + +server.ssl.key-store=classpath:keystore.p12 +server.ssl.key-store-password= +server.ssl.key-store-type=pkcs12 +server.ssl.key-alias=tomcat +server.ssl.key-password= + +homePage=https://${server.address}:${server.port} +endSessionEndpoint=https://login.microsoftonline.com/common/oauth2/v2.0/logout \ No newline at end of file diff --git a/src/test/java/com/microsoft/aad/msal4j/DefaultHttpClientTest.java b/src/test/java/com/microsoft/aad/msal4j/DefaultHttpClientTest.java new file mode 100644 index 00000000..33b4cd6c --- /dev/null +++ b/src/test/java/com/microsoft/aad/msal4j/DefaultHttpClientTest.java @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.aad.msal4j; + +import org.apache.commons.io.IOUtils; +import org.easymock.EasyMock; +import org.powermock.api.easymock.PowerMock; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.testng.PowerMockTestCase; +import org.testng.Assert; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import javax.net.ssl.HttpsURLConnection; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.powermock.api.easymock.PowerMock.expectPrivate; + +@PrepareForTest({DefaultHttpClient.class}) +public class DefaultHttpClientTest extends PowerMockTestCase { + + @Test + public void ValidNotOkHttpResponse() throws Exception { + String TEST_URL = "https://somehost.com"; + + HttpsURLConnection mockCon = PowerMock.createMock(HttpsURLConnection.class); + + EasyMock.expect(mockCon.getResponseCode()) + .andReturn(HttpURLConnection.HTTP_INTERNAL_ERROR).times(1); + + String errorResponse = "Error Message"; + InputStream inputStream = IOUtils.toInputStream(errorResponse, "UTF-8"); + EasyMock.expect(mockCon.getErrorStream()).andReturn(inputStream).times(1); + + Map> expectedHeaders = new HashMap<>(); + expectedHeaders.put("header1", Arrays.asList("val1", "val2")); + + EasyMock.expect(mockCon.getHeaderFields()).andReturn(expectedHeaders).times(1); + + DefaultHttpClient httpClient = + PowerMock.createPartialMock(DefaultHttpClient.class, "openConnection"); + + expectPrivate(httpClient, "openConnection", EasyMock.isA(URL.class)).andReturn(mockCon); + + PowerMock.replayAll(mockCon, httpClient); + + + HttpRequest httpRequest = new HttpRequest(HttpMethod.GET, TEST_URL); + IHttpResponse response = httpClient.send(httpRequest); + + + Assert.assertEquals(response.body(), errorResponse); + Assert.assertEquals(response.headers(), expectedHeaders); + } +} From 347129f8de4a35d85255c3e4ef5c1f46fb152c88 Mon Sep 17 00:00:00 2001 From: sgonzalezMSFT Date: Fri, 31 Jan 2020 17:09:41 -0800 Subject: [PATCH 07/24] Add authorizationUrl helper. Refactor TcpListener to HttpListener --- ...AcquireTokenByInteractiveFlowSupplier.java | 117 +++----- .../aad/msal4j/AuthenticationErrorCode.java | 9 +- .../msal4j/AuthorizationCodeParameters.java | 14 +- .../aad/msal4j/AuthorizationCodeRequest.java | 14 +- .../aad/msal4j/AuthorizationRequestUrl.java | 259 ++++++++++++++++++ .../msal4j/AuthorizationResponseHandler.java | 83 ++++++ .../aad/msal4j/AuthorizationResult.java | 90 ++++++ .../microsoft/aad/msal4j/HttpListener.java | 28 ++ .../aad/msal4j/InteractiveRequest.java | 126 +++++---- .../java/com/microsoft/aad/msal4j/Prompt.java | 19 ++ .../microsoft/aad/msal4j/ResponseMode.java | 18 ++ .../com/microsoft/aad/msal4j/TcpListener.java | 7 +- 12 files changed, 644 insertions(+), 140 deletions(-) create mode 100644 src/main/java/com/microsoft/aad/msal4j/AuthorizationRequestUrl.java create mode 100644 src/main/java/com/microsoft/aad/msal4j/AuthorizationResponseHandler.java create mode 100644 src/main/java/com/microsoft/aad/msal4j/AuthorizationResult.java create mode 100644 src/main/java/com/microsoft/aad/msal4j/HttpListener.java create mode 100644 src/main/java/com/microsoft/aad/msal4j/Prompt.java create mode 100644 src/main/java/com/microsoft/aad/msal4j/ResponseMode.java diff --git a/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByInteractiveFlowSupplier.java b/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByInteractiveFlowSupplier.java index 4b0aaf97..89a07f98 100644 --- a/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByInteractiveFlowSupplier.java +++ b/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByInteractiveFlowSupplier.java @@ -6,17 +6,16 @@ import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -public class AcquireTokenByInteractiveFlowSupplier extends AuthenticationResultSupplier { - - private BlockingQueue AuthorizationCodeQueue; - private TcpListener tcpListener; +class AcquireTokenByInteractiveFlowSupplier extends AuthenticationResultSupplier { private PublicClientApplication clientApplication; private InteractiveRequest interactiveRequest; + private BlockingQueue authorizationCodeQueue; + private HttpListener httpListener; + private AuthorizationResponseHandler authorizationResponseHandler; + AcquireTokenByInteractiveFlowSupplier(PublicClientApplication clientApplication, InteractiveRequest request){ super(clientApplication, request); @@ -24,55 +23,41 @@ public class AcquireTokenByInteractiveFlowSupplier extends AuthenticationResultS this.interactiveRequest = request; } - // This where the high level logic for the flow will go + @Override AuthenticationResult execute() throws Exception{ - String authorizationCode = getAuthorizationCode(); - return acquireTokenWithAuthorizationCode(authorizationCode); + AuthorizationResult authorizationResult = getAuthorizationResult(); + return acquireTokenWithAuthorizationCode(authorizationResult); } - private String getAuthorizationCode(){ - - BlockingQueue tcpStartUpNotificationQueue = new LinkedBlockingQueue<>(); - startTcpListener(tcpStartUpNotificationQueue); + private AuthorizationResult getAuthorizationResult(){ + SystemBrowserOptions systemBrowserOptions= + interactiveRequest.interactiveRequestParameters.systemBrowserOptions(); - String authServerResponse; - try{ - - Boolean tcpListenerStarted = tcpStartUpNotificationQueue.poll( - 30, - TimeUnit.SECONDS); - if (tcpListenerStarted == null || !tcpListenerStarted){ - throw new RuntimeException("Could not start TCP listener"); - } + authorizationCodeQueue = new LinkedBlockingQueue<>(); + authorizationResponseHandler = new AuthorizationResponseHandler( + authorizationCodeQueue, + systemBrowserOptions); + startHttpListener(authorizationResponseHandler); - SystemBrowserOptions options = interactiveRequest.interactiveRequestParameters.systemBrowserOptions(); - if(options != null && options.openBrowserAction() != null){ - interactiveRequest.interactiveRequestParameters.systemBrowserOptions().openBrowserAction().openBrowser( - interactiveRequest.authorizationURI); - } else { - openDefaultSystemBrowser(interactiveRequest.authorizationURI); - } - authServerResponse = getResponseFromTcpListener(); - } catch(Exception e){ - throw new MsalClientException("Error", AuthenticationErrorCode.AUTHORIZATION_CODE_BLANK); + if (systemBrowserOptions != null && systemBrowserOptions.openBrowserAction() != null) { + interactiveRequest.interactiveRequestParameters.systemBrowserOptions().openBrowserAction() + .openBrowser(interactiveRequest.authorizationURI); + } else { + openDefaultSystemBrowser(interactiveRequest.authorizationURI); } - return parseServerResponse(authServerResponse, clientApplication.authenticationAuthority.authorityType); - } - - private void startTcpListener(BlockingQueue tcpStartUpNotifierQueue){ - AuthorizationCodeQueue = new LinkedBlockingQueue<>(); - - tcpListener = new TcpListener( - AuthorizationCodeQueue, - tcpStartUpNotifierQueue); + return getAuthorizationResultFromHttpListener(); +} + private void startHttpListener(AuthorizationResponseHandler handler){ // if port is unspecified, set to 0, which will cause socket to find a free port int port = interactiveRequest.interactiveRequestParameters.redirectUri().getPort() == -1 ? 0 : interactiveRequest.interactiveRequestParameters.redirectUri().getPort(); - tcpListener.startServer(new int[] {port}); + + httpListener = new HttpListener(); + httpListener.startListener(port, handler); } private void openDefaultSystemBrowser(URI uri){ @@ -85,54 +70,36 @@ private void openDefaultSystemBrowser(URI uri){ } } - private String getResponseFromTcpListener(){ - - String response = null; + private AuthorizationResult getAuthorizationResultFromHttpListener(){ + AuthorizationResult result = null; try { - long expirationTime = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()) + 300; + long expirationTime = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()) + 120; - while(StringHelper.isBlank(response) && !interactiveRequest.futureReference.get().isCancelled() && + while(result == null && !interactiveRequest.futureReference.get().isCancelled() && TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()) < expirationTime) { - response = AuthorizationCodeQueue.poll(100, TimeUnit.MILLISECONDS); - } - - if (StringHelper.isBlank(response)) { - throw new MsalClientException("No Authorization code was returned from the server", - AuthenticationErrorCode.AUTHORIZATION_CODE_BLANK); + result = authorizationCodeQueue.poll(100, TimeUnit.MILLISECONDS); } - } catch(Exception e){ throw new MsalClientException(e); + } finally { + if(httpListener != null){ + httpListener.stopListener(); + } } - return response; - } - - //TODO: Bring up difference in between auth code response and ask B2C team if it's possible to standardize - private String parseServerResponse(String serverResponse, AuthorityType authorityType){ - // Response will be a GET request with query parameter ?code=authCode - String regexp; - if(authorityType == AuthorityType.B2C){ - regexp = "(?<=code=)(?:(?! HTTP).)*"; - } else { - regexp = "(?<=code=)(?:(?!&).)*"; - } - - Pattern pattern = Pattern.compile(regexp); - Matcher matcher = pattern.matcher(serverResponse); - if(!matcher.find()){ - throw new MsalClientException("No authorization code in server response", - AuthenticationErrorCode.AUTHORIZATION_CODE_BLANK); + if (result == null || StringHelper.isBlank(result.code())) { + throw new MsalClientException("No Authorization code was returned from the server", + AuthenticationErrorCode.AUTHORIZATION_RESULT_BLANK); } - return matcher.group(0); + return result; } - private AuthenticationResult acquireTokenWithAuthorizationCode(String authorizationCode) + private AuthenticationResult acquireTokenWithAuthorizationCode(AuthorizationResult authorizationResult) throws Exception{ AuthorizationCodeParameters parameters = AuthorizationCodeParameters - .builder(authorizationCode, interactiveRequest.interactiveRequestParameters.redirectUri()) + .builder(authorizationResult.code(), interactiveRequest.interactiveRequestParameters.redirectUri()) .scopes(interactiveRequest.interactiveRequestParameters.scopes()) .codeVerifier(interactiveRequest.verifier()) .build(); diff --git a/src/main/java/com/microsoft/aad/msal4j/AuthenticationErrorCode.java b/src/main/java/com/microsoft/aad/msal4j/AuthenticationErrorCode.java index 33b30b79..b2adda5c 100644 --- a/src/main/java/com/microsoft/aad/msal4j/AuthenticationErrorCode.java +++ b/src/main/java/com/microsoft/aad/msal4j/AuthenticationErrorCode.java @@ -58,15 +58,20 @@ public class AuthenticationErrorCode { public final static String UNKNOWN = "unknown"; /** - * The curretn redirect URL is not a loopback URL. To use OS browser, a loopback URL must be + * The current redirect URL is not a loopback URL. To use OS browser, a loopback URL must be * configured both during app registration as well as when initializing the InteractiveRequestParameters * object */ public final static String LOOPBACK_REDIRECT_URI = "loopback_redirect_uri"; + /** + * Unable to start Http listener to the specified port. This might be because the port is unable. + */ + public final static String UNABLE_TO_START_HTTP_LISTENER = "unable_to_start_http_listener"; + public final static String PORT_BLOCKED = "port_blocked"; - public final static String AUTHORIZATION_CODE_BLANK = "authorization_code_blank"; + public final static String AUTHORIZATION_RESULT_BLANK = "authorization_code_blank"; } diff --git a/src/main/java/com/microsoft/aad/msal4j/AuthorizationCodeParameters.java b/src/main/java/com/microsoft/aad/msal4j/AuthorizationCodeParameters.java index 0ba4cff1..520b54c2 100644 --- a/src/main/java/com/microsoft/aad/msal4j/AuthorizationCodeParameters.java +++ b/src/main/java/com/microsoft/aad/msal4j/AuthorizationCodeParameters.java @@ -22,19 +22,19 @@ @AllArgsConstructor(access = AccessLevel.PRIVATE) public class AuthorizationCodeParameters { - /** - * Code verifier used for PKCE - */ - private String codeVerifier; - - private Set scopes; - @NonNull private String authorizationCode; @NonNull private URI redirectUri; + private Set scopes; + + /** + * Code verifier used for PKCE. + */ + private String codeVerifier; + private static AuthorizationCodeParametersBuilder builder() { return new AuthorizationCodeParametersBuilder(); diff --git a/src/main/java/com/microsoft/aad/msal4j/AuthorizationCodeRequest.java b/src/main/java/com/microsoft/aad/msal4j/AuthorizationCodeRequest.java index 96ea38c2..57b6183f 100644 --- a/src/main/java/com/microsoft/aad/msal4j/AuthorizationCodeRequest.java +++ b/src/main/java/com/microsoft/aad/msal4j/AuthorizationCodeRequest.java @@ -7,6 +7,7 @@ import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant; import com.nimbusds.oauth2.sdk.AuthorizationGrant; import com.nimbusds.oauth2.sdk.auth.ClientAuthentication; +import com.nimbusds.oauth2.sdk.pkce.CodeVerifier; import lombok.Builder; import java.net.URI; @@ -22,8 +23,17 @@ class AuthorizationCodeRequest extends MsalRequest { private static AbstractMsalAuthorizationGrant createMsalGrant(AuthorizationCodeParameters parameters){ - AuthorizationGrant authorizationGrant = new AuthorizationCodeGrant( - new AuthorizationCode(parameters.authorizationCode()), parameters.redirectUri()); + AuthorizationGrant authorizationGrant; + if(parameters.codeVerifier() != null){ + authorizationGrant = new AuthorizationCodeGrant( + new AuthorizationCode(parameters.authorizationCode()), + parameters.redirectUri(), + new CodeVerifier(parameters.codeVerifier())); + + } else { + authorizationGrant =new AuthorizationCodeGrant( + new AuthorizationCode(parameters.authorizationCode()),parameters.redirectUri()); + } return new OAuthAuthorizationGrant(authorizationGrant, parameters.scopes()); } diff --git a/src/main/java/com/microsoft/aad/msal4j/AuthorizationRequestUrl.java b/src/main/java/com/microsoft/aad/msal4j/AuthorizationRequestUrl.java new file mode 100644 index 00000000..509f7213 --- /dev/null +++ b/src/main/java/com/microsoft/aad/msal4j/AuthorizationRequestUrl.java @@ -0,0 +1,259 @@ +package com.microsoft.aad.msal4j; + +import com.nimbusds.oauth2.sdk.util.URLUtils; +import lombok.Getter; +import lombok.NonNull; +import lombok.experimental.Accessors; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +@Accessors(fluent = true) +@Getter +public class AuthorizationRequestUrl { + + private URL authorizationRequestUrl; + + @NonNull + private String clientId; + @NonNull + private String redirectUri; + @NonNull + private Set scopes; + private String authority; + private String codeChallenge; + private String codeChallengeMethod; + private String state; + private String nonce; + private ResponseMode responseMode; + private String loginHint; + private Prompt prompt; + private String correlationId; + + public static Builder builder(String clientId, + String redirectUri, + Set scopes) { + + ParameterValidationUtils.validateNotBlank("clientId", clientId); + ParameterValidationUtils.validateNotBlank("redirect_uri", redirectUri); + ParameterValidationUtils.validateNotEmpty("scopes", scopes); + + return builder() + .clientId(clientId) + .redirectUri(redirectUri) + .scopes(scopes); + } + + private static Builder builder() { + return new Builder(); + } + + private AuthorizationRequestUrl(Builder builder){ + Map> requestParameters = new HashMap<>(); + + //required parameters + this.clientId = builder.clientId; + requestParameters.put("client_id", Collections.singletonList(this.clientId)); + this.redirectUri = builder.redirectUri; + requestParameters.put("redirect_uri", Collections.singletonList(this.redirectUri)); + this.scopes = builder.scopes; + + String scopesParam = AbstractMsalAuthorizationGrant.COMMON_SCOPES_PARAM + + AbstractMsalAuthorizationGrant.SCOPES_DELIMITER + + String.join(" ", builder.scopes); + this.scopes = new HashSet<>(Arrays.asList(scopesParam.split(" "))); + requestParameters.put("scope", Collections.singletonList(scopesParam)); + requestParameters.put("response_type",Collections.singletonList("code")); + + // Optional parameters + if(builder.authority != null){ + this.authority = ClientApplicationBase.canonicalizeUrl(builder.authority); + } + + if(builder.b2cAuthority != null){ + this.authority = ClientApplicationBase.canonicalizeUrl(builder.authority); + } + + if(builder.claims != null){ + String claimsParam = String.join(" ", builder.claims); + requestParameters.put("claims", Collections.singletonList(claimsParam)); + } + + if(builder.codeChallenge != null){ + this.codeChallenge = builder.codeChallenge; + requestParameters.put("code_challenge", Collections.singletonList(builder.codeChallenge)); + } + + if(builder.codeChallengeMethod != null){ + this.codeChallengeMethod = builder.codeChallengeMethod; + requestParameters.put("code_challenge_method", Collections.singletonList(builder.codeChallengeMethod)); + } + + if(builder.state != null){ + this.state = builder.state; + requestParameters.put("state", Collections.singletonList(builder.state)); + } + + if(builder.nonce != null){ + this.nonce = builder.nonce; + requestParameters.put("nonce", Collections.singletonList(builder.nonce)); + } + + if(builder.responseMode != null){ + this.responseMode = builder.responseMode; + requestParameters.put("response_mode", Collections.singletonList( + builder.responseMode.toString())); + } else { + this.responseMode = ResponseMode.FORM_POST; + requestParameters.put("response_mode", Collections.singletonList( + ResponseMode.FORM_POST.toString())); + } + + if(builder.loginHint != null){ + this.loginHint = loginHint(); + requestParameters.put("login_hint", Collections.singletonList(builder.loginHint)); + } + + if(builder.prompt != null){ + this.prompt = builder.prompt; + requestParameters.put("prompt", Collections.singletonList(builder.prompt.toString())); + } + + if(builder.correlationId != null){ + this.correlationId = builder.correlationId; + requestParameters.put("correlation_id", Collections.singletonList(builder.correlationId)); + } + + try { + if(this.authority == null){ + this.authority = IClientApplicationBase.DEFAULT_AUTHORITY; + } + + String authorizationCodeEndpoint = buildAuthorizationCodeEndpoint(requestParameters); + String uriString = authorizationCodeEndpoint + "?" + + URLUtils.serializeParameters(requestParameters); + + this.authorizationRequestUrl = new URL(uriString); + } catch(MalformedURLException ex){ + throw new MsalClientException(ex); + } + } + + private String buildAuthorizationCodeEndpoint(Map requestParameters){ + AuthorityType authorityType = Authority.detectAuthorityType(this.authority); + + String authorizationCodeEndpoint = null; + if(authorityType == AuthorityType.AAD){ + authorizationCodeEndpoint = this.authority + "oauth2/v2.0/authorize"; + } else if(authorityType == AuthorityType.ADFS){ + authorizationCodeEndpoint = this.authority = "oauth2/authorize"; + } else if(authorityType == AuthorityType.B2C){ + + } + return authorizationCodeEndpoint; + } + + + public static class Builder { + + private String authority; + private String b2cAuthority; + private String clientId; + private String redirectUri; + private Set scopes; + private Set claims; + private String codeChallenge; + private String codeChallengeMethod; + private String state; + private String nonce; + private ResponseMode responseMode; + private String loginHint; + private Prompt prompt; + private String correlationId; + + public AuthorizationRequestUrl build(){ + return new AuthorizationRequestUrl(this); + } + + private Builder self() { + return this; + } + + public Builder clientId(String val){ + this.clientId = val; + return self(); + } + + public Builder redirectUri(String val){ + this.redirectUri = val; + return self(); + } + + public Builder scopes(Set val){ + this.scopes = val; + return self(); + } + + public Builder authority(String val){ + this.authority = val; + return self(); + } + + public Builder b2cAuthority(String val){ + this.b2cAuthority = val; + return self(); + } + + public Builder claims(Set val){ + this.claims = val; + return self(); + } + + public Builder codeChallenge(String val){ + this.codeChallenge = val; + return self(); + } + + public Builder codeChallengeMethod(String val){ + this.codeChallengeMethod = val; + return self(); + } + + public Builder state(String val){ + this.state = val; + return self(); + } + + public Builder nonce(String val){ + this.nonce = val; + return self(); + } + + public Builder responseMode(ResponseMode val){ + this.responseMode = val; + return self(); + } + + public Builder loginHint(String val){ + this.loginHint = val; + return self(); + } + + public Builder prompt(Prompt val){ + this.prompt = val; + return self(); + } + + public Builder correlationId(String val){ + this.correlationId = val; + return self(); + } + } +} diff --git a/src/main/java/com/microsoft/aad/msal4j/AuthorizationResponseHandler.java b/src/main/java/com/microsoft/aad/msal4j/AuthorizationResponseHandler.java new file mode 100644 index 00000000..b7cc6301 --- /dev/null +++ b/src/main/java/com/microsoft/aad/msal4j/AuthorizationResponseHandler.java @@ -0,0 +1,83 @@ +package com.microsoft.aad.msal4j; + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import lombok.Getter; +import lombok.experimental.Accessors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.util.concurrent.BlockingQueue; +import java.util.stream.Collectors; + +@Getter +@Accessors(fluent = true) +class AuthorizationResponseHandler implements HttpHandler { + + private final static Logger LOG = LoggerFactory.getLogger(TcpListener.class); + private final static String DEFAULT_SUCCESS_MESSAGE = "Authentication Complete"+ + " Authentication complete. You can close the browser and return to the application."+ + " " ; + + private final static String DEFAULT_FAILURE_MESSAGE = " " + + "Authentication Failed " + + " Authentication failed. You can return to the application. Feel free to close this browser tab. " + + "



Error details: error {0} error_description: {1} "; + + private BlockingQueue authorizationResultQueue; + private SystemBrowserOptions systemBrowserOptions; + + AuthorizationResponseHandler(BlockingQueue authorizationResultQueue, + SystemBrowserOptions systemBrowserOptions){ + this.authorizationResultQueue = authorizationResultQueue; + this.systemBrowserOptions = systemBrowserOptions; + } + + @Override + public void handle(HttpExchange httpExchange) throws IOException { + try{ + if(!httpExchange.getRequestURI().getPath().equalsIgnoreCase("/")){ + httpExchange.sendResponseHeaders(200, 0); + return; + } + String responseBody = new BufferedReader(new InputStreamReader( + httpExchange.getRequestBody())).lines().collect(Collectors.joining("\n")); + + AuthorizationResult result = AuthorizationResult.fromResponseBody(responseBody); + sendResponse(httpExchange, result); + authorizationResultQueue.put(result); + + } catch (InterruptedException ex){ + LOG.error("Error reading response from socket: " + ex.getMessage()); + throw new MsalClientException(ex); + } finally { + httpExchange.close(); + } + } + + private void sendResponse(HttpExchange httpExchange, AuthorizationResult result) + throws IOException{ + + String response; + switch (result.status()){ + case Success: + response = DEFAULT_SUCCESS_MESSAGE; + break; + case ProtocolError: + response = DEFAULT_FAILURE_MESSAGE; + break; + default: + //TODO better exception + throw new RuntimeException(); + } + + httpExchange.sendResponseHeaders(200, response.length()); + OutputStream os = httpExchange.getResponseBody(); + os.write(response.getBytes()); + os.close(); + } +} diff --git a/src/main/java/com/microsoft/aad/msal4j/AuthorizationResult.java b/src/main/java/com/microsoft/aad/msal4j/AuthorizationResult.java new file mode 100644 index 00000000..4ba53d46 --- /dev/null +++ b/src/main/java/com/microsoft/aad/msal4j/AuthorizationResult.java @@ -0,0 +1,90 @@ +package com.microsoft.aad.msal4j; + +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + +import java.net.URLDecoder; +import java.util.LinkedHashMap; +import java.util.Map; + +@Getter +@Setter +@Accessors(fluent = true) +class AuthorizationResult { + + private String code; + private String state; + private AuthorizationStatus status; + private String error; + private String errorDescription; + + enum AuthorizationStatus { + Success, + ErrorHttp, + ProtocolError, + UserCancel, + UnknownError + } + + public static AuthorizationResult fromResponseBody(String responseBody){ + + if(StringHelper.isBlank(responseBody)){ + return new AuthorizationResult( + AuthorizationStatus.UnknownError, + AuthenticationErrorCode.AUTHORIZATION_RESULT_BLANK, + "The authorization server returned an invalid response"); + } + + Map queryParameters = parseParameters(responseBody); + + if(queryParameters.containsKey("error")){ + return new AuthorizationResult( + AuthorizationStatus.ProtocolError, + queryParameters.get("error"), + !StringHelper.isBlank(queryParameters.get("error_description")) ? + queryParameters.get("error_description") : + null); + } + + AuthorizationResult result = new AuthorizationResult(); + + if(queryParameters.containsKey("code")){ + result.code = queryParameters.get("code"); + result.status = AuthorizationStatus.Success; + } + + if(queryParameters.containsKey("state")){ + result.state = queryParameters.get("state"); + } + return result; + } + + private AuthorizationResult(){ + } + private AuthorizationResult(AuthorizationStatus status, String error, String errorDescription){ + this.status = status; + this.error = error; + this.errorDescription = errorDescription; + } + + private static Map parseParameters(String serverResponse) { + Map query_pairs = new LinkedHashMap<>(); + try { + String[] pairs = serverResponse.split("&"); + for (String pair : pairs) { + int idx = pair.indexOf("="); + String key = URLDecoder.decode(pair.substring(0, idx), "UTF-8"); + String value = URLDecoder.decode(pair.substring(idx + 1), "UTF-8"); + query_pairs.put(key, value); + } + } catch(Exception ex){ + //TODO better exception + System.out.println(ex.getMessage()); + } + + return query_pairs; + } + + +} diff --git a/src/main/java/com/microsoft/aad/msal4j/HttpListener.java b/src/main/java/com/microsoft/aad/msal4j/HttpListener.java new file mode 100644 index 00000000..32b5f645 --- /dev/null +++ b/src/main/java/com/microsoft/aad/msal4j/HttpListener.java @@ -0,0 +1,28 @@ +package com.microsoft.aad.msal4j; + +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; + +import java.net.InetSocketAddress; + +class HttpListener { + + private HttpServer server; + + void startListener(int port, HttpHandler httpHandler) { + try { + server = HttpServer.create(new InetSocketAddress(port), 0); + server.createContext("/", httpHandler); + server.start(); + } catch (Exception e){ + //TODO handle exception + System.out.println(e.getMessage()); + } + } + + void stopListener(){ + if(server != null){ + server.stop(0); + } + } +} diff --git a/src/main/java/com/microsoft/aad/msal4j/InteractiveRequest.java b/src/main/java/com/microsoft/aad/msal4j/InteractiveRequest.java index 7d58c54d..45814970 100644 --- a/src/main/java/com/microsoft/aad/msal4j/InteractiveRequest.java +++ b/src/main/java/com/microsoft/aad/msal4j/InteractiveRequest.java @@ -1,20 +1,18 @@ package com.microsoft.aad.msal4j; -import com.nimbusds.oauth2.sdk.util.URLUtils; import lombok.AccessLevel; import lombok.Getter; import lombok.experimental.Accessors; import java.net.InetAddress; import java.net.URI; -import java.net.URISyntaxException; +import java.net.URL; import java.net.UnknownHostException; import java.security.SecureRandom; +import java.util.Arrays; import java.util.Base64; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.HashSet; +import java.util.Set; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicReference; @@ -26,7 +24,7 @@ class InteractiveRequest extends MsalRequest{ AtomicReference> futureReference; private PublicClientApplication publicClientApplication; - URI authorizationURI; + URL authorizationUrl; InteractiveRequestParameters interactiveRequestParameters; private String verifier; @@ -42,9 +40,8 @@ class InteractiveRequest extends MsalRequest{ this.interactiveRequestParameters = parameters; this.futureReference = futureReference; this.publicClientApplication = publicClientApplication; - this.authorizationURI = createAuthorizationURI(); + this.authorizationUrl = createAuthorizationUrl(); validateRedirectURI(parameters.redirectUri()); - } private void validateRedirectURI(URI redirectURI) { @@ -69,58 +66,85 @@ private void validateRedirectURI(URI redirectURI) { } } - private URI createAuthorizationURI() { - - URI uri; - try{ - Map> authorizationRequestParameters = createAuthorizationRequestParameters(); - String authorizationCodeEndpoint = publicClientApplication.authority() + "oauth2/v2.0/authorize"; - String uriString = authorizationCodeEndpoint + "?" + URLUtils.serializeParameters(authorizationRequestParameters); - - uri = new URI(uriString); - } catch (URISyntaxException exception) { - throw new MsalClientException(exception); + private URL createAuthorizationUrl(){ + + Set scopesParam = new HashSet<>(interactiveRequestParameters.scopes()); + String[] commonScopes = AbstractMsalAuthorizationGrant.COMMON_SCOPES_PARAM.split(" "); + scopesParam.addAll(Arrays.asList(commonScopes)); + + AuthorizationRequestUrl.Builder authorizationRequestUrlBuilder = + AuthorizationRequestUrl + .builder(publicClientApplication.clientId(), + interactiveRequestParameters.redirectUri().toString(), + scopesParam) + .correlationId(publicClientApplication.correlationId()); + + AuthorityType authorityType = publicClientApplication.authenticationAuthority.authorityType; + if(authorityType == AuthorityType.AAD || authorityType == AuthorityType.ADFS) { + authorizationRequestUrlBuilder + .authority(publicClientApplication.authority()); + } else if(authorityType == AuthorityType.B2C){ + authorizationRequestUrlBuilder + .b2cAuthority(publicClientApplication.authority()); } + addPkceAndState(authorizationRequestUrlBuilder); - return uri; - } - - private Map> createAuthorizationRequestParameters(){ - - Map> requestParameters = new HashMap<>(); - - addPkceAndState(requestParameters); - - String scopesParam = AbstractMsalAuthorizationGrant.COMMON_SCOPES_PARAM + - AbstractMsalAuthorizationGrant.SCOPES_DELIMITER + - String.join(" ", interactiveRequestParameters.scopes()); - - requestParameters.put("scope", Collections.singletonList(scopesParam)); - requestParameters.put("response_type", Collections.singletonList("code")); - requestParameters.put("response_mode", Collections.singletonList("query")); - requestParameters.put("client_id", Collections.singletonList( - publicClientApplication.clientId())); - requestParameters.put("redirect_uri", Collections.singletonList( - interactiveRequestParameters.redirectUri().toString())); - requestParameters.put("correlation_id", Collections.singletonList( - publicClientApplication.correlationId())); - - return requestParameters; + return authorizationRequestUrlBuilder.build().authorizationRequestUrl(); } - private void addPkceAndState(Map> requestParameters) { +// private URI createAuthorizationURI() { +// +// URI uri; +// try{ +// Map> authorizationRequestParameters = createAuthorizationRequestParameters(); +// String authorizationCodeEndpoint = publicClientApplication.authority() + "oauth2/v2.0/authorize"; +// String uriString = authorizationCodeEndpoint + "?" + +// URLUtils.serializeParameters(authorizationRequestParameters); +// +// uri = new URI(uriString); +// } catch (URISyntaxException exception) { +// throw new MsalClientException(exception); +// } +// +// return uri; +// } +// +// private Map> createAuthorizationRequestParameters(){ +// +// Map> requestParameters = new HashMap<>(); +// +// addPkceAndState(requestParameters); +// +// String scopesParam = AbstractMsalAuthorizationGrant.COMMON_SCOPES_PARAM + +// AbstractMsalAuthorizationGrant.SCOPES_DELIMITER + +// String.join(" ", interactiveRequestParameters.scopes()); +// +// requestParameters.put("scope", Collections.singletonList(scopesParam)); +// requestParameters.put("response_type", Collections.singletonList("code")); +// requestParameters.put("response_mode", Collections.singletonList("form_post")); +// requestParameters.put("clientId", Collections.singletonList( +// publicClientApplication.clientId())); +// requestParameters.put("redirect_uri", Collections.singletonList( +// interactiveRequestParameters.redirectUri().toString())); +// requestParameters.put("correlation_id", Collections.singletonList( +// publicClientApplication.correlationId())); +// +// return requestParameters; +// } + + private void addPkceAndState(AuthorizationRequestUrl.Builder builder) { + + // Create code verifier and code challenge as described in https://tools.ietf.org/html/rfc7636 SecureRandom secureRandom = new SecureRandom(); byte[] randomBytes = new byte[32]; secureRandom.nextBytes(randomBytes); verifier = Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes); - - requestParameters.put("code_challenge", Collections.singletonList( - StringHelper.createBase64EncodedSha256Hash(verifier))); - requestParameters.put("code_challenge_method", Collections.singletonList("S256")); - state = UUID.randomUUID().toString() + UUID.randomUUID().toString(); - requestParameters.put("state", Collections.singletonList(state)); + + builder.codeChallenge(StringHelper.createBase64EncodedSha256Hash(verifier)) + .codeChallengeMethod("256") + .state(state); } } diff --git a/src/main/java/com/microsoft/aad/msal4j/Prompt.java b/src/main/java/com/microsoft/aad/msal4j/Prompt.java new file mode 100644 index 00000000..60c02ff3 --- /dev/null +++ b/src/main/java/com/microsoft/aad/msal4j/Prompt.java @@ -0,0 +1,19 @@ +package com.microsoft.aad.msal4j; + +public enum Prompt { + LOGIN ("login"), + SELECT_ACCOUNT ("select_account"), + CONSENT ("consent"), + ADMING_CONSENT ("admin_consent"); + + private String prompt; + + Prompt(String prompt){ + this.prompt = prompt; + } + + @Override + public String toString(){ + return prompt; + } +} diff --git a/src/main/java/com/microsoft/aad/msal4j/ResponseMode.java b/src/main/java/com/microsoft/aad/msal4j/ResponseMode.java new file mode 100644 index 00000000..304adae8 --- /dev/null +++ b/src/main/java/com/microsoft/aad/msal4j/ResponseMode.java @@ -0,0 +1,18 @@ +package com.microsoft.aad.msal4j; + +public enum ResponseMode { + FORM_POST("form_post"), + QUERY("query"), + FRAGMENT("fragment"); + + private String responseMode; + + ResponseMode(String responseMode){ + this.responseMode = responseMode; + } + + @Override + public String toString(){ + return this.responseMode; + } +} diff --git a/src/main/java/com/microsoft/aad/msal4j/TcpListener.java b/src/main/java/com/microsoft/aad/msal4j/TcpListener.java index e68f9e7e..373c8ae9 100644 --- a/src/main/java/com/microsoft/aad/msal4j/TcpListener.java +++ b/src/main/java/com/microsoft/aad/msal4j/TcpListener.java @@ -15,6 +15,7 @@ public class TcpListener implements AutoCloseable{ + private final static Logger LOG = LoggerFactory.getLogger(TcpListener.class); private BlockingQueue authorizationCodeQueue; @@ -62,10 +63,10 @@ public void run(){ StringBuilder builder = new StringBuilder(); try(BufferedReader in = new BufferedReader( new InputStreamReader(clientSocket.getInputStream()))) { - String line = in.readLine(); - while(!line.equals("")){ + String line; + while((line = in.readLine()) != null){ builder.append(line); - line = in.readLine(); + //line = in.readLine(); } authorizationCodeQueue.put(builder.toString()); } catch (Exception e) { From 604415a116e13e51a3944db61aadd3313091a96f Mon Sep 17 00:00:00 2001 From: sgonzalezMSFT Date: Mon, 3 Feb 2020 16:06:40 -0800 Subject: [PATCH 08/24] Update authorities. Add authorizationCodeUrl API to ClientApplicationBase --- .../microsoft/aad/msal4j/AADAuthority.java | 22 +++--- .../microsoft/aad/msal4j/ADFSAuthority.java | 6 +- .../com/microsoft/aad/msal4j/Authority.java | 2 +- ...=> AuthorizationRequestUrlParameters.java} | 78 ++++--------------- .../microsoft/aad/msal4j/B2CAuthority.java | 13 ++-- .../aad/msal4j/ClientApplicationBase.java | 12 +++ .../aad/msal4j/InteractiveRequest.java | 6 +- .../aad/msal4j/TokenRequestExecutor.java | 2 +- 8 files changed, 54 insertions(+), 87 deletions(-) rename src/main/java/com/microsoft/aad/msal4j/{AuthorizationRequestUrl.java => AuthorizationRequestUrlParameters.java} (71%) diff --git a/src/main/java/com/microsoft/aad/msal4j/AADAuthority.java b/src/main/java/com/microsoft/aad/msal4j/AADAuthority.java index 6ef2ca7e..15e9117e 100644 --- a/src/main/java/com/microsoft/aad/msal4j/AADAuthority.java +++ b/src/main/java/com/microsoft/aad/msal4j/AADAuthority.java @@ -14,12 +14,14 @@ class AADAuthority extends Authority { private final static String TENANTLESS_TENANT_NAME = "common"; - - private final static String AADAuthorityFormat = "https://%s/%s/"; - private final static String AADtokenEndpointFormat = "https://%s/{tenant}" + TOKEN_ENDPOINT; - + private final static String AUTHORIZATION_ENDPOINT = "oauth2/v2.0/authorize"; + private final static String TOKEN_ENDPOINT = "oauth2/v2.0/token"; final static String DEVICE_CODE_ENDPOINT = "/oauth2/v2.0/devicecode"; - private final static String deviceCodeEndpointFormat = "https://%s/{tenant}" + DEVICE_CODE_ENDPOINT; + + private final static String AAD_AUTHORITY_FORMAT = "https://%s/%s/"; + private final static String AAD_AUTHORIZATION_ENDPOINT_FORMAT = AAD_AUTHORITY_FORMAT + AUTHORIZATION_ENDPOINT; + private final static String AAD_TOKEN_ENDPOINT_FORMAT = AAD_AUTHORITY_FORMAT + TOKEN_ENDPOINT; + private final static String DEVICE_CODE_ENDPOINT_FORMAT = AAD_AUTHORITY_FORMAT + DEVICE_CODE_ENDPOINT; String deviceCodeEndpoint; @@ -27,15 +29,13 @@ class AADAuthority extends Authority { super(authorityUrl); validateAuthorityUrl(); setAuthorityProperties(); - this.authority = String.format(AADAuthorityFormat, host, tenant); + this.authority = String.format(AAD_AUTHORITY_FORMAT, host, tenant); } private void setAuthorityProperties() { - this.tokenEndpoint = String.format(AADtokenEndpointFormat, host); - this.tokenEndpoint = this.tokenEndpoint.replace("{tenant}", tenant); - - this.deviceCodeEndpoint = String.format(this.deviceCodeEndpointFormat, host); - this.deviceCodeEndpoint = this.deviceCodeEndpoint.replace("{tenant}", tenant); + this.authorizationEndpoint = String.format(AAD_AUTHORIZATION_ENDPOINT_FORMAT, host, tenant); + this.tokenEndpoint = String.format(AAD_TOKEN_ENDPOINT_FORMAT, host, tenant); + this.deviceCodeEndpoint = String.format(DEVICE_CODE_ENDPOINT_FORMAT, host, tenant); this.isTenantless = TENANTLESS_TENANT_NAME.equalsIgnoreCase(tenant); this.selfSignedJwtAudience = this.tokenEndpoint; diff --git a/src/main/java/com/microsoft/aad/msal4j/ADFSAuthority.java b/src/main/java/com/microsoft/aad/msal4j/ADFSAuthority.java index f40b04c4..21f71800 100644 --- a/src/main/java/com/microsoft/aad/msal4j/ADFSAuthority.java +++ b/src/main/java/com/microsoft/aad/msal4j/ADFSAuthority.java @@ -7,13 +7,15 @@ class ADFSAuthority extends Authority{ + final static String AUTHORIZATION_ENDPOINT = "oauth2/authorize"; final static String TOKEN_ENDPOINT = "oauth2/token"; - private final static String ADFSAuthorityFormat = "https://%s/%s/"; + private final static String ADFS_AUTHORITY_FORMAT = "https://%s/%s/"; ADFSAuthority(final URL authorityUrl) { super(authorityUrl); - this.authority = String.format(ADFSAuthorityFormat, host, tenant); + this.authority = String.format(ADFS_AUTHORITY_FORMAT, host, tenant); + this.authorizationEndpoint = authority + AUTHORIZATION_ENDPOINT; this.tokenEndpoint = authority + TOKEN_ENDPOINT; this.selfSignedJwtAudience = this.tokenEndpoint; } diff --git a/src/main/java/com/microsoft/aad/msal4j/Authority.java b/src/main/java/com/microsoft/aad/msal4j/Authority.java index 0c09a395..6bc1c21d 100644 --- a/src/main/java/com/microsoft/aad/msal4j/Authority.java +++ b/src/main/java/com/microsoft/aad/msal4j/Authority.java @@ -21,7 +21,6 @@ abstract class Authority { private static final String ADFS_PATH_SEGMENT = "adfs"; private static final String B2C_PATH_SEGMENT = "tfp"; - final static String TOKEN_ENDPOINT = "/oauth2/v2.0/token"; private final static String USER_REALM_ENDPOINT = "common/userrealm"; private final static String userRealmEndpointFormat = "https://%s/" + USER_REALM_ENDPOINT + "/%s?api-version=1.0"; @@ -34,6 +33,7 @@ abstract class Authority { String tenant; boolean isTenantless; + String authorizationEndpoint; String tokenEndpoint; URL tokenEndpointUrl() throws MalformedURLException { diff --git a/src/main/java/com/microsoft/aad/msal4j/AuthorizationRequestUrl.java b/src/main/java/com/microsoft/aad/msal4j/AuthorizationRequestUrlParameters.java similarity index 71% rename from src/main/java/com/microsoft/aad/msal4j/AuthorizationRequestUrl.java rename to src/main/java/com/microsoft/aad/msal4j/AuthorizationRequestUrlParameters.java index 509f7213..af877240 100644 --- a/src/main/java/com/microsoft/aad/msal4j/AuthorizationRequestUrl.java +++ b/src/main/java/com/microsoft/aad/msal4j/AuthorizationRequestUrlParameters.java @@ -17,17 +17,12 @@ @Accessors(fluent = true) @Getter -public class AuthorizationRequestUrl { +public class AuthorizationRequestUrlParameters { - private URL authorizationRequestUrl; - - @NonNull - private String clientId; @NonNull private String redirectUri; @NonNull private Set scopes; - private String authority; private String codeChallenge; private String codeChallengeMethod; private String state; @@ -37,16 +32,15 @@ public class AuthorizationRequestUrl { private Prompt prompt; private String correlationId; - public static Builder builder(String clientId, - String redirectUri, + Map> requestParameters = new HashMap<>(); + + public static Builder builder(String redirectUri, Set scopes) { - ParameterValidationUtils.validateNotBlank("clientId", clientId); ParameterValidationUtils.validateNotBlank("redirect_uri", redirectUri); ParameterValidationUtils.validateNotEmpty("scopes", scopes); return builder() - .clientId(clientId) .redirectUri(redirectUri) .scopes(scopes); } @@ -55,12 +49,8 @@ private static Builder builder() { return new Builder(); } - private AuthorizationRequestUrl(Builder builder){ - Map> requestParameters = new HashMap<>(); - + private AuthorizationRequestUrlParameters(Builder builder){ //required parameters - this.clientId = builder.clientId; - requestParameters.put("client_id", Collections.singletonList(this.clientId)); this.redirectUri = builder.redirectUri; requestParameters.put("redirect_uri", Collections.singletonList(this.redirectUri)); this.scopes = builder.scopes; @@ -73,14 +63,6 @@ private AuthorizationRequestUrl(Builder builder){ requestParameters.put("response_type",Collections.singletonList("code")); // Optional parameters - if(builder.authority != null){ - this.authority = ClientApplicationBase.canonicalizeUrl(builder.authority); - } - - if(builder.b2cAuthority != null){ - this.authority = ClientApplicationBase.canonicalizeUrl(builder.authority); - } - if(builder.claims != null){ String claimsParam = String.join(" ", builder.claims); requestParameters.put("claims", Collections.singletonList(claimsParam)); @@ -130,42 +112,25 @@ private AuthorizationRequestUrl(Builder builder){ this.correlationId = builder.correlationId; requestParameters.put("correlation_id", Collections.singletonList(builder.correlationId)); } + } + URL createAuthorizationURL(Authority authority, + Map> requestParameters){ + URL authorizationRequestUrl; try { - if(this.authority == null){ - this.authority = IClientApplicationBase.DEFAULT_AUTHORITY; - } - - String authorizationCodeEndpoint = buildAuthorizationCodeEndpoint(requestParameters); + String authorizationCodeEndpoint = authority.authorizationEndpoint(); String uriString = authorizationCodeEndpoint + "?" + URLUtils.serializeParameters(requestParameters); - this.authorizationRequestUrl = new URL(uriString); + authorizationRequestUrl = new URL(uriString); } catch(MalformedURLException ex){ throw new MsalClientException(ex); } + return authorizationRequestUrl; } - private String buildAuthorizationCodeEndpoint(Map requestParameters){ - AuthorityType authorityType = Authority.detectAuthorityType(this.authority); - - String authorizationCodeEndpoint = null; - if(authorityType == AuthorityType.AAD){ - authorizationCodeEndpoint = this.authority + "oauth2/v2.0/authorize"; - } else if(authorityType == AuthorityType.ADFS){ - authorizationCodeEndpoint = this.authority = "oauth2/authorize"; - } else if(authorityType == AuthorityType.B2C){ - - } - return authorizationCodeEndpoint; - } - - public static class Builder { - private String authority; - private String b2cAuthority; - private String clientId; private String redirectUri; private Set scopes; private Set claims; @@ -178,19 +143,14 @@ public static class Builder { private Prompt prompt; private String correlationId; - public AuthorizationRequestUrl build(){ - return new AuthorizationRequestUrl(this); + public AuthorizationRequestUrlParameters build(){ + return new AuthorizationRequestUrlParameters(this); } private Builder self() { return this; } - public Builder clientId(String val){ - this.clientId = val; - return self(); - } - public Builder redirectUri(String val){ this.redirectUri = val; return self(); @@ -201,16 +161,6 @@ public Builder scopes(Set val){ return self(); } - public Builder authority(String val){ - this.authority = val; - return self(); - } - - public Builder b2cAuthority(String val){ - this.b2cAuthority = val; - return self(); - } - public Builder claims(Set val){ this.claims = val; return self(); diff --git a/src/main/java/com/microsoft/aad/msal4j/B2CAuthority.java b/src/main/java/com/microsoft/aad/msal4j/B2CAuthority.java index 86e51d95..0a85a76e 100644 --- a/src/main/java/com/microsoft/aad/msal4j/B2CAuthority.java +++ b/src/main/java/com/microsoft/aad/msal4j/B2CAuthority.java @@ -13,8 +13,12 @@ @Getter(AccessLevel.PACKAGE) class B2CAuthority extends Authority{ - final static String B2CTokenEndpointFormat = "https://%s/{tenant}" + TOKEN_ENDPOINT + "?p={policy}"; - String policy; + private final static String AUTHORIZATION_ENDPOINT = "/oauth2/v2.0/authorize"; + private final static String TOKEN_ENDPOINT = "/oauth2/v2.0/token"; + + private final static String B2C_AUTHORIZATION_ENDPOINT_FORMAT = "https://%s/%s/%s" + AUTHORIZATION_ENDPOINT; + private final static String B2C_TOKEN_ENDPOINT_FORMAT = "https://%s/%s" + TOKEN_ENDPOINT + "?p=%s"; + private String policy; B2CAuthority(final URL authorityUrl){ super(authorityUrl); @@ -40,9 +44,8 @@ private void setAuthorityProperties() { segments[1], segments[2]); - this.tokenEndpoint = String.format(B2CTokenEndpointFormat, host); - this.tokenEndpoint = this.tokenEndpoint.replace("{tenant}", tenant); - this.tokenEndpoint = this.tokenEndpoint.replace("{policy}", policy); + this.authorizationEndpoint = String.format(B2C_TOKEN_ENDPOINT_FORMAT, host, tenant, policy); + this.tokenEndpoint = String.format(B2C_TOKEN_ENDPOINT_FORMAT, host, tenant, policy); this.selfSignedJwtAudience = this.tokenEndpoint; } } diff --git a/src/main/java/com/microsoft/aad/msal4j/ClientApplicationBase.java b/src/main/java/com/microsoft/aad/msal4j/ClientApplicationBase.java index fedba7d9..08a0f933 100644 --- a/src/main/java/com/microsoft/aad/msal4j/ClientApplicationBase.java +++ b/src/main/java/com/microsoft/aad/msal4j/ClientApplicationBase.java @@ -159,6 +159,18 @@ public CompletableFuture removeAccount(IAccount account) { return future; } + public URL getAuthorizationRequestUrl(AuthorizationRequestUrlParameters parameters) { + + validateNotNull("parameters", parameters); + + parameters.requestParameters.put("client_id", Collections.singletonList(this.clientId)); + + return parameters.createAuthorizationURL( + this.authenticationAuthority, + parameters.requestParameters()); + } + + AuthenticationResult acquireTokenCommon(MsalRequest msalRequest, Authority requestAuthority) throws Exception { diff --git a/src/main/java/com/microsoft/aad/msal4j/InteractiveRequest.java b/src/main/java/com/microsoft/aad/msal4j/InteractiveRequest.java index 45814970..99605b9a 100644 --- a/src/main/java/com/microsoft/aad/msal4j/InteractiveRequest.java +++ b/src/main/java/com/microsoft/aad/msal4j/InteractiveRequest.java @@ -72,8 +72,8 @@ private URL createAuthorizationUrl(){ String[] commonScopes = AbstractMsalAuthorizationGrant.COMMON_SCOPES_PARAM.split(" "); scopesParam.addAll(Arrays.asList(commonScopes)); - AuthorizationRequestUrl.Builder authorizationRequestUrlBuilder = - AuthorizationRequestUrl + AuthorizationRequestUrlParameters.Builder authorizationRequestUrlBuilder = + AuthorizationRequestUrlParameters .builder(publicClientApplication.clientId(), interactiveRequestParameters.redirectUri().toString(), scopesParam) @@ -133,7 +133,7 @@ private URL createAuthorizationUrl(){ // return requestParameters; // } - private void addPkceAndState(AuthorizationRequestUrl.Builder builder) { + private void addPkceAndState(AuthorizationRequestUrlParameters.Builder builder) { // Create code verifier and code challenge as described in https://tools.ietf.org/html/rfc7636 SecureRandom secureRandom = new SecureRandom(); diff --git a/src/main/java/com/microsoft/aad/msal4j/TokenRequestExecutor.java b/src/main/java/com/microsoft/aad/msal4j/TokenRequestExecutor.java index 9bfddf1b..1fb95edd 100644 --- a/src/main/java/com/microsoft/aad/msal4j/TokenRequestExecutor.java +++ b/src/main/java/com/microsoft/aad/msal4j/TokenRequestExecutor.java @@ -94,7 +94,7 @@ private AuthenticationResult createAuthenticationResultFromOauthHttpResponse( response.getClientInfo(), requestAuthority, idToken, - authority.policy); + authority.policy()); } else { accountCacheEntity = AccountCacheEntity.create( response.getClientInfo(), From c058f4a35d16e3a04ee39e6ba96fe444614c9ff5 Mon Sep 17 00:00:00 2001 From: sgonzalezMSFT Date: Wed, 5 Feb 2020 17:40:32 -0800 Subject: [PATCH 09/24] Remove TcpListener. Update tests to use HttpListener. Add tests for AuthorizationRequestBuilder. --- .../AuthorizationCodeIT.java | 240 +++++++----------- .../microsoft/aad/msal4j/AADAuthority.java | 2 +- ...AcquireTokenByInteractiveFlowSupplier.java | 12 +- .../msal4j/AuthorizationResponseHandler.java | 2 +- .../microsoft/aad/msal4j/HttpListener.java | 8 + .../aad/msal4j/InteractiveRequest.java | 64 +---- .../aad/msal4j/OpenBrowserAction.java | 4 +- .../com/microsoft/aad/msal4j/TcpListener.java | 110 -------- ...AuthorizationRequestUrlParametersTest.java | 120 +++++++++ 9 files changed, 242 insertions(+), 320 deletions(-) delete mode 100644 src/main/java/com/microsoft/aad/msal4j/TcpListener.java create mode 100644 src/test/java/com/microsoft/aad/msal4j/AuthorizationRequestUrlParametersTest.java diff --git a/src/integrationtest/java/com.microsoft.aad.msal4j/AuthorizationCodeIT.java b/src/integrationtest/java/com.microsoft.aad.msal4j/AuthorizationCodeIT.java index 52ab0b71..6458359c 100644 --- a/src/integrationtest/java/com.microsoft.aad.msal4j/AuthorizationCodeIT.java +++ b/src/integrationtest/java/com.microsoft.aad.msal4j/AuthorizationCodeIT.java @@ -13,26 +13,21 @@ import org.testng.annotations.BeforeClass; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; -import org.testng.util.Strings; -import java.io.UnsupportedEncodingException; +import java.net.MalformedURLException; import java.net.URI; -import java.net.URLEncoder; import java.util.Collections; import java.util.Set; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; -import java.util.regex.Matcher; -import java.util.regex.Pattern; public class AuthorizationCodeIT { private final static Logger LOG = LoggerFactory.getLogger(AuthorizationCodeIT.class); private LabUserProvider labUserProvider; private WebDriver seleniumDriver; - private TcpListener tcpListener; - private BlockingQueue AuthorizationCodeQueue; + private HttpListener httpListener; @BeforeClass public void setUpLapUserProvider() { @@ -42,10 +37,7 @@ public void setUpLapUserProvider() { @AfterMethod public void cleanUp(){ seleniumDriver.quit(); - if(AuthorizationCodeQueue != null){ - AuthorizationCodeQueue.clear(); - } - tcpListener.close(); + httpListener.stopListener(); } @BeforeMethod @@ -56,7 +48,6 @@ public void startUpBrowser(){ @Test public void acquireTokenWithAuthorizationCode_ManagedUser(){ User user = labUserProvider.getDefaultUser(); - assertAcquireTokenAAD(user); } @@ -67,7 +58,6 @@ public void acquireTokenWithAuthorizationCode_ADFSv2019_Federated(){ query.parameters.put(UserQueryParameters.USER_TYPE, UserType.FEDERATED); User user = labUserProvider.getLabUser(query); - assertAcquireTokenAAD(user); } @@ -78,7 +68,6 @@ public void acquireTokenWithAuthorizationCode_ADFSv2019_OnPrem(){ query.parameters.put(UserQueryParameters.USER_TYPE, UserType.ON_PREM); User user = labUserProvider.getLabUser(query); - assertAcquireTokenADFS2019(user); } @@ -89,7 +78,6 @@ public void acquireTokenWithAuthorizationCode_ADFSv4_Federated(){ query.parameters.put(UserQueryParameters.USER_TYPE, UserType.FEDERATED); User user = labUserProvider.getLabUser(query); - assertAcquireTokenAAD(user); } @@ -100,7 +88,6 @@ public void acquireTokenWithAuthorizationCode_ADFSv3_Federated(){ query.parameters.put(UserQueryParameters.USER_TYPE, UserType.FEDERATED); User user = labUserProvider.getLabUser(query); - assertAcquireTokenAAD(user); } @@ -111,7 +98,6 @@ public void acquireTokenWithAuthorizationCode_ADFSv2_Federated(){ query.parameters.put(UserQueryParameters.USER_TYPE, UserType.FEDERATED); User user = labUserProvider.getLabUser(query); - assertAcquireTokenAAD(user); } @@ -179,10 +165,21 @@ public void acquireTokenWithAuthorizationCode_B2C_Facebook(){ } private void assertAcquireTokenADFS2019(User user){ - String authCode = acquireAuthorizationCodeAutomated(user, AuthorityType.ADFS); - IAuthenticationResult result = acquireTokenInteractive(authCode, - TestConstants.ADFS_AUTHORITY, Collections.singleton(TestConstants.ADFS_SCOPE), - TestConstants.ADFS_APP_ID); + PublicClientApplication pca; + try { + pca = PublicClientApplication.builder( + TestConstants.ADFS_APP_ID). + authority(TestConstants.ADFS_AUTHORITY). + build(); + } catch(MalformedURLException ex){ + throw new RuntimeException(ex.getMessage()); + } + + String authCode = acquireAuthorizationCodeAutomated(user, pca); + IAuthenticationResult result = acquireTokenAuthorizationCodeFlow( + pca, + authCode, + Collections.singleton(TestConstants.ADFS_SCOPE)); Assert.assertNotNull(result); Assert.assertNotNull(result.accessToken()); @@ -191,8 +188,22 @@ private void assertAcquireTokenADFS2019(User user){ } private void assertAcquireTokenAAD(User user){ - String authCode = acquireAuthorizationCodeAutomated(user, AuthorityType.AAD); - IAuthenticationResult result = acquireTokenInteractiveAAD(user, authCode); + + PublicClientApplication pca; + try { + pca = PublicClientApplication.builder( + user.getAppId()). + authority(TestConstants.ORGANIZATIONS_AUTHORITY). + build(); + } catch(MalformedURLException ex){ + throw new RuntimeException(ex.getMessage()); + } + + String authCode = acquireAuthorizationCodeAutomated(user, pca); + IAuthenticationResult result = acquireTokenAuthorizationCodeFlow( + pca, + authCode, + Collections.singleton(TestConstants.GRAPH_DEFAULT_SCOPE)); Assert.assertNotNull(result); Assert.assertNotNull(result.accessToken()); @@ -201,8 +212,21 @@ private void assertAcquireTokenAAD(User user){ } private void assertAcquireTokenB2C(User user){ - String authCode = acquireAuthorizationCodeAutomated(user, AuthorityType.B2C); - IAuthenticationResult result = acquireTokenInteractiveB2C(user, authCode); + + ConfidentialClientApplication cca; + try { + IClientCredential credential = ClientCredentialFactory.createFromSecret(""); + cca = ConfidentialClientApplication.builder( + user.getAppId(), + credential) + .b2cAuthority(TestConstants.B2C_AUTHORITY_SIGN_IN) + .build(); + } catch(Exception ex){ + throw new RuntimeException(ex.getMessage()); + } + + String authCode = acquireAuthorizationCodeAutomated(user, cca); + IAuthenticationResult result = acquireTokenInteractiveB2C(cca, authCode); Assert.assertNotNull(result); Assert.assertNotNull(result.accessToken()); @@ -210,22 +234,16 @@ private void assertAcquireTokenB2C(User user){ Assert.assertEquals(user.getUpn(), result.account().username()); } - private IAuthenticationResult acquireTokenInteractive( + private IAuthenticationResult acquireTokenAuthorizationCodeFlow( + PublicClientApplication pca, String authCode, - String authority, - Set scopes, - String clientId){ + Set scopes){ IAuthenticationResult result; try { - PublicClientApplication pca = PublicClientApplication.builder( - clientId). - authority(authority). - build(); - result = pca.acquireToken(AuthorizationCodeParameters .builder(authCode, - new URI(TestConstants.LOCALHOST + tcpListener.getPort())) + new URI(TestConstants.LOCALHOST + httpListener.port())) .scopes(scopes) .build()) .get(); @@ -237,29 +255,13 @@ private IAuthenticationResult acquireTokenInteractive( return result; } - private IAuthenticationResult acquireTokenInteractiveAAD( - User user, - String authCode){ - return acquireTokenInteractive(authCode, - TestConstants.ORGANIZATIONS_AUTHORITY, - Collections.singleton(TestConstants.GRAPH_DEFAULT_SCOPE), - user.getAppId()); - } - - private IAuthenticationResult acquireTokenInteractiveB2C(User user, + private IAuthenticationResult acquireTokenInteractiveB2C(ConfidentialClientApplication cca, String authCode) { IAuthenticationResult result; try{ - IClientCredential credential = ClientCredentialFactory.createFromSecret(""); - ConfidentialClientApplication cca = ConfidentialClientApplication.builder( - user.getAppId(), - credential) - .b2cAuthority(TestConstants.B2C_AUTHORITY_SIGN_IN) - .build(); - result = cca.acquireToken(AuthorizationCodeParameters.builder( authCode, - new URI(TestConstants.LOCALHOST + tcpListener.getPort())) + new URI(TestConstants.LOCALHOST + httpListener.port())) .scopes(Collections.singleton(TestConstants.B2C_LAB_SCOPE)) .build()) .get(); @@ -270,40 +272,50 @@ private IAuthenticationResult acquireTokenInteractiveB2C(User user, return result; } - private String acquireAuthorizationCodeAutomated( User user, - AuthorityType authorityType){ - BlockingQueue tcpStartUpNotificationQueue = new LinkedBlockingQueue<>(); - startTcpListener(tcpStartUpNotificationQueue); + ClientApplicationBase app){ - String authServerResponse; + BlockingQueue authorizationCodeQueue = new LinkedBlockingQueue<>(); + + AuthorizationResponseHandler authorizationResponseHandler = new AuthorizationResponseHandler( + authorizationCodeQueue, + new SystemBrowserOptions()); + + int[] ports = {3843, 4584, 4843, 60000}; + httpListener = new HttpListener(); + httpListener.startListener(ports[0], authorizationResponseHandler); + + AuthorizationResult result = null; try { - Boolean tcpListenerStarted = tcpStartUpNotificationQueue.poll( - 30, - TimeUnit.SECONDS); - if (tcpListenerStarted == null || !tcpListenerStarted){ - throw new RuntimeException("Could not start TCP listener"); + runSeleniumAutomatedLogin(user, app); + long expirationTime = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()) + 120; + + while(result == null && + TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()) < expirationTime) { + + result = authorizationCodeQueue.poll(100, TimeUnit.MILLISECONDS); } - runSeleniumAutomatedLogin(user, authorityType); - String page = seleniumDriver.getPageSource(); - authServerResponse = getResponseFromTcpListener(); } catch(Exception e){ - if(!Strings.isNullOrEmpty( - System.getenv(TestConstants.LOCAL_FLAG_ENV_VAR))){ - SeleniumExtensions.takeScreenShot(seleniumDriver); + throw new MsalClientException(e); + } finally { + if(httpListener != null){ + httpListener.stopListener(); } - LOG.error("Error running automated selenium login: " + e.getMessage()); - throw new RuntimeException("Error running automated selenium login: " + e.getMessage()); } - return parseServerResponse(authServerResponse,authorityType); + + if (result == null || StringHelper.isBlank(result.code())) { + throw new MsalClientException("No Authorization code was returned from the server", + AuthenticationErrorCode.AUTHORIZATION_RESULT_BLANK); + } + return result.code(); } - private void runSeleniumAutomatedLogin(User user, AuthorityType authorityType) - throws UnsupportedEncodingException{ - String url = buildAuthenticationCodeURL(user.getAppId(), authorityType); + private void runSeleniumAutomatedLogin(User user, ClientApplicationBase app) { + String url = buildAuthenticationCodeURL(app); seleniumDriver.navigate().to(url); + AuthorityType authorityType = app.authenticationAuthority.authorityType; if(authorityType == AuthorityType.B2C){ switch(user.getB2cProvider().toLowerCase()){ case B2CProvider.LOCAL: @@ -324,86 +336,28 @@ else if (authorityType == AuthorityType.ADFS) { } } - private void startTcpListener(BlockingQueue tcpStartUpNotifierQueue){ - AuthorizationCodeQueue = new LinkedBlockingQueue<>(); - tcpListener = new TcpListener(AuthorizationCodeQueue, tcpStartUpNotifierQueue); - - // these are the ports that are registered for B2C tests - int[] ports = { 3843,4584, 4843, 60000 }; - tcpListener.startServer(ports); - } - - private String getResponseFromTcpListener(){ - String response; - try { - response = AuthorizationCodeQueue.poll(30, TimeUnit.SECONDS); - if (Strings.isNullOrEmpty(response)){ - LOG.error("Server response is null"); - throw new NullPointerException("Server response is null"); - } - } catch(Exception e){ - LOG.error("Error reading from server response AuthorizationCodeQueue: " + e.getMessage()); - throw new RuntimeException("Error reading from server response AuthorizationCodeQueue: " + - e.getMessage()); - } - return response; - } - - private String parseServerResponse(String serverResponse, AuthorityType authorityType){ - // Response will be a GET request with query parameter ?code=authCode - String regexp; - if(authorityType == AuthorityType.B2C){ - regexp = "(?<=code=)(?:(?! HTTP).)*"; - } else { - regexp = "(?<=code=)(?:(?!&).)*"; - } - - Pattern pattern = Pattern.compile(regexp); - Matcher matcher = pattern.matcher(serverResponse); - - if(!matcher.find()){ - LOG.error("No authorization code in server response: " + serverResponse); - throw new IllegalStateException("No authorization code in server response: " + - serverResponse); - } - return matcher.group(0); - } - - private String buildAuthenticationCodeURL(String appId, AuthorityType authorityType) - throws UnsupportedEncodingException{ - String redirectUrl; - int portNumber = tcpListener.getPort(); - - String authority; + private String buildAuthenticationCodeURL(ClientApplicationBase app) { String scope; + + AuthorityType authorityType= app.authenticationAuthority.authorityType; if(authorityType == AuthorityType.AAD){ - authority = TestConstants.ORGANIZATIONS_AUTHORITY; scope = TestConstants.GRAPH_DEFAULT_SCOPE; } else if (authorityType == AuthorityType.B2C) { - authority = TestConstants.B2C_AUTHORITY_URL; scope = TestConstants.B2C_LAB_SCOPE; } else if (authorityType == AuthorityType.ADFS){ - authority = TestConstants.ADFS_AUTHORITY; scope = TestConstants.ADFS_SCOPE; } else{ - return null; + throw new RuntimeException("Authority type not recognized"); } - redirectUrl = authority + "oauth2/" + - (authorityType != AuthorityType.ADFS ? "v2.0/" : "") + - "authorize?" + - "response_type=code" + - "&response_mode=query" + - "&client_id=" + (authorityType == AuthorityType.ADFS ? TestConstants.ADFS_APP_ID : appId) + - "&redirect_uri=" + URLEncoder.encode(TestConstants.LOCALHOST + portNumber, "UTF-8") + - "&scope=" + URLEncoder.encode("openid offline_access profile " + scope, "UTF-8"); - - if(authorityType == AuthorityType.B2C){ - redirectUrl = redirectUrl + "&p=" + TestConstants.B2C_SIGN_IN_POLICY; - } + AuthorizationRequestUrlParameters parameters = + AuthorizationRequestUrlParameters + .builder(TestConstants.LOCALHOST + httpListener.port(), + Collections.singleton(scope)) + .build(); - return redirectUrl; + return app.getAuthorizationRequestUrl(parameters).toString(); } } diff --git a/src/main/java/com/microsoft/aad/msal4j/AADAuthority.java b/src/main/java/com/microsoft/aad/msal4j/AADAuthority.java index 15e9117e..ff0af644 100644 --- a/src/main/java/com/microsoft/aad/msal4j/AADAuthority.java +++ b/src/main/java/com/microsoft/aad/msal4j/AADAuthority.java @@ -16,7 +16,7 @@ class AADAuthority extends Authority { private final static String TENANTLESS_TENANT_NAME = "common"; private final static String AUTHORIZATION_ENDPOINT = "oauth2/v2.0/authorize"; private final static String TOKEN_ENDPOINT = "oauth2/v2.0/token"; - final static String DEVICE_CODE_ENDPOINT = "/oauth2/v2.0/devicecode"; + final static String DEVICE_CODE_ENDPOINT = "oauth2/v2.0/devicecode"; private final static String AAD_AUTHORITY_FORMAT = "https://%s/%s/"; private final static String AAD_AUTHORIZATION_ENDPOINT_FORMAT = AAD_AUTHORITY_FORMAT + AUTHORIZATION_ENDPOINT; diff --git a/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByInteractiveFlowSupplier.java b/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByInteractiveFlowSupplier.java index 89a07f98..f086b591 100644 --- a/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByInteractiveFlowSupplier.java +++ b/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByInteractiveFlowSupplier.java @@ -3,6 +3,7 @@ import java.awt.*; import java.io.IOException; import java.net.URI; +import java.net.URL; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; @@ -39,12 +40,11 @@ private AuthorizationResult getAuthorizationResult(){ systemBrowserOptions); startHttpListener(authorizationResponseHandler); - if (systemBrowserOptions != null && systemBrowserOptions.openBrowserAction() != null) { interactiveRequest.interactiveRequestParameters.systemBrowserOptions().openBrowserAction() - .openBrowser(interactiveRequest.authorizationURI); + .openBrowser(interactiveRequest.authorizationUrl()); } else { - openDefaultSystemBrowser(interactiveRequest.authorizationURI); + openDefaultSystemBrowser(interactiveRequest.authorizationUrl()); } return getAuthorizationResultFromHttpListener(); @@ -60,12 +60,12 @@ private void startHttpListener(AuthorizationResponseHandler handler){ httpListener.startListener(port, handler); } - private void openDefaultSystemBrowser(URI uri){ + private void openDefaultSystemBrowser(URL url){ try{ if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) { - Desktop.getDesktop().browse(uri); + Desktop.getDesktop().browse(url.toURI()); } - } catch(IOException e){ + } catch(Exception e){ throw new MsalClientException(e); } } diff --git a/src/main/java/com/microsoft/aad/msal4j/AuthorizationResponseHandler.java b/src/main/java/com/microsoft/aad/msal4j/AuthorizationResponseHandler.java index b7cc6301..632735ae 100644 --- a/src/main/java/com/microsoft/aad/msal4j/AuthorizationResponseHandler.java +++ b/src/main/java/com/microsoft/aad/msal4j/AuthorizationResponseHandler.java @@ -18,7 +18,7 @@ @Accessors(fluent = true) class AuthorizationResponseHandler implements HttpHandler { - private final static Logger LOG = LoggerFactory.getLogger(TcpListener.class); + private final static Logger LOG = LoggerFactory.getLogger(AuthorizationResponseHandler.class); private final static String DEFAULT_SUCCESS_MESSAGE = "Authentication Complete"+ " Authentication complete. You can close the browser and return to the application."+ " " ; diff --git a/src/main/java/com/microsoft/aad/msal4j/HttpListener.java b/src/main/java/com/microsoft/aad/msal4j/HttpListener.java index 32b5f645..4e0a464a 100644 --- a/src/main/java/com/microsoft/aad/msal4j/HttpListener.java +++ b/src/main/java/com/microsoft/aad/msal4j/HttpListener.java @@ -2,15 +2,23 @@ import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpServer; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.experimental.Accessors; import java.net.InetSocketAddress; +@Accessors(fluent = true) class HttpListener { private HttpServer server; + @Getter(AccessLevel.PACKAGE) + private int port; + void startListener(int port, HttpHandler httpHandler) { try { + this.port = port; server = HttpServer.create(new InetSocketAddress(port), 0); server.createContext("/", httpHandler); server.start(); diff --git a/src/main/java/com/microsoft/aad/msal4j/InteractiveRequest.java b/src/main/java/com/microsoft/aad/msal4j/InteractiveRequest.java index 99605b9a..69211287 100644 --- a/src/main/java/com/microsoft/aad/msal4j/InteractiveRequest.java +++ b/src/main/java/com/microsoft/aad/msal4j/InteractiveRequest.java @@ -74,64 +74,14 @@ private URL createAuthorizationUrl(){ AuthorizationRequestUrlParameters.Builder authorizationRequestUrlBuilder = AuthorizationRequestUrlParameters - .builder(publicClientApplication.clientId(), - interactiveRequestParameters.redirectUri().toString(), - scopesParam) - .correlationId(publicClientApplication.correlationId()); - - AuthorityType authorityType = publicClientApplication.authenticationAuthority.authorityType; - if(authorityType == AuthorityType.AAD || authorityType == AuthorityType.ADFS) { - authorizationRequestUrlBuilder - .authority(publicClientApplication.authority()); - } else if(authorityType == AuthorityType.B2C){ - authorizationRequestUrlBuilder - .b2cAuthority(publicClientApplication.authority()); - } - addPkceAndState(authorizationRequestUrlBuilder); - - return authorizationRequestUrlBuilder.build().authorizationRequestUrl(); - } + .builder(interactiveRequestParameters.redirectUri().toString(), scopesParam) + .correlationId(publicClientApplication.correlationId()); -// private URI createAuthorizationURI() { -// -// URI uri; -// try{ -// Map> authorizationRequestParameters = createAuthorizationRequestParameters(); -// String authorizationCodeEndpoint = publicClientApplication.authority() + "oauth2/v2.0/authorize"; -// String uriString = authorizationCodeEndpoint + "?" + -// URLUtils.serializeParameters(authorizationRequestParameters); -// -// uri = new URI(uriString); -// } catch (URISyntaxException exception) { -// throw new MsalClientException(exception); -// } -// -// return uri; -// } -// -// private Map> createAuthorizationRequestParameters(){ -// -// Map> requestParameters = new HashMap<>(); -// -// addPkceAndState(requestParameters); -// -// String scopesParam = AbstractMsalAuthorizationGrant.COMMON_SCOPES_PARAM + -// AbstractMsalAuthorizationGrant.SCOPES_DELIMITER + -// String.join(" ", interactiveRequestParameters.scopes()); -// -// requestParameters.put("scope", Collections.singletonList(scopesParam)); -// requestParameters.put("response_type", Collections.singletonList("code")); -// requestParameters.put("response_mode", Collections.singletonList("form_post")); -// requestParameters.put("clientId", Collections.singletonList( -// publicClientApplication.clientId())); -// requestParameters.put("redirect_uri", Collections.singletonList( -// interactiveRequestParameters.redirectUri().toString())); -// requestParameters.put("correlation_id", Collections.singletonList( -// publicClientApplication.correlationId())); -// -// return requestParameters; -// } + addPkceAndState(authorizationRequestUrlBuilder); + return publicClientApplication.getAuthorizationRequestUrl( + authorizationRequestUrlBuilder.build()); + } private void addPkceAndState(AuthorizationRequestUrlParameters.Builder builder) { @@ -144,7 +94,7 @@ private void addPkceAndState(AuthorizationRequestUrlParameters.Builder builder) state = UUID.randomUUID().toString() + UUID.randomUUID().toString(); builder.codeChallenge(StringHelper.createBase64EncodedSha256Hash(verifier)) - .codeChallengeMethod("256") + .codeChallengeMethod("S256") .state(state); } } diff --git a/src/main/java/com/microsoft/aad/msal4j/OpenBrowserAction.java b/src/main/java/com/microsoft/aad/msal4j/OpenBrowserAction.java index 8a65c707..240669e3 100644 --- a/src/main/java/com/microsoft/aad/msal4j/OpenBrowserAction.java +++ b/src/main/java/com/microsoft/aad/msal4j/OpenBrowserAction.java @@ -1,12 +1,12 @@ package com.microsoft.aad.msal4j; -import java.net.URI; +import java.net.URL; /** * Interface to be implemented to override system browser initialization logic. Otherwise, * PublicClientApplication defaults to using default system browser */ public interface OpenBrowserAction { - void openBrowser(URI uri); + void openBrowser(URL url); } diff --git a/src/main/java/com/microsoft/aad/msal4j/TcpListener.java b/src/main/java/com/microsoft/aad/msal4j/TcpListener.java deleted file mode 100644 index 373c8ae9..00000000 --- a/src/main/java/com/microsoft/aad/msal4j/TcpListener.java +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package com.microsoft.aad.msal4j; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.net.ServerSocket; -import java.net.Socket; -import java.util.concurrent.BlockingQueue; - -public class TcpListener implements AutoCloseable{ - - - private final static Logger LOG = LoggerFactory.getLogger(TcpListener.class); - - private BlockingQueue authorizationCodeQueue; - private BlockingQueue tcpStartUpNotificationQueue; - private int port; - private Thread serverThread; - private ServerSocket serverSocket; - - public TcpListener(BlockingQueue authorizationCodeQueue, - BlockingQueue tcpStartUpNotificationQueue){ - - this.authorizationCodeQueue = authorizationCodeQueue; - this.tcpStartUpNotificationQueue = tcpStartUpNotificationQueue; - } - - public void startServer(int[] ports){ - Runnable serverTask = () -> { - try(ServerSocket serverSocket = createSocket(ports)) { - this.serverSocket = serverSocket; - port = serverSocket.getLocalPort(); - tcpStartUpNotificationQueue.put(Boolean.TRUE); - - Socket clientSocket = serverSocket.accept(); - - new ClientTask(clientSocket).run(); - } catch (Exception e) { - LOG.error("Unable to process client request: " + e.getMessage()); - throw new RuntimeException("Unable to process client request: " + e.getMessage()); - } - }; - - serverThread = new Thread(serverTask); - serverThread.start(); - } - - private class ClientTask implements Runnable { - private final Socket clientSocket; - - private ClientTask(Socket clientSocket) { - this.clientSocket = clientSocket; - } - - @Override - public void run(){ - StringBuilder builder = new StringBuilder(); - try(BufferedReader in = new BufferedReader( - new InputStreamReader(clientSocket.getInputStream()))) { - String line; - while((line = in.readLine()) != null){ - builder.append(line); - //line = in.readLine(); - } - authorizationCodeQueue.put(builder.toString()); - } catch (Exception e) { - LOG.error("Error reading response from socket: " + e.getMessage()); - throw new RuntimeException("Error reading response from socket: " + e.getMessage()); - } finally { - try { - clientSocket.close(); - } catch (IOException e) { - LOG.error("Error closing socket: " + e.getMessage()); - } - } - } - } - - public ServerSocket createSocket(int[] ports) { - - for (int port : ports) { - try { - return new ServerSocket(port); - } catch (IOException ex) { - LOG.warn("Port: " + port + "is blocked"); - } - } - throw new MsalClientException(String.format( - "Unable to open port specified in redirect URI. Make sure port %s is not being used" + - "by another process", ports.toString()), AuthenticationErrorCode.PORT_BLOCKED); - } - - public int getPort() { - return port; - } - - public void close(){ - try { - serverSocket.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } -} diff --git a/src/test/java/com/microsoft/aad/msal4j/AuthorizationRequestUrlParametersTest.java b/src/test/java/com/microsoft/aad/msal4j/AuthorizationRequestUrlParametersTest.java new file mode 100644 index 00000000..50fc5b1c --- /dev/null +++ b/src/test/java/com/microsoft/aad/msal4j/AuthorizationRequestUrlParametersTest.java @@ -0,0 +1,120 @@ +package com.microsoft.aad.msal4j; + +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.io.UnsupportedEncodingException; +import java.net.URL; +import java.net.URLDecoder; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +public class AuthorizationRequestUrlParametersTest { + + @Test + public void testBuilder_onlyRequiredParameters() throws UnsupportedEncodingException { + PublicClientApplication app = PublicClientApplication.builder("client_id").build(); + + String redirectUri = "http://localhost:8080"; + Set scope = Collections.singleton("scope"); + + AuthorizationRequestUrlParameters parameters = + AuthorizationRequestUrlParameters + .builder(redirectUri, scope) + .build(); + + Assert.assertEquals(parameters.responseMode(), ResponseMode.FORM_POST); + Assert.assertEquals(parameters.redirectUri(), redirectUri); + Assert.assertEquals(parameters.scopes().size(), 4); + + Assert.assertNull(parameters.loginHint()); + Assert.assertNull(parameters.codeChallenge()); + Assert.assertNull(parameters.codeChallengeMethod()); + Assert.assertNull(parameters.correlationId()); + Assert.assertNull(parameters.nonce()); + Assert.assertNull(parameters.prompt()); + Assert.assertNull(parameters.state()); + + URL authorizationUrl = app.getAuthorizationRequestUrl(parameters); + + Assert.assertEquals(authorizationUrl.getHost(), "login.microsoftonline.com"); + Assert.assertEquals(authorizationUrl.getPath(), "/common/oauth2/v2.0/authorize"); + + Map queryParameters = new HashMap<>(); + String query = authorizationUrl.getQuery(); + + String[] queryPairs = query.split("&"); + for(String pair: queryPairs){ + int idx = pair.indexOf("="); + queryParameters.put( + URLDecoder.decode(pair.substring(0, idx), "UTF-8"), + URLDecoder.decode(pair.substring(idx+1), "UTF-8")); + } + + Assert.assertEquals(queryParameters.get("scope"), "openid profile offline_access scope"); + Assert.assertEquals(queryParameters.get("response_type"), "code"); + Assert.assertEquals(queryParameters.get("redirect_uri"), "http://localhost:8080"); + Assert.assertEquals(queryParameters.get("client_id"), "client_id"); + Assert.assertEquals(queryParameters.get("response_mode"), "form_post"); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testBuilder_invalidRequiredParameters(){ + String redirectUri = ""; + Set scope = Collections.singleton("scope"); + + AuthorizationRequestUrlParameters parameters = + AuthorizationRequestUrlParameters + .builder(redirectUri, scope) + .build(); + } + + @Test + public void testBuilder_optionalParameters() throws UnsupportedEncodingException{ + PublicClientApplication app = PublicClientApplication.builder("client_id").build(); + + String redirectUri = "http://localhost:8080"; + Set scope = Collections.singleton("scope"); + + AuthorizationRequestUrlParameters parameters = + AuthorizationRequestUrlParameters + .builder(redirectUri, scope) + .responseMode(ResponseMode.QUERY) + .codeChallenge("challenge") + .codeChallengeMethod("method") + .state("app_state") + .nonce("app_nonce") + .correlationId("corr_id") + .loginHint("hint") + .prompt(Prompt.SELECT_ACCOUNT) + .build(); + + URL authorizationUrl = app.getAuthorizationRequestUrl(parameters); + + Map queryParameters = new HashMap<>(); + String query = authorizationUrl.getQuery(); + + String[] queryPairs = query.split("&"); + for(String pair: queryPairs){ + int idx = pair.indexOf("="); + queryParameters.put( + URLDecoder.decode(pair.substring(0, idx), "UTF-8"), + URLDecoder.decode(pair.substring(idx+1), "UTF-8")); + } + + Assert.assertEquals(queryParameters.get("scope"), "openid profile offline_access scope"); + Assert.assertEquals(queryParameters.get("response_type"), "code"); + Assert.assertEquals(queryParameters.get("redirect_uri"), "http://localhost:8080"); + Assert.assertEquals(queryParameters.get("client_id"), "client_id"); + Assert.assertEquals(queryParameters.get("prompt"), "select_account"); + Assert.assertEquals(queryParameters.get("response_mode"), "query"); + Assert.assertEquals(queryParameters.get("code_challenge"), "challenge"); + Assert.assertEquals(queryParameters.get("code_challenge_method"), "method"); + Assert.assertEquals(queryParameters.get("state"), "app_state"); + Assert.assertEquals(queryParameters.get("nonce"), "app_nonce"); + Assert.assertEquals(queryParameters.get("correlation_id"), "corr_id"); + Assert.assertEquals(queryParameters.get("login_hint"), "hint"); + } +} From f7991b71bd99336372ab8b94c92b526c76fdc143 Mon Sep 17 00:00:00 2001 From: sgonzalezMSFT Date: Fri, 7 Feb 2020 16:29:16 -0800 Subject: [PATCH 10/24] Add ATinteractive integration tests. Add license to new files. --- .../AcquireTokenInteractiveIT.java | 4 + .../AuthorizationCodeIT.java | 145 ++++-------------- .../DeviceCodeIT.java | 4 +- .../SeleniumTest.java | 60 ++++++++ .../TestConstants.java | 4 +- .../java/labapi/LabConstants.java | 2 +- .../java/labapi/LabService.java | 4 +- .../java/labapi/LabUserProvider.java | 26 +++- ...AcquireTokenByInteractiveFlowSupplier.java | 97 ++++++++---- .../aad/msal4j/AuthenticationErrorCode.java | 4 + .../aad/msal4j/AuthorizationCodeRequest.java | 2 +- .../AuthorizationRequestUrlParameters.java | 15 +- .../msal4j/AuthorizationResponseHandler.java | 7 +- .../aad/msal4j/AuthorizationResult.java | 3 + .../microsoft/aad/msal4j/B2CAuthority.java | 2 +- .../microsoft/aad/msal4j/HttpListener.java | 7 +- .../aad/msal4j/InteractiveRequest.java | 51 +++--- .../msal4j/InteractiveRequestParameters.java | 22 +++ .../aad/msal4j/OpenBrowserAction.java | 4 +- .../java/com/microsoft/aad/msal4j/Prompt.java | 3 + .../microsoft/aad/msal4j/ResponseMode.java | 3 + .../aad/msal4j/SystemBrowserOptions.java | 3 + ...AuthorizationRequestUrlParametersTest.java | 7 +- .../aad/msal4j/DeviceCodeFlowTest.java | 2 +- 24 files changed, 287 insertions(+), 194 deletions(-) create mode 100644 src/integrationtest/java/com.microsoft.aad.msal4j/AcquireTokenInteractiveIT.java create mode 100644 src/integrationtest/java/com.microsoft.aad.msal4j/SeleniumTest.java diff --git a/src/integrationtest/java/com.microsoft.aad.msal4j/AcquireTokenInteractiveIT.java b/src/integrationtest/java/com.microsoft.aad.msal4j/AcquireTokenInteractiveIT.java new file mode 100644 index 00000000..dc489272 --- /dev/null +++ b/src/integrationtest/java/com.microsoft.aad.msal4j/AcquireTokenInteractiveIT.java @@ -0,0 +1,4 @@ +package com.microsoft.aad.msal4j; + +public class AcquireTokenInteractiveIT { +} diff --git a/src/integrationtest/java/com.microsoft.aad.msal4j/AuthorizationCodeIT.java b/src/integrationtest/java/com.microsoft.aad.msal4j/AuthorizationCodeIT.java index 6458359c..5725fbea 100644 --- a/src/integrationtest/java/com.microsoft.aad.msal4j/AuthorizationCodeIT.java +++ b/src/integrationtest/java/com.microsoft.aad.msal4j/AuthorizationCodeIT.java @@ -3,15 +3,10 @@ package com.microsoft.aad.msal4j; -import infrastructure.SeleniumExtensions; import labapi.*; -import org.openqa.selenium.WebDriver; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.testng.Assert; -import org.testng.annotations.AfterMethod; -import org.testng.annotations.BeforeClass; -import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; import java.net.MalformedURLException; @@ -22,29 +17,9 @@ import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; -public class AuthorizationCodeIT { +public class AuthorizationCodeIT extends SeleniumTest { private final static Logger LOG = LoggerFactory.getLogger(AuthorizationCodeIT.class); - private LabUserProvider labUserProvider; - private WebDriver seleniumDriver; - private HttpListener httpListener; - - @BeforeClass - public void setUpLapUserProvider() { - labUserProvider = LabUserProvider.getInstance(); - } - - @AfterMethod - public void cleanUp(){ - seleniumDriver.quit(); - httpListener.stopListener(); - } - - @BeforeMethod - public void startUpBrowser(){ - seleniumDriver = SeleniumExtensions.createDefaultWebDriver(); - } - @Test public void acquireTokenWithAuthorizationCode_ManagedUser(){ User user = labUserProvider.getDefaultUser(); @@ -52,74 +27,40 @@ public void acquireTokenWithAuthorizationCode_ManagedUser(){ } @Test - public void acquireTokenWithAuthorizationCode_ADFSv2019_Federated(){ - UserQueryParameters query = new UserQueryParameters(); - query.parameters.put(UserQueryParameters.FEDERATION_PROVIDER, FederationProvider.ADFS_2019); - query.parameters.put(UserQueryParameters.USER_TYPE, UserType.FEDERATED); - - User user = labUserProvider.getLabUser(query); - assertAcquireTokenAAD(user); + public void acquireTokenWithAuthorizationCode_ADFSv2019_OnPrem(){ + User user = labUserProvider.getOnPremAdfsUser(FederationProvider.ADFS_2019); + assertAcquireTokenADFS2019(user); } @Test - public void acquireTokenWithAuthorizationCode_ADFSv2019_OnPrem(){ - UserQueryParameters query = new UserQueryParameters(); - query.parameters.put(UserQueryParameters.FEDERATION_PROVIDER, FederationProvider.ADFS_2019); - query.parameters.put(UserQueryParameters.USER_TYPE, UserType.ON_PREM); - - User user = labUserProvider.getLabUser(query); - assertAcquireTokenADFS2019(user); + public void acquireTokenWithAuthorizationCode_ADFSv2019_Federated(){ + User user = labUserProvider.getFederatedAdfsUser(FederationProvider.ADFS_2019); + assertAcquireTokenAAD(user); } @Test public void acquireTokenWithAuthorizationCode_ADFSv4_Federated(){ - UserQueryParameters query = new UserQueryParameters(); - query.parameters.put(UserQueryParameters.FEDERATION_PROVIDER, FederationProvider.ADFS_4); - query.parameters.put(UserQueryParameters.USER_TYPE, UserType.FEDERATED); + User user = labUserProvider.getFederatedAdfsUser(FederationProvider.ADFS_4); - User user = labUserProvider.getLabUser(query); assertAcquireTokenAAD(user); } @Test public void acquireTokenWithAuthorizationCode_ADFSv3_Federated(){ - UserQueryParameters query = new UserQueryParameters(); - query.parameters.put(UserQueryParameters.FEDERATION_PROVIDER, FederationProvider.ADFS_3); - query.parameters.put(UserQueryParameters.USER_TYPE, UserType.FEDERATED); - - User user = labUserProvider.getLabUser(query); + User user = labUserProvider.getFederatedAdfsUser(FederationProvider.ADFS_3); assertAcquireTokenAAD(user); } @Test public void acquireTokenWithAuthorizationCode_ADFSv2_Federated(){ - UserQueryParameters query = new UserQueryParameters(); - query.parameters.put(UserQueryParameters.FEDERATION_PROVIDER, FederationProvider.ADFS_2); - query.parameters.put(UserQueryParameters.USER_TYPE, UserType.FEDERATED); - - User user = labUserProvider.getLabUser(query); + User user = labUserProvider.getFederatedAdfsUser(FederationProvider.ADFS_2); assertAcquireTokenAAD(user); } - //@Test + @Test // TODO Redirect URI localhost in not registered public void acquireTokenWithAuthorizationCode_B2C_Local(){ -/* User labResponse = labUserProvider.getLabUser( - B2CIdentityProvider.LOCAL, - false); - labUserProvider.getUserPassword(labResponse.getUser()); - - String b2CAppId = "b876a048-55a5-4fc5-9403-f5d90cb1c852"; - labResponse.setAppId(b2CAppId);*/ - - UserQueryParameters query = new UserQueryParameters(); - query.parameters.put(UserQueryParameters.USER_TYPE, UserType.B2C); - query.parameters.put(UserQueryParameters.B2C_PROVIDER, B2CProvider.LOCAL); - User user = labUserProvider.getLabUser(query); - - /*String b2CAppId = "b876a048-55a5-4fc5-9403-f5d90cb1c852"; - labResponse.setAppId(b2CAppId); -*/ + User user = labUserProvider.getB2cUser(B2CProvider.LOCAL); assertAcquireTokenB2C(user); } @@ -134,13 +75,7 @@ public void acquireTokenWithAuthorizationCode_B2C_Google(){ String b2CAppId = "b876a048-55a5-4fc5-9403-f5d90cb1c852"; labResponse.setAppId(b2CAppId);*/ - - UserQueryParameters query = new UserQueryParameters(); - query.parameters.put(UserQueryParameters.USER_TYPE, UserType.B2C); - query.parameters.put(UserQueryParameters.B2C_PROVIDER, B2CProvider.GOOGLE); - - User user = labUserProvider.getLabUser(query); - + User user = labUserProvider.getB2cUser(B2CProvider.GOOGLE); assertAcquireTokenB2C(user); } @@ -155,11 +90,7 @@ public void acquireTokenWithAuthorizationCode_B2C_Facebook(){ String b2CAppId = "b876a048-55a5-4fc5-9403-f5d90cb1c852"; labResponse.setAppId(b2CAppId);*/ - UserQueryParameters query = new UserQueryParameters(); - query.parameters.put(UserQueryParameters.USER_TYPE, UserType.B2C); - query.parameters.put(UserQueryParameters.B2C_PROVIDER, B2CProvider.FACEBOOK); - - User user = labUserProvider.getLabUser(query); + User user = labUserProvider.getB2cUser(B2CProvider.FACEBOOK); assertAcquireTokenB2C(user); } @@ -213,12 +144,14 @@ private void assertAcquireTokenAAD(User user){ private void assertAcquireTokenB2C(User user){ + String appId = LabService.getSecret(TestConstants.B2C_LAB_APP_ID); + String appSecret = LabService.getSecret(TestConstants.B2C_LAB_APP_SECRET); + ConfidentialClientApplication cca; try { - IClientCredential credential = ClientCredentialFactory.createFromSecret(""); - cca = ConfidentialClientApplication.builder( - user.getAppId(), - credential) + IClientCredential credential = ClientCredentialFactory.createFromSecret(appSecret); + cca = ConfidentialClientApplication + .builder(appId, credential) .b2cAuthority(TestConstants.B2C_AUTHORITY_SIGN_IN) .build(); } catch(Exception ex){ @@ -231,7 +164,6 @@ private void assertAcquireTokenB2C(User user){ Assert.assertNotNull(result); Assert.assertNotNull(result.accessToken()); Assert.assertNotNull(result.idToken()); - Assert.assertEquals(user.getUpn(), result.account().username()); } private IAuthenticationResult acquireTokenAuthorizationCodeFlow( @@ -259,9 +191,8 @@ private IAuthenticationResult acquireTokenInteractiveB2C(ConfidentialClientAppli String authCode) { IAuthenticationResult result; try{ - result = cca.acquireToken(AuthorizationCodeParameters.builder( - authCode, - new URI(TestConstants.LOCALHOST + httpListener.port())) + result = cca.acquireToken(AuthorizationCodeParameters + .builder(authCode, new URI(TestConstants.LOCALHOST + httpListener.port())) .scopes(Collections.singleton(TestConstants.B2C_LAB_SCOPE)) .build()) .get(); @@ -282,13 +213,15 @@ private String acquireAuthorizationCodeAutomated( authorizationCodeQueue, new SystemBrowserOptions()); - int[] ports = {3843, 4584, 4843, 60000}; httpListener = new HttpListener(); - httpListener.startListener(ports[0], authorizationResponseHandler); + httpListener.startListener(8080, authorizationResponseHandler); AuthorizationResult result = null; try { + String url = buildAuthenticationCodeURL(app); + seleniumDriver.navigate().to(url); runSeleniumAutomatedLogin(user, app); + long expirationTime = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()) + 120; while(result == null && @@ -310,32 +243,6 @@ private String acquireAuthorizationCodeAutomated( } return result.code(); } - - private void runSeleniumAutomatedLogin(User user, ClientApplicationBase app) { - String url = buildAuthenticationCodeURL(app); - seleniumDriver.navigate().to(url); - - AuthorityType authorityType = app.authenticationAuthority.authorityType; - if(authorityType == AuthorityType.B2C){ - switch(user.getB2cProvider().toLowerCase()){ - case B2CProvider.LOCAL: - SeleniumExtensions.performLocalLogin(seleniumDriver, user); - break; - case B2CProvider.GOOGLE: - SeleniumExtensions.performGoogleLogin(seleniumDriver, user); - break; - case B2CProvider.FACEBOOK: - SeleniumExtensions.performFacebookLogin(seleniumDriver, user); - break; - } - } else if (authorityType == AuthorityType.AAD) { - SeleniumExtensions.performADLogin(seleniumDriver, user); - } - else if (authorityType == AuthorityType.ADFS) { - SeleniumExtensions.performADFS2019Login(seleniumDriver, user); - } - } - private String buildAuthenticationCodeURL(ClientApplicationBase app) { String scope; diff --git a/src/integrationtest/java/com.microsoft.aad.msal4j/DeviceCodeIT.java b/src/integrationtest/java/com.microsoft.aad.msal4j/DeviceCodeIT.java index 10938086..dfe85c8c 100644 --- a/src/integrationtest/java/com.microsoft.aad.msal4j/DeviceCodeIT.java +++ b/src/integrationtest/java/com.microsoft.aad.msal4j/DeviceCodeIT.java @@ -56,8 +56,8 @@ public void DeviceCodeFlowTest() throws Exception { } private void runAutomatedDeviceCodeFlow(DeviceCode deviceCode, User user){ - boolean isRunningLocally = true; /*!Strings.isNullOrEmpty( - System.getenv(TestConstants.LOCAL_FLAG_ENV_VAR));*/ + boolean isRunningLocally = !Strings.isNullOrEmpty( + System.getenv(TestConstants.LOCAL_FLAG_ENV_VAR)); LOG.info("Device code running locally: " + isRunningLocally); try{ diff --git a/src/integrationtest/java/com.microsoft.aad.msal4j/SeleniumTest.java b/src/integrationtest/java/com.microsoft.aad.msal4j/SeleniumTest.java new file mode 100644 index 00000000..84634a41 --- /dev/null +++ b/src/integrationtest/java/com.microsoft.aad.msal4j/SeleniumTest.java @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.aad.msal4j; + +import infrastructure.SeleniumExtensions; +import labapi.B2CProvider; +import labapi.LabUserProvider; +import labapi.User; +import org.openqa.selenium.WebDriver; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; + +abstract class SeleniumTest { + + protected LabUserProvider labUserProvider; + WebDriver seleniumDriver; + HttpListener httpListener; + + @BeforeClass + public void setUpLapUserProvider() { + labUserProvider = LabUserProvider.getInstance(); + } + + @AfterMethod + public void cleanUp(){ + seleniumDriver.quit(); + if(httpListener != null) { + httpListener.stopListener(); + } + } + + @BeforeMethod + public void startUpBrowser(){ + seleniumDriver = SeleniumExtensions.createDefaultWebDriver(); + } + + void runSeleniumAutomatedLogin(User user, ClientApplicationBase app) { + AuthorityType authorityType = app.authenticationAuthority.authorityType; + if(authorityType == AuthorityType.B2C){ + switch(user.getB2cProvider().toLowerCase()){ + case B2CProvider.LOCAL: + SeleniumExtensions.performLocalLogin(seleniumDriver, user); + break; + case B2CProvider.GOOGLE: + SeleniumExtensions.performGoogleLogin(seleniumDriver, user); + break; + case B2CProvider.FACEBOOK: + SeleniumExtensions.performFacebookLogin(seleniumDriver, user); + break; + } + } else if (authorityType == AuthorityType.AAD) { + SeleniumExtensions.performADLogin(seleniumDriver, user); + } + else if (authorityType == AuthorityType.ADFS) { + SeleniumExtensions.performADFS2019Login(seleniumDriver, user); + } + } +} diff --git a/src/integrationtest/java/com.microsoft.aad.msal4j/TestConstants.java b/src/integrationtest/java/com.microsoft.aad.msal4j/TestConstants.java index 0fc75643..7649d5f6 100644 --- a/src/integrationtest/java/com.microsoft.aad.msal4j/TestConstants.java +++ b/src/integrationtest/java/com.microsoft.aad.msal4j/TestConstants.java @@ -3,14 +3,14 @@ package com.microsoft.aad.msal4j; -import java.util.stream.Stream; - public class TestConstants { public final static String KEYVAULT_DEFAULT_SCOPE = "https://vault.azure.net/.default"; public final static String MSIDLAB_DEFAULT_SCOPE = "https://msidlab.com/.default"; public final static String GRAPH_DEFAULT_SCOPE = "https://graph.windows.net/.default"; public final static String USER_READ_SCOPE = "user.read"; public final static String B2C_LAB_SCOPE = "https://msidlabb2c.onmicrosoft.com/msaapp/user_impersonation"; + public final static String B2C_LAB_APP_SECRET = "MSIDLABB2C-MSAapp-AppSecret"; + public final static String B2C_LAB_APP_ID = "MSIDLABB2C-MSAapp-AppID"; public final static String MICROSOFT_AUTHORITY_HOST = "https://login.microsoftonline.com/"; public final static String ORGANIZATIONS_AUTHORITY = MICROSOFT_AUTHORITY_HOST + "organizations/"; diff --git a/src/integrationtest/java/labapi/LabConstants.java b/src/integrationtest/java/labapi/LabConstants.java index f9a87334..a1d2839b 100644 --- a/src/integrationtest/java/labapi/LabConstants.java +++ b/src/integrationtest/java/labapi/LabConstants.java @@ -5,7 +5,7 @@ public class LabConstants { public final static String LAB_USER_ENDPOINT = "https://msidlab.com/api/user"; - public final static String LAB_USER_SECRET_ENDPOINT = "https://msidlab.com/api/LabUserSecret"; + public final static String LAB_USER_SECRET_ENDPOINT = "https://msidlab.com/api/LabSecret"; public final static String LAB_APP_ENDPOINT = "https://msidlab.com/api/App"; public final static String LAB_LAB_ENDPOINT = "https://msidlab.com/api/Lab"; diff --git a/src/integrationtest/java/labapi/LabService.java b/src/integrationtest/java/labapi/LabService.java index 57898afd..6c74b716 100644 --- a/src/integrationtest/java/labapi/LabService.java +++ b/src/integrationtest/java/labapi/LabService.java @@ -59,7 +59,7 @@ User getUser(UserQueryParameters query){ User[] users = convertJsonToObject(result, User[].class); User user = users[0]; - user.setPassword(getUserSecret(user.getLabName())); + user.setPassword(getSecret(user.getLabName())); if (query.parameters.containsKey(UserQueryParameters.FEDERATION_PROVIDER)) { user.setFederationProvider(query.parameters.get(UserQueryParameters.FEDERATION_PROVIDER)); } else { @@ -94,7 +94,7 @@ public static Lab getLab(String labId) { } } - private String getUserSecret(String labName){ + public static String getSecret(String labName){ String result; try { Map queryMap = new HashMap<>(); diff --git a/src/integrationtest/java/labapi/LabUserProvider.java b/src/integrationtest/java/labapi/LabUserProvider.java index 02e82b80..e1c9b90e 100644 --- a/src/integrationtest/java/labapi/LabUserProvider.java +++ b/src/integrationtest/java/labapi/LabUserProvider.java @@ -39,19 +39,35 @@ public User getDefaultUser() { return getLabUser(query); } - public User getUserByAzureEnvironment(String azureEnvironment) { + public User getFederatedAdfsUser(String federationProvider){ UserQueryParameters query = new UserQueryParameters(); - query.parameters.put(UserQueryParameters.AZURE_ENVIRONMENT, azureEnvironment); + query.parameters.put(UserQueryParameters.FEDERATION_PROVIDER, federationProvider); + query.parameters.put(UserQueryParameters.USER_TYPE, UserType.FEDERATED); return getLabUser(query); } - public User getFederatedAdfsUser(String federationProvider){ + public User getOnPremAdfsUser(String federationProvider){ + UserQueryParameters query = new UserQueryParameters(); + query.parameters.put(UserQueryParameters.FEDERATION_PROVIDER, federationProvider); + query.parameters.put(UserQueryParameters.USER_TYPE, UserType.ON_PREM); + + return getLabUser(query); + } + public User getB2cUser(String b2cProvider) { UserQueryParameters query = new UserQueryParameters(); - query.parameters.put(UserQueryParameters.FEDERATION_PROVIDER, federationProvider); - query.parameters.put(UserQueryParameters.USER_TYPE, UserType.FEDERATED); + query.parameters.put(UserQueryParameters.USER_TYPE, UserType.B2C); + query.parameters.put(UserQueryParameters.B2C_PROVIDER, b2cProvider); + + return getLabUser(query); + } + + public User getUserByAzureEnvironment(String azureEnvironment) { + + UserQueryParameters query = new UserQueryParameters(); + query.parameters.put(UserQueryParameters.AZURE_ENVIRONMENT, azureEnvironment); return getLabUser(query); } diff --git a/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByInteractiveFlowSupplier.java b/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByInteractiveFlowSupplier.java index f086b591..891c8137 100644 --- a/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByInteractiveFlowSupplier.java +++ b/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByInteractiveFlowSupplier.java @@ -1,8 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + package com.microsoft.aad.msal4j; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.awt.*; -import java.io.IOException; import java.net.URI; +import java.net.URISyntaxException; import java.net.URL; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; @@ -10,12 +16,13 @@ class AcquireTokenByInteractiveFlowSupplier extends AuthenticationResultSupplier { + private final static Logger LOG = LoggerFactory.getLogger(AcquireTokenByAuthorizationGrantSupplier.class); + private PublicClientApplication clientApplication; private InteractiveRequest interactiveRequest; private BlockingQueue authorizationCodeQueue; private HttpListener httpListener; - private AuthorizationResponseHandler authorizationResponseHandler; AcquireTokenByInteractiveFlowSupplier(PublicClientApplication clientApplication, InteractiveRequest request){ @@ -27,37 +34,77 @@ class AcquireTokenByInteractiveFlowSupplier extends AuthenticationResultSupplier @Override AuthenticationResult execute() throws Exception{ AuthorizationResult authorizationResult = getAuthorizationResult(); + validateState(authorizationResult); return acquireTokenWithAuthorizationCode(authorizationResult); } private AuthorizationResult getAuthorizationResult(){ - SystemBrowserOptions systemBrowserOptions= - interactiveRequest.interactiveRequestParameters.systemBrowserOptions(); - - authorizationCodeQueue = new LinkedBlockingQueue<>(); - authorizationResponseHandler = new AuthorizationResponseHandler( - authorizationCodeQueue, - systemBrowserOptions); - startHttpListener(authorizationResponseHandler); - - if (systemBrowserOptions != null && systemBrowserOptions.openBrowserAction() != null) { - interactiveRequest.interactiveRequestParameters.systemBrowserOptions().openBrowserAction() - .openBrowser(interactiveRequest.authorizationUrl()); - } else { - openDefaultSystemBrowser(interactiveRequest.authorizationUrl()); + + AuthorizationResult result; + try { + SystemBrowserOptions systemBrowserOptions = + interactiveRequest.interactiveRequestParameters().systemBrowserOptions(); + + authorizationCodeQueue = new LinkedBlockingQueue<>(); + AuthorizationResponseHandler authorizationResponseHandler = + new AuthorizationResponseHandler( + authorizationCodeQueue, + systemBrowserOptions); + + startHttpListener(authorizationResponseHandler); + + if (systemBrowserOptions != null && systemBrowserOptions.openBrowserAction() != null) { + interactiveRequest.interactiveRequestParameters().systemBrowserOptions().openBrowserAction() + .openBrowser(interactiveRequest.authorizationUrl()); + } else { + openDefaultSystemBrowser(interactiveRequest.authorizationUrl()); + } + + result = getAuthorizationResultFromHttpListener(); + } finally { + if(httpListener != null){ + httpListener.stopListener(); + } } - return getAuthorizationResultFromHttpListener(); -} + return result; + } + + private void validateState(AuthorizationResult authorizationResult){ + if(StringHelper.isBlank(authorizationResult.state()) || + !authorizationResult.state().equals(interactiveRequest.state())){ + + throw new MsalClientException("State returned in authorization result is blank or does " + + "not match state sent on outgoing request", + AuthenticationErrorCode.INVALID_AUTHORIZATION_RESULT_STATE); + } + } private void startHttpListener(AuthorizationResponseHandler handler){ // if port is unspecified, set to 0, which will cause socket to find a free port - int port = interactiveRequest.interactiveRequestParameters.redirectUri().getPort() == -1 ? + int port = interactiveRequest.interactiveRequestParameters().redirectUri().getPort() == -1 ? 0 : - interactiveRequest.interactiveRequestParameters.redirectUri().getPort(); + interactiveRequest.interactiveRequestParameters().redirectUri().getPort(); httpListener = new HttpListener(); httpListener.startListener(port, handler); + + //If no port is passed, http listener finds a free one. We should update redirect URL to + // point to this port. + if(port != httpListener.port()){ + updateRedirectUrl(); + } + } + + private void updateRedirectUrl(){ + try { + URI updatedRedirectUrl = new URI("http://localhost:" + httpListener.port()); + interactiveRequest.interactiveRequestParameters().redirectUri(updatedRedirectUrl); + LOG.debug("Redirect URI updated to" + updatedRedirectUrl); + } catch (URISyntaxException ex){ + throw new MsalClientException("Error updating redirect URI. Not a valid URI format", + AuthenticationErrorCode.INVALID_REDIRECT_URI); + } } private void openDefaultSystemBrowser(URL url){ @@ -75,17 +122,13 @@ private AuthorizationResult getAuthorizationResultFromHttpListener(){ try { long expirationTime = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()) + 120; - while(result == null && !interactiveRequest.futureReference.get().isCancelled() && + while(result == null && !interactiveRequest.futureReference().get().isCancelled() && TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()) < expirationTime) { result = authorizationCodeQueue.poll(100, TimeUnit.MILLISECONDS); } } catch(Exception e){ throw new MsalClientException(e); - } finally { - if(httpListener != null){ - httpListener.stopListener(); - } } if (result == null || StringHelper.isBlank(result.code())) { @@ -99,8 +142,8 @@ private AuthenticationResult acquireTokenWithAuthorizationCode(AuthorizationResu throws Exception{ AuthorizationCodeParameters parameters = AuthorizationCodeParameters - .builder(authorizationResult.code(), interactiveRequest.interactiveRequestParameters.redirectUri()) - .scopes(interactiveRequest.interactiveRequestParameters.scopes()) + .builder(authorizationResult.code(), interactiveRequest.interactiveRequestParameters().redirectUri()) + .scopes(interactiveRequest.interactiveRequestParameters().scopes()) .codeVerifier(interactiveRequest.verifier()) .build(); diff --git a/src/main/java/com/microsoft/aad/msal4j/AuthenticationErrorCode.java b/src/main/java/com/microsoft/aad/msal4j/AuthenticationErrorCode.java index b2adda5c..679d1cd3 100644 --- a/src/main/java/com/microsoft/aad/msal4j/AuthenticationErrorCode.java +++ b/src/main/java/com/microsoft/aad/msal4j/AuthenticationErrorCode.java @@ -73,5 +73,9 @@ public class AuthenticationErrorCode { public final static String PORT_BLOCKED = "port_blocked"; public final static String AUTHORIZATION_RESULT_BLANK = "authorization_code_blank"; + + public final static String INVALID_AUTHORIZATION_RESULT_STATE = "invalid_authorization_result_state"; + + public final static String INVALID_REDIRECT_URI = "incalid_redirect_uri"; } diff --git a/src/main/java/com/microsoft/aad/msal4j/AuthorizationCodeRequest.java b/src/main/java/com/microsoft/aad/msal4j/AuthorizationCodeRequest.java index 57b6183f..70e60f34 100644 --- a/src/main/java/com/microsoft/aad/msal4j/AuthorizationCodeRequest.java +++ b/src/main/java/com/microsoft/aad/msal4j/AuthorizationCodeRequest.java @@ -31,7 +31,7 @@ private static AbstractMsalAuthorizationGrant createMsalGrant(AuthorizationCodeP new CodeVerifier(parameters.codeVerifier())); } else { - authorizationGrant =new AuthorizationCodeGrant( + authorizationGrant = new AuthorizationCodeGrant( new AuthorizationCode(parameters.authorizationCode()),parameters.redirectUri()); } diff --git a/src/main/java/com/microsoft/aad/msal4j/AuthorizationRequestUrlParameters.java b/src/main/java/com/microsoft/aad/msal4j/AuthorizationRequestUrlParameters.java index af877240..77b5a39c 100644 --- a/src/main/java/com/microsoft/aad/msal4j/AuthorizationRequestUrlParameters.java +++ b/src/main/java/com/microsoft/aad/msal4j/AuthorizationRequestUrlParameters.java @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + package com.microsoft.aad.msal4j; import com.nimbusds.oauth2.sdk.util.URLUtils; @@ -14,6 +17,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.TreeSet; @Accessors(fluent = true) @Getter @@ -55,11 +59,12 @@ private AuthorizationRequestUrlParameters(Builder builder){ requestParameters.put("redirect_uri", Collections.singletonList(this.redirectUri)); this.scopes = builder.scopes; - String scopesParam = AbstractMsalAuthorizationGrant.COMMON_SCOPES_PARAM + - AbstractMsalAuthorizationGrant.SCOPES_DELIMITER + - String.join(" ", builder.scopes); - this.scopes = new HashSet<>(Arrays.asList(scopesParam.split(" "))); - requestParameters.put("scope", Collections.singletonList(scopesParam)); + Set scopesParam = new TreeSet<>(builder.scopes); + String[] commonScopes = AbstractMsalAuthorizationGrant.COMMON_SCOPES_PARAM.split(" "); + scopesParam.addAll(Arrays.asList(commonScopes)); + + this.scopes = scopesParam; + requestParameters.put("scope", Collections.singletonList(String.join(" ", scopesParam))); requestParameters.put("response_type",Collections.singletonList("code")); // Optional parameters diff --git a/src/main/java/com/microsoft/aad/msal4j/AuthorizationResponseHandler.java b/src/main/java/com/microsoft/aad/msal4j/AuthorizationResponseHandler.java index 632735ae..e278d9a7 100644 --- a/src/main/java/com/microsoft/aad/msal4j/AuthorizationResponseHandler.java +++ b/src/main/java/com/microsoft/aad/msal4j/AuthorizationResponseHandler.java @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + package com.microsoft.aad.msal4j; import com.sun.net.httpserver.HttpExchange; @@ -48,8 +51,8 @@ public void handle(HttpExchange httpExchange) throws IOException { httpExchange.getRequestBody())).lines().collect(Collectors.joining("\n")); AuthorizationResult result = AuthorizationResult.fromResponseBody(responseBody); - sendResponse(httpExchange, result); authorizationResultQueue.put(result); + sendResponse(httpExchange, result); } catch (InterruptedException ex){ LOG.error("Error reading response from socket: " + ex.getMessage()); @@ -72,7 +75,7 @@ private void sendResponse(HttpExchange httpExchange, AuthorizationResult result) break; default: //TODO better exception - throw new RuntimeException(); + throw new MsalClientException("Received unknown result from authorization response", "error"); } httpExchange.sendResponseHeaders(200, response.length()); diff --git a/src/main/java/com/microsoft/aad/msal4j/AuthorizationResult.java b/src/main/java/com/microsoft/aad/msal4j/AuthorizationResult.java index 4ba53d46..946dfa86 100644 --- a/src/main/java/com/microsoft/aad/msal4j/AuthorizationResult.java +++ b/src/main/java/com/microsoft/aad/msal4j/AuthorizationResult.java @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + package com.microsoft.aad.msal4j; import lombok.Getter; diff --git a/src/main/java/com/microsoft/aad/msal4j/B2CAuthority.java b/src/main/java/com/microsoft/aad/msal4j/B2CAuthority.java index 0a85a76e..6c93092b 100644 --- a/src/main/java/com/microsoft/aad/msal4j/B2CAuthority.java +++ b/src/main/java/com/microsoft/aad/msal4j/B2CAuthority.java @@ -44,7 +44,7 @@ private void setAuthorityProperties() { segments[1], segments[2]); - this.authorizationEndpoint = String.format(B2C_TOKEN_ENDPOINT_FORMAT, host, tenant, policy); + this.authorizationEndpoint = String.format(B2C_AUTHORIZATION_ENDPOINT_FORMAT, host, tenant, policy); this.tokenEndpoint = String.format(B2C_TOKEN_ENDPOINT_FORMAT, host, tenant, policy); this.selfSignedJwtAudience = this.tokenEndpoint; } diff --git a/src/main/java/com/microsoft/aad/msal4j/HttpListener.java b/src/main/java/com/microsoft/aad/msal4j/HttpListener.java index 4e0a464a..b44d7907 100644 --- a/src/main/java/com/microsoft/aad/msal4j/HttpListener.java +++ b/src/main/java/com/microsoft/aad/msal4j/HttpListener.java @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + package com.microsoft.aad.msal4j; import com.sun.net.httpserver.HttpHandler; @@ -18,13 +21,15 @@ class HttpListener { void startListener(int port, HttpHandler httpHandler) { try { - this.port = port; server = HttpServer.create(new InetSocketAddress(port), 0); server.createContext("/", httpHandler); + this.port = server.getAddress().getPort(); server.start(); + } catch (Exception e){ //TODO handle exception System.out.println(e.getMessage()); + throw new MsalClientException(e); } } diff --git a/src/main/java/com/microsoft/aad/msal4j/InteractiveRequest.java b/src/main/java/com/microsoft/aad/msal4j/InteractiveRequest.java index 69211287..93c5d0ee 100644 --- a/src/main/java/com/microsoft/aad/msal4j/InteractiveRequest.java +++ b/src/main/java/com/microsoft/aad/msal4j/InteractiveRequest.java @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + package com.microsoft.aad.msal4j; import lombok.AccessLevel; @@ -7,29 +10,30 @@ import java.net.InetAddress; import java.net.URI; import java.net.URL; -import java.net.UnknownHostException; import java.security.SecureRandom; -import java.util.Arrays; import java.util.Base64; -import java.util.HashSet; -import java.util.Set; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicReference; -@Getter(AccessLevel.PACKAGE) @Accessors(fluent = true) class InteractiveRequest extends MsalRequest{ - AtomicReference> futureReference; - private PublicClientApplication publicClientApplication; + @Getter(AccessLevel.PACKAGE) + private AtomicReference> futureReference; - URL authorizationUrl; - InteractiveRequestParameters interactiveRequestParameters; + @Getter(AccessLevel.PACKAGE) + private InteractiveRequestParameters interactiveRequestParameters; + @Getter(AccessLevel.PACKAGE) private String verifier; + + @Getter(AccessLevel.PACKAGE) private String state; + private PublicClientApplication publicClientApplication; + private URL authorizationUrl; + InteractiveRequest(InteractiveRequestParameters parameters, AtomicReference> futureReference, PublicClientApplication publicClientApplication, @@ -40,41 +44,44 @@ class InteractiveRequest extends MsalRequest{ this.interactiveRequestParameters = parameters; this.futureReference = futureReference; this.publicClientApplication = publicClientApplication; - this.authorizationUrl = createAuthorizationUrl(); - validateRedirectURI(parameters.redirectUri()); + validateRedirectUrl(parameters.redirectUri()); } - private void validateRedirectURI(URI redirectURI) { + URL authorizationUrl(){ + if(this.authorizationUrl == null) { + authorizationUrl = createAuthorizationUrl(); + } + return authorizationUrl; + } + + private void validateRedirectUrl(URI redirectUri) { try { - if (!InetAddress.getByName(redirectURI.getHost()).isLoopbackAddress()) { + if (!InetAddress.getByName(redirectUri.getHost()).isLoopbackAddress()) { throw new MsalClientException(String.format( "Only loopback redirect uri is supported, but %s was found " + "Configure http://localhost or http://localhost:port both during app registration" + - "and when you create the create the InteractiveRequestParameters object", redirectURI.getHost()), + "and when you create the create the InteractiveRequestParameters object", redirectUri.getHost()), AuthenticationErrorCode.LOOPBACK_REDIRECT_URI); } - if (!redirectURI.getScheme().equals("http")) { + if (!redirectUri.getScheme().equals("http")) { throw new MsalClientException(String.format( "Only http uri scheme is supported but %s was found. Configure http://localhost" + "or http://localhost:port both during app registration and when you create" + - " the create the InteractiveRequestParameters object", redirectURI.toString()), + " the create the InteractiveRequestParameters object", redirectUri.toString()), AuthenticationErrorCode.LOOPBACK_REDIRECT_URI); } - } catch (UnknownHostException exception){ + } catch (Exception exception){ throw new MsalClientException(exception); } } private URL createAuthorizationUrl(){ - Set scopesParam = new HashSet<>(interactiveRequestParameters.scopes()); - String[] commonScopes = AbstractMsalAuthorizationGrant.COMMON_SCOPES_PARAM.split(" "); - scopesParam.addAll(Arrays.asList(commonScopes)); - AuthorizationRequestUrlParameters.Builder authorizationRequestUrlBuilder = AuthorizationRequestUrlParameters - .builder(interactiveRequestParameters.redirectUri().toString(), scopesParam) + .builder(interactiveRequestParameters.redirectUri().toString(), + interactiveRequestParameters.scopes()) .correlationId(publicClientApplication.correlationId()); diff --git a/src/main/java/com/microsoft/aad/msal4j/InteractiveRequestParameters.java b/src/main/java/com/microsoft/aad/msal4j/InteractiveRequestParameters.java index 837a0d97..88e23640 100644 --- a/src/main/java/com/microsoft/aad/msal4j/InteractiveRequestParameters.java +++ b/src/main/java/com/microsoft/aad/msal4j/InteractiveRequestParameters.java @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + package com.microsoft.aad.msal4j; import lombok.AccessLevel; @@ -5,11 +8,17 @@ import lombok.Builder; import lombok.Getter; import lombok.NonNull; +import lombok.Setter; import lombok.experimental.Accessors; import java.net.URI; +import java.net.URL; import java.util.Set; +import static com.microsoft.aad.msal4j.ParameterValidationUtils.validateNotBlank; +import static com.microsoft.aad.msal4j.ParameterValidationUtils.validateNotEmpty; +import static com.microsoft.aad.msal4j.ParameterValidationUtils.validateNotNull; + /** * Object containing parameters for interactive requests. Can be used as parameter to * {@link PublicClientApplication#acquireToken(InteractiveRequestParameters)} @@ -23,6 +32,7 @@ public class InteractiveRequestParameters { @NonNull private Set scopes; + @Setter(AccessLevel.PACKAGE) @NonNull private URI redirectUri; @@ -33,5 +43,17 @@ public class InteractiveRequestParameters { */ private SystemBrowserOptions systemBrowserOptions; + private static InteractiveRequestParametersBuilder builder() { + return new InteractiveRequestParametersBuilder(); + } + + public static InteractiveRequestParametersBuilder builder(Set scopes, URI redirectUri) { + + validateNotEmpty("scopes", scopes); + validateNotNull("redirect_uri", redirectUri); + return builder() + .scopes(scopes) + .redirectUri(redirectUri); + } } diff --git a/src/main/java/com/microsoft/aad/msal4j/OpenBrowserAction.java b/src/main/java/com/microsoft/aad/msal4j/OpenBrowserAction.java index 240669e3..ac1fdff2 100644 --- a/src/main/java/com/microsoft/aad/msal4j/OpenBrowserAction.java +++ b/src/main/java/com/microsoft/aad/msal4j/OpenBrowserAction.java @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + package com.microsoft.aad.msal4j; import java.net.URL; @@ -9,4 +12,3 @@ public interface OpenBrowserAction { void openBrowser(URL url); } - diff --git a/src/main/java/com/microsoft/aad/msal4j/Prompt.java b/src/main/java/com/microsoft/aad/msal4j/Prompt.java index 60c02ff3..fd154ea3 100644 --- a/src/main/java/com/microsoft/aad/msal4j/Prompt.java +++ b/src/main/java/com/microsoft/aad/msal4j/Prompt.java @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + package com.microsoft.aad.msal4j; public enum Prompt { diff --git a/src/main/java/com/microsoft/aad/msal4j/ResponseMode.java b/src/main/java/com/microsoft/aad/msal4j/ResponseMode.java index 304adae8..1523594f 100644 --- a/src/main/java/com/microsoft/aad/msal4j/ResponseMode.java +++ b/src/main/java/com/microsoft/aad/msal4j/ResponseMode.java @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + package com.microsoft.aad.msal4j; public enum ResponseMode { diff --git a/src/main/java/com/microsoft/aad/msal4j/SystemBrowserOptions.java b/src/main/java/com/microsoft/aad/msal4j/SystemBrowserOptions.java index 495ef9af..f98a493e 100644 --- a/src/main/java/com/microsoft/aad/msal4j/SystemBrowserOptions.java +++ b/src/main/java/com/microsoft/aad/msal4j/SystemBrowserOptions.java @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + package com.microsoft.aad.msal4j; import lombok.Getter; diff --git a/src/test/java/com/microsoft/aad/msal4j/AuthorizationRequestUrlParametersTest.java b/src/test/java/com/microsoft/aad/msal4j/AuthorizationRequestUrlParametersTest.java index 50fc5b1c..d6b44474 100644 --- a/src/test/java/com/microsoft/aad/msal4j/AuthorizationRequestUrlParametersTest.java +++ b/src/test/java/com/microsoft/aad/msal4j/AuthorizationRequestUrlParametersTest.java @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + package com.microsoft.aad.msal4j; import org.testng.Assert; @@ -53,7 +56,7 @@ public void testBuilder_onlyRequiredParameters() throws UnsupportedEncodingExcep URLDecoder.decode(pair.substring(idx+1), "UTF-8")); } - Assert.assertEquals(queryParameters.get("scope"), "openid profile offline_access scope"); + Assert.assertEquals(queryParameters.get("scope"), "offline_access openid profile scope"); Assert.assertEquals(queryParameters.get("response_type"), "code"); Assert.assertEquals(queryParameters.get("redirect_uri"), "http://localhost:8080"); Assert.assertEquals(queryParameters.get("client_id"), "client_id"); @@ -104,7 +107,7 @@ public void testBuilder_optionalParameters() throws UnsupportedEncodingException URLDecoder.decode(pair.substring(idx+1), "UTF-8")); } - Assert.assertEquals(queryParameters.get("scope"), "openid profile offline_access scope"); + Assert.assertEquals(queryParameters.get("scope"), "offline_access openid profile scope"); Assert.assertEquals(queryParameters.get("response_type"), "code"); Assert.assertEquals(queryParameters.get("redirect_uri"), "http://localhost:8080"); Assert.assertEquals(queryParameters.get("client_id"), "client_id"); diff --git a/src/test/java/com/microsoft/aad/msal4j/DeviceCodeFlowTest.java b/src/test/java/com/microsoft/aad/msal4j/DeviceCodeFlowTest.java index 77f5855d..df35f318 100644 --- a/src/test/java/com/microsoft/aad/msal4j/DeviceCodeFlowTest.java +++ b/src/test/java/com/microsoft/aad/msal4j/DeviceCodeFlowTest.java @@ -132,7 +132,7 @@ public void deviceCodeFlowTest() throws Exception { URL url = capturedHttpRequest.getValue().url(); Assert.assertEquals(url.getAuthority(), AAD_PREFERRED_NETWORK_ENV_ALIAS); Assert.assertEquals(url.getPath(), - "/" + AAD_TENANT_NAME + AADAuthority.DEVICE_CODE_ENDPOINT); + "/" + AAD_TENANT_NAME + "/" + AADAuthority.DEVICE_CODE_ENDPOINT); Map expectedQueryParams = new HashMap<>(); expectedQueryParams.put("client_id", AAD_CLIENT_ID); From 30524d9e3d9ba6b08a86b09d59c4105d0ee6324c Mon Sep 17 00:00:00 2001 From: sgonzalezMSFT Date: Mon, 10 Feb 2020 16:48:11 -0800 Subject: [PATCH 11/24] Add acquireTokenInteractive IT. Add javadoc to public APIs --- .../AcquireTokenInteractiveIT.java | 140 +++++++++++++++++- .../AuthorizationCodeIT.java | 2 +- ...AcquireTokenByInteractiveFlowSupplier.java | 4 +- .../aad/msal4j/AuthenticationErrorCode.java | 16 +- .../AuthorizationRequestUrlParameters.java | 68 ++++++++- .../msal4j/AuthorizationResponseHandler.java | 50 ++++++- .../aad/msal4j/AuthorizationResult.java | 32 ++-- .../aad/msal4j/ClientApplicationBase.java | 1 + .../aad/msal4j/IClientApplicationBase.java | 14 ++ .../aad/msal4j/IPublicClientApplication.java | 10 +- .../aad/msal4j/InteractiveRequest.java | 4 +- .../msal4j/InteractiveRequestParameters.java | 34 ++++- .../aad/msal4j/OpenBrowserAction.java | 8 + .../java/com/microsoft/aad/msal4j/Prompt.java | 21 +++ .../microsoft/aad/msal4j/ResponseMode.java | 19 +++ .../microsoft/aad/msal4j/StringHelper.java | 2 - .../aad/msal4j/SystemBrowserOptions.java | 28 ++++ ...AuthorizationRequestUrlParametersTest.java | 2 + 18 files changed, 413 insertions(+), 42 deletions(-) diff --git a/src/integrationtest/java/com.microsoft.aad.msal4j/AcquireTokenInteractiveIT.java b/src/integrationtest/java/com.microsoft.aad.msal4j/AcquireTokenInteractiveIT.java index dc489272..98181849 100644 --- a/src/integrationtest/java/com.microsoft.aad.msal4j/AcquireTokenInteractiveIT.java +++ b/src/integrationtest/java/com.microsoft.aad.msal4j/AcquireTokenInteractiveIT.java @@ -1,4 +1,142 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + package com.microsoft.aad.msal4j; -public class AcquireTokenInteractiveIT { +import labapi.FederationProvider; +import labapi.User; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; +import java.util.Collections; + +public class AcquireTokenInteractiveIT extends SeleniumTest { + + private final static Logger LOG = LoggerFactory.getLogger(AuthorizationCodeIT.class); + + @Test + public void acquireTokenInteractive_ManagedUser(){ + User user = labUserProvider.getDefaultUser(); + assertAcquireTokenAAD(user); + } + + @Test + public void acquireTokenInteractive_ADFSv2019_OnPrem(){ + User user = labUserProvider.getOnPremAdfsUser(FederationProvider.ADFS_2019); + assertAcquireTokenADFS2019(user); + } + + @Test + public void acquireTokenInteractive_ADFSv2019_Federated(){ + User user = labUserProvider.getFederatedAdfsUser(FederationProvider.ADFS_2019); + assertAcquireTokenAAD(user); + } + + @Test + public void acquireTokenInteractive_ADFSv4_Federated(){ + User user = labUserProvider.getFederatedAdfsUser(FederationProvider.ADFS_4); + assertAcquireTokenAAD(user); + } + + @Test + public void acquireTokenInteractive_ADFSv3_Federated(){ + User user = labUserProvider.getFederatedAdfsUser(FederationProvider.ADFS_3); + assertAcquireTokenAAD(user); + } + + @Test + public void acquireTokenInteractive_ADFSv2_Federated(){ + User user = labUserProvider.getFederatedAdfsUser(FederationProvider.ADFS_2); + assertAcquireTokenAAD(user); + } + + private void assertAcquireTokenAAD(User user){ + + PublicClientApplication pca; + try { + pca = PublicClientApplication.builder( + user.getAppId()). + authority(TestConstants.ORGANIZATIONS_AUTHORITY). + build(); + } catch(MalformedURLException ex){ + throw new RuntimeException(ex.getMessage()); + } + + IAuthenticationResult result = acquireTokenInteractive( + user, + pca, + TestConstants.GRAPH_DEFAULT_SCOPE); + + Assert.assertNotNull(result); + Assert.assertNotNull(result.accessToken()); + Assert.assertNotNull(result.idToken()); + Assert.assertEquals(user.getUpn(), result.account().username()); + } + + private void assertAcquireTokenADFS2019(User user){ + PublicClientApplication pca; + try { + pca = PublicClientApplication.builder( + TestConstants.ADFS_APP_ID). + authority(TestConstants.ADFS_AUTHORITY). + build(); + } catch(MalformedURLException ex){ + throw new RuntimeException(ex.getMessage()); + } + + IAuthenticationResult result = acquireTokenInteractive(user, pca, TestConstants.ADFS_SCOPE); + + Assert.assertNotNull(result); + Assert.assertNotNull(result.accessToken()); + Assert.assertNotNull(result.idToken()); + Assert.assertEquals(user.getUpn(), result.account().username()); + } + + private IAuthenticationResult acquireTokenInteractive( + User user, + PublicClientApplication pca, + String scope){ + + IAuthenticationResult result; + try { + URI url = new URI("http://localhost:8080"); + + SystemBrowserOptions browserOptions = new SystemBrowserOptions(); + browserOptions.openBrowserAction( + new SeleniumOpenBrowserAction(user, pca)); + + InteractiveRequestParameters parameters = InteractiveRequestParameters + .builder(Collections.singleton(scope), url) + .systemBrowserOptions(browserOptions) + .build(); + + result = pca.acquireToken(parameters).get(); + + } catch(Exception e){ + LOG.error("Error acquiring token with authCode: " + e.getMessage()); + throw new RuntimeException("Error acquiring token with authCode: " + e.getMessage()); + } + return result; + } + + class SeleniumOpenBrowserAction implements OpenBrowserAction { + + private User user; + private PublicClientApplication pca; + + SeleniumOpenBrowserAction(User user, PublicClientApplication pca){ + this.user = user; + this.pca = pca; + } + + public void openBrowser(URL url){ + seleniumDriver.navigate().to(url); + runSeleniumAutomatedLogin(user, pca); + } + } } diff --git a/src/integrationtest/java/com.microsoft.aad.msal4j/AuthorizationCodeIT.java b/src/integrationtest/java/com.microsoft.aad.msal4j/AuthorizationCodeIT.java index 5725fbea..f0be5ba9 100644 --- a/src/integrationtest/java/com.microsoft.aad.msal4j/AuthorizationCodeIT.java +++ b/src/integrationtest/java/com.microsoft.aad.msal4j/AuthorizationCodeIT.java @@ -239,7 +239,7 @@ private String acquireAuthorizationCodeAutomated( if (result == null || StringHelper.isBlank(result.code())) { throw new MsalClientException("No Authorization code was returned from the server", - AuthenticationErrorCode.AUTHORIZATION_RESULT_BLANK); + AuthenticationErrorCode.INVALID_AUTHORIZATION_RESULT); } return result.code(); } diff --git a/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByInteractiveFlowSupplier.java b/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByInteractiveFlowSupplier.java index 891c8137..f3a52123 100644 --- a/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByInteractiveFlowSupplier.java +++ b/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByInteractiveFlowSupplier.java @@ -76,7 +76,7 @@ private void validateState(AuthorizationResult authorizationResult){ throw new MsalClientException("State returned in authorization result is blank or does " + "not match state sent on outgoing request", - AuthenticationErrorCode.INVALID_AUTHORIZATION_RESULT_STATE); + AuthenticationErrorCode.INVALID_AUTHORIZATION_RESULT); } } @@ -133,7 +133,7 @@ private AuthorizationResult getAuthorizationResultFromHttpListener(){ if (result == null || StringHelper.isBlank(result.code())) { throw new MsalClientException("No Authorization code was returned from the server", - AuthenticationErrorCode.AUTHORIZATION_RESULT_BLANK); + AuthenticationErrorCode.INVALID_AUTHORIZATION_RESULT); } return result; } diff --git a/src/main/java/com/microsoft/aad/msal4j/AuthenticationErrorCode.java b/src/main/java/com/microsoft/aad/msal4j/AuthenticationErrorCode.java index 679d1cd3..b4ced34d 100644 --- a/src/main/java/com/microsoft/aad/msal4j/AuthenticationErrorCode.java +++ b/src/main/java/com/microsoft/aad/msal4j/AuthenticationErrorCode.java @@ -64,18 +64,20 @@ public class AuthenticationErrorCode { */ public final static String LOOPBACK_REDIRECT_URI = "loopback_redirect_uri"; - /** * Unable to start Http listener to the specified port. This might be because the port is unable. */ public final static String UNABLE_TO_START_HTTP_LISTENER = "unable_to_start_http_listener"; - public final static String PORT_BLOCKED = "port_blocked"; - - public final static String AUTHORIZATION_RESULT_BLANK = "authorization_code_blank"; - - public final static String INVALID_AUTHORIZATION_RESULT_STATE = "invalid_authorization_result_state"; + /** + * Authorization result response is invalid, either because is valid or it does not contain + * an authorization code. + */ + public final static String INVALID_AUTHORIZATION_RESULT = "invalid_authorization_result"; - public final static String INVALID_REDIRECT_URI = "incalid_redirect_uri"; + /** + * Redirect URI provided to MSAL is of invalid format. Redirect URL must be a loopback URL. + */ + public final static String INVALID_REDIRECT_URI = "invalid_redirect_uri"; } diff --git a/src/main/java/com/microsoft/aad/msal4j/AuthorizationRequestUrlParameters.java b/src/main/java/com/microsoft/aad/msal4j/AuthorizationRequestUrlParameters.java index 77b5a39c..d8e7da5b 100644 --- a/src/main/java/com/microsoft/aad/msal4j/AuthorizationRequestUrlParameters.java +++ b/src/main/java/com/microsoft/aad/msal4j/AuthorizationRequestUrlParameters.java @@ -13,12 +13,14 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeSet; +/** + * Parameters for {@link ClientApplicationBase#getAuthorizationRequestUrl(AuthorizationRequestUrlParameters)} + */ @Accessors(fluent = true) @Getter public class AuthorizationRequestUrlParameters { @@ -33,6 +35,7 @@ public class AuthorizationRequestUrlParameters { private String nonce; private ResponseMode responseMode; private String loginHint; + private String domainHint; private Prompt prompt; private String correlationId; @@ -108,6 +111,11 @@ private AuthorizationRequestUrlParameters(Builder builder){ requestParameters.put("login_hint", Collections.singletonList(builder.loginHint)); } + if(builder.domainHint != null){ + this.domainHint = domainHint(); + requestParameters.put("domain_hint", Collections.singletonList(builder.domainHint)); + } + if(builder.prompt != null){ this.prompt = builder.prompt; requestParameters.put("prompt", Collections.singletonList(builder.prompt.toString())); @@ -145,6 +153,7 @@ public static class Builder { private String nonce; private ResponseMode responseMode; private String loginHint; + private String domainHint; private Prompt prompt; private String correlationId; @@ -156,56 +165,113 @@ private Builder self() { return this; } + /** + * The redirect URI where authentication responses can be received by your application. It + * must exactly match one of the redirect URIs registered in the Azure portal. + */ public Builder redirectUri(String val){ this.redirectUri = val; return self(); } + /** + * Scopes which the application is requesting access to and the user will consent to. + */ public Builder scopes(Set val){ this.scopes = val; return self(); } + /** + * In cases where Azure AD tenant admin has enabled conditional access policies, and the + * policy has not been met,{@link MsalServiceException} will contain claims that need be + * consented to. + */ public Builder claims(Set val){ this.claims = val; return self(); } + /** + * Used to secure authorization code grant via Proof of Key for Code Exchange (PKCE). + * Required if codeChallenge is included. For more information, see the PKCE RCF: + * https://tools.ietf.org/html/rfc7636 + */ public Builder codeChallenge(String val){ this.codeChallenge = val; return self(); } + /** + * The method used to encode the code verifier for the code challenge parameter. Can be one + * of plain or S256. If excluded, code challenge is assumed to be plaintext. For more + * information, see the PKCE RCF: https://tools.ietf.org/html/rfc7636 + */ public Builder codeChallengeMethod(String val){ this.codeChallengeMethod = val; return self(); } + /** + * A value included in the request that is also returned in the token response. A randomly + * generated unique value is typically used for preventing cross site request forgery attacks. + * The state is also used to encode information about the user's state in the app before the + * authentication request occurred. + * */ public Builder state(String val){ this.state = val; return self(); } + /** + * A value included in the request that is also returned in the token response. A randomly + * generated unique value is typically used for preventing cross site request forgery attacks. + */ public Builder nonce(String val){ this.nonce = val; return self(); } + /** + * Specifies the method that should be used to send the authentication result to your app. + */ public Builder responseMode(ResponseMode val){ this.responseMode = val; return self(); } + /** + * Can be used to pre-fill the username/email address field of the sign-in page for the user, + * if you know the username/email address ahead of time. Often apps use this parameter during + * re-authentication, having already extracted the username from a previous sign-in using the + * preferred_username claim. + */ public Builder loginHint(String val){ this.loginHint = val; return self(); } + /** + * Provides a hint about the tenant or domain that the user should use to sign in. The value + * of the domain hint is a registered domain for the tenant. + **/ + public Builder domainHint(String val){ + this.domainHint = val; + return self(); + } + + /** + * Indicates the type of user interaction that is required. Possible values are + * {@link Prompt} + */ public Builder prompt(Prompt val){ this.prompt = val; return self(); } + /** + * Identifier used to correlate requests for telemetry purposes. Usually a GUID. + */ public Builder correlationId(String val){ this.correlationId = val; return self(); diff --git a/src/main/java/com/microsoft/aad/msal4j/AuthorizationResponseHandler.java b/src/main/java/com/microsoft/aad/msal4j/AuthorizationResponseHandler.java index e278d9a7..73b76626 100644 --- a/src/main/java/com/microsoft/aad/msal4j/AuthorizationResponseHandler.java +++ b/src/main/java/com/microsoft/aad/msal4j/AuthorizationResponseHandler.java @@ -3,6 +3,7 @@ package com.microsoft.aad.msal4j; +import com.sun.net.httpserver.Headers; import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpHandler; import lombok.Getter; @@ -22,12 +23,12 @@ class AuthorizationResponseHandler implements HttpHandler { private final static Logger LOG = LoggerFactory.getLogger(AuthorizationResponseHandler.class); + private final static String DEFAULT_SUCCESS_MESSAGE = "Authentication Complete"+ " Authentication complete. You can close the browser and return to the application."+ " " ; - private final static String DEFAULT_FAILURE_MESSAGE = " " + - "Authentication Failed " + + private final static String DEFAULT_FAILURE_MESSAGE = "Authentication Failed " + " Authentication failed. You can return to the application. Feel free to close this browser tab. " + "



Error details: error {0} error_description: {1} "; @@ -65,22 +66,55 @@ public void handle(HttpExchange httpExchange) throws IOException { private void sendResponse(HttpExchange httpExchange, AuthorizationResult result) throws IOException{ - String response; switch (result.status()){ case Success: - response = DEFAULT_SUCCESS_MESSAGE; + sendSuccessResponse(httpExchange, getSuccessfulResponseMessage()); break; case ProtocolError: - response = DEFAULT_FAILURE_MESSAGE; + case UnknownError: + sendErrorResponse(httpExchange, getErrorResponseMessage()); break; - default: - //TODO better exception - throw new MsalClientException("Received unknown result from authorization response", "error"); } + } + + private void sendSuccessResponse(HttpExchange httpExchange, String response) throws IOException { + if (systemBrowserOptions().browserRedirectSuccess() != null) { + send302Response(httpExchange, systemBrowserOptions().browserRedirectSuccess().toString()); + } else { + send200Response(httpExchange, response); + } + } + + private void sendErrorResponse(HttpExchange httpExchange, String response) throws IOException { + if(systemBrowserOptions().browserRedirectError() != null){ + send302Response(httpExchange, systemBrowserOptions().browserRedirectError().toString()); + } else { + send200Response(httpExchange, response); + } + } + + private void send302Response(HttpExchange httpExchange, String redirectUri) throws IOException{ + Headers responseHeaders = httpExchange.getResponseHeaders(); + responseHeaders.set("Location", redirectUri); + httpExchange.sendResponseHeaders(302, 0); + } + private void send200Response(HttpExchange httpExchange, String response) throws IOException{ httpExchange.sendResponseHeaders(200, response.length()); OutputStream os = httpExchange.getResponseBody(); os.write(response.getBytes()); os.close(); } + + private String getSuccessfulResponseMessage(){ + return systemBrowserOptions().htmlMessageSuccess() != null ? + systemBrowserOptions().htmlMessageSuccess() : + DEFAULT_SUCCESS_MESSAGE; + } + + private String getErrorResponseMessage(){ + return systemBrowserOptions().htmlMessageError() != null ? + systemBrowserOptions().htmlMessageError() : + DEFAULT_FAILURE_MESSAGE; + } } diff --git a/src/main/java/com/microsoft/aad/msal4j/AuthorizationResult.java b/src/main/java/com/microsoft/aad/msal4j/AuthorizationResult.java index 946dfa86..0aa13102 100644 --- a/src/main/java/com/microsoft/aad/msal4j/AuthorizationResult.java +++ b/src/main/java/com/microsoft/aad/msal4j/AuthorizationResult.java @@ -24,19 +24,18 @@ class AuthorizationResult { enum AuthorizationStatus { Success, - ErrorHttp, ProtocolError, - UserCancel, UnknownError } - public static AuthorizationResult fromResponseBody(String responseBody){ + static AuthorizationResult fromResponseBody(String responseBody){ if(StringHelper.isBlank(responseBody)){ return new AuthorizationResult( AuthorizationStatus.UnknownError, - AuthenticationErrorCode.AUTHORIZATION_RESULT_BLANK, - "The authorization server returned an invalid response"); + AuthenticationErrorCode.INVALID_AUTHORIZATION_RESULT, + "The authorization server returned an invalid response: response " + + "is null or empty"); } Map queryParameters = parseParameters(responseBody); @@ -50,21 +49,27 @@ public static AuthorizationResult fromResponseBody(String responseBody){ null); } - AuthorizationResult result = new AuthorizationResult(); - - if(queryParameters.containsKey("code")){ - result.code = queryParameters.get("code"); - result.status = AuthorizationStatus.Success; + if(!queryParameters.containsKey("code")){ + return new AuthorizationResult( + AuthorizationStatus.UnknownError, + AuthenticationErrorCode.INVALID_AUTHORIZATION_RESULT, + "Authorization result response does not contain authorization code"); } + AuthorizationResult result = new AuthorizationResult(); + result.code = queryParameters.get("code"); + result.status = AuthorizationStatus.Success; + if(queryParameters.containsKey("state")){ result.state = queryParameters.get("state"); } + return result; } private AuthorizationResult(){ } + private AuthorizationResult(AuthorizationStatus status, String error, String errorDescription){ this.status = status; this.error = error; @@ -82,12 +87,11 @@ private static Map parseParameters(String serverResponse) { query_pairs.put(key, value); } } catch(Exception ex){ - //TODO better exception - System.out.println(ex.getMessage()); + throw new MsalClientException( + AuthenticationErrorCode.INVALID_AUTHORIZATION_RESULT, + String.format("Error parsing authorization result: %s", ex.getMessage())); } return query_pairs; } - - } diff --git a/src/main/java/com/microsoft/aad/msal4j/ClientApplicationBase.java b/src/main/java/com/microsoft/aad/msal4j/ClientApplicationBase.java index 08a0f933..ec9b7afd 100644 --- a/src/main/java/com/microsoft/aad/msal4j/ClientApplicationBase.java +++ b/src/main/java/com/microsoft/aad/msal4j/ClientApplicationBase.java @@ -159,6 +159,7 @@ public CompletableFuture removeAccount(IAccount account) { return future; } + @Override public URL getAuthorizationRequestUrl(AuthorizationRequestUrlParameters parameters) { validateNotNull("parameters", parameters); diff --git a/src/main/java/com/microsoft/aad/msal4j/IClientApplicationBase.java b/src/main/java/com/microsoft/aad/msal4j/IClientApplicationBase.java index 8caa8be8..58f5b152 100644 --- a/src/main/java/com/microsoft/aad/msal4j/IClientApplicationBase.java +++ b/src/main/java/com/microsoft/aad/msal4j/IClientApplicationBase.java @@ -6,6 +6,7 @@ import javax.net.ssl.SSLSocketFactory; import java.net.MalformedURLException; import java.net.Proxy; +import java.net.URL; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; @@ -66,6 +67,19 @@ interface IClientApplicationBase { // */ // java.util.function.Consumer>> telemetryConsumer(); + /** + * Computes the URL of the authorization request letting the user sign-in and consent to the + * application. The URL target the /authorize endpoint of the authority configured in the + * application object. + * + * Once the user successfully authenticates, the response should contain an authorization code, + * which can then be passed in to{@link ClientApplicationBase#acquireToken(AuthorizationCodeParameters)} + * to be exchanged for a token + * @param parameters {@link AuthorizationRequestUrlParameters} + * @return url of the authorization endpoint where the user can sign-in and consent to the application. + */ + URL getAuthorizationRequestUrl(AuthorizationRequestUrlParameters parameters); + /** * Acquires security token from the authority using an authorization code previously received. * diff --git a/src/main/java/com/microsoft/aad/msal4j/IPublicClientApplication.java b/src/main/java/com/microsoft/aad/msal4j/IPublicClientApplication.java index 28410f08..aed245bf 100644 --- a/src/main/java/com/microsoft/aad/msal4j/IPublicClientApplication.java +++ b/src/main/java/com/microsoft/aad/msal4j/IPublicClientApplication.java @@ -45,6 +45,14 @@ public interface IPublicClientApplication extends IClientApplicationBase { */ CompletableFuture acquireToken(DeviceCodeFlowParameters parameters); - //TODO fill in JavaDoc + /** + * Acquires tokens from the authority using authorization code grant. Will attempt to open the + * default system browser where the user can input the credentials interactively, consent to scopes, + * and do multi-factor authentication if such a policy is enabled on the Azure AD tenant. + * System browser can behavior can be customized via {@link InteractiveRequestParameters#systemBrowserOptions}. + * For more information, see https://aka.ms/msal4j-interactive-request + * @param parameters instance of {@link InteractiveRequestParameters} + * @return {@link CompletableFuture} containing an {@link IAuthenticationResult} + */ CompletableFuture acquireToken(InteractiveRequestParameters parameters); } diff --git a/src/main/java/com/microsoft/aad/msal4j/InteractiveRequest.java b/src/main/java/com/microsoft/aad/msal4j/InteractiveRequest.java index 93c5d0ee..e76492f1 100644 --- a/src/main/java/com/microsoft/aad/msal4j/InteractiveRequest.java +++ b/src/main/java/com/microsoft/aad/msal4j/InteractiveRequest.java @@ -82,9 +82,11 @@ private URL createAuthorizationUrl(){ AuthorizationRequestUrlParameters .builder(interactiveRequestParameters.redirectUri().toString(), interactiveRequestParameters.scopes()) + .prompt(interactiveRequestParameters.prompt()) + .loginHint(interactiveRequestParameters.loginHint()) + .domainHint(interactiveRequestParameters.domainHint()) .correlationId(publicClientApplication.correlationId()); - addPkceAndState(authorizationRequestUrlBuilder); return publicClientApplication.getAuthorizationRequestUrl( authorizationRequestUrlBuilder.build()); diff --git a/src/main/java/com/microsoft/aad/msal4j/InteractiveRequestParameters.java b/src/main/java/com/microsoft/aad/msal4j/InteractiveRequestParameters.java index 88e23640..8c03dc15 100644 --- a/src/main/java/com/microsoft/aad/msal4j/InteractiveRequestParameters.java +++ b/src/main/java/com/microsoft/aad/msal4j/InteractiveRequestParameters.java @@ -21,7 +21,7 @@ /** * Object containing parameters for interactive requests. Can be used as parameter to - * {@link PublicClientApplication#acquireToken(InteractiveRequestParameters)} + * {@link PublicClientApplication#acquireToken(InteractiveRequestParameters)}. */ @Builder @Accessors(fluent = true) @@ -29,17 +29,43 @@ @AllArgsConstructor(access = AccessLevel.PRIVATE) public class InteractiveRequestParameters { + /** + * Scopes that the application is requesting access to and the user will consent to. + */ @NonNull private Set scopes; + /** + * Redirect URI where MSAL will listen to for the authorization code returned by Azure AD. + * Should be a loopback URL with a port specified (for example, http://localhost:3671). If no + * port is specified, MSAL will find an open port. For more information, see + * https://aka.ms/msal4j-interactive-request. + */ @Setter(AccessLevel.PACKAGE) @NonNull private URI redirectUri; /** - * Sets system browser options to be used by the PublicClientApplication - * @param systemBrowserOptions System browser options when using acquireTokenInteractiveRequest - * @return instance of the Builder on which method was called + * Indicate the type of user interaction that is required. + */ + private Prompt prompt; + + /** + * Can be used to pre-fill the username/email address field of the sign-in page for the user, + * if you know the username/email address ahead of time. Often apps use this parameter during + * re-authentication, having already extracted the username from a previous sign-in using the + * preferred_username claim. + */ + private String loginHint; + + /** + * Provides a hint about the tenant or domain that the user should use to sign in. The value + * of the domain hint is a registered domain for the tenant. + **/ + private String domainHint; + + /** + * Sets {@link SystemBrowserOptions} to be used by the PublicClientApplication */ private SystemBrowserOptions systemBrowserOptions; diff --git a/src/main/java/com/microsoft/aad/msal4j/OpenBrowserAction.java b/src/main/java/com/microsoft/aad/msal4j/OpenBrowserAction.java index ac1fdff2..ee558227 100644 --- a/src/main/java/com/microsoft/aad/msal4j/OpenBrowserAction.java +++ b/src/main/java/com/microsoft/aad/msal4j/OpenBrowserAction.java @@ -10,5 +10,13 @@ * PublicClientApplication defaults to using default system browser */ public interface OpenBrowserAction { + + /** + * Override for providing custom browser initialization logic. Method that is called by MSAL + * when doing {@link IPublicClientApplication#acquireToken(InteractiveRequestParameters)}. If + * not overridden, MSAL will attempt to open URL in default system browser. + * @param url URL to the /authorize endpoint which should opened up in a browser so the user can + * provide their credentials and consent to scopes. + */ void openBrowser(URL url); } diff --git a/src/main/java/com/microsoft/aad/msal4j/Prompt.java b/src/main/java/com/microsoft/aad/msal4j/Prompt.java index fd154ea3..73c67079 100644 --- a/src/main/java/com/microsoft/aad/msal4j/Prompt.java +++ b/src/main/java/com/microsoft/aad/msal4j/Prompt.java @@ -3,10 +3,31 @@ package com.microsoft.aad.msal4j; +/** + * Indicate the type of user interaction that is required when sending authorization code request. + */ public enum Prompt { + + /** + * The user should be prompted to reauthenticate. + */ LOGIN ("login"), + + /** + *The user is prompted to select an account, interrupting single sign on. The user may select + * an existing signed-in account, enter their credentials for a remembered account, + * or choose to use a different account altogether. + */ SELECT_ACCOUNT ("select_account"), + + /** + * User consent has been granted, but needs to be updated. The user should be prompted to consent. + */ CONSENT ("consent"), + + /** + * An administrator should be prompted to consent on behalf of all users in their organization. + */ ADMING_CONSENT ("admin_consent"); private String prompt; diff --git a/src/main/java/com/microsoft/aad/msal4j/ResponseMode.java b/src/main/java/com/microsoft/aad/msal4j/ResponseMode.java index 1523594f..43767bc0 100644 --- a/src/main/java/com/microsoft/aad/msal4j/ResponseMode.java +++ b/src/main/java/com/microsoft/aad/msal4j/ResponseMode.java @@ -3,9 +3,28 @@ package com.microsoft.aad.msal4j; +/** + * Values for possible methods in which AAD can send the authorization result back to the calling + * application + */ public enum ResponseMode { + + /** + * Authorization result is encoded as HTML form values that are transmitted via a HTTP POST + * to the redirect URL + */ FORM_POST("form_post"), + + /** + * Authorization result returned as query string in the redirect URL when redirecting back to the + * client application. + */ QUERY("query"), + + /** + * Authorization result is returned in the fragment added to the redirect URL when redirecting + * back to the client application + */ FRAGMENT("fragment"); private String responseMode; diff --git a/src/main/java/com/microsoft/aad/msal4j/StringHelper.java b/src/main/java/com/microsoft/aad/msal4j/StringHelper.java index fe73cb6f..601ffcc8 100644 --- a/src/main/java/com/microsoft/aad/msal4j/StringHelper.java +++ b/src/main/java/com/microsoft/aad/msal4j/StringHelper.java @@ -25,6 +25,4 @@ static String createBase64EncodedSha256Hash(String stringToHash){ } return base64EncodedSha256Hash; } - - } diff --git a/src/main/java/com/microsoft/aad/msal4j/SystemBrowserOptions.java b/src/main/java/com/microsoft/aad/msal4j/SystemBrowserOptions.java index f98a493e..870a8d60 100644 --- a/src/main/java/com/microsoft/aad/msal4j/SystemBrowserOptions.java +++ b/src/main/java/com/microsoft/aad/msal4j/SystemBrowserOptions.java @@ -9,18 +9,46 @@ import java.net.URI; +/** + * Options for using the default OS browser as a separate process to handle interactive authentication. + * MSAL will listen for the OS browser to finish authenticating, but it cannot close the browser. + * It can however response with a HTTP 200 OK message or a 302 Redirect, which can be configured here. + * For more details, see https://aka.ms/msal4j-os-browser + */ @Accessors(fluent = true) @Getter @Setter public class SystemBrowserOptions { + /** + * When the user finishes authenticating, MSAL will respond with a Http 200 OK message, which the + * browser will show to the user + */ private String htmlMessageSuccess; + /** + * WHen the user finishes authenticating, but an error occurred, MSAL will respond with a + * Http 200 Ok message, which the browser will show to the user. + */ private String htmlMessageError; + /** + * When the user finishes authenticating, MSAL will redirect the browser to the given URI. + * Takes precedence over htmlMessageSuccess + */ private URI browserRedirectSuccess; + /** + * When the the user finishes authenticating, but an error occurred, MSAL will redirect the + * browser to the given URI. + * Takes precedence over htmlMessageError + */ private URI browserRedirectError; + /** + * Allows developers to implement their own logic for starting a browser and navigating to a + * specific Uri. Msal will use this when opening the browser. If not set, the user configured + * browser will be used. + */ private OpenBrowserAction openBrowserAction; } diff --git a/src/test/java/com/microsoft/aad/msal4j/AuthorizationRequestUrlParametersTest.java b/src/test/java/com/microsoft/aad/msal4j/AuthorizationRequestUrlParametersTest.java index d6b44474..f5b464b8 100644 --- a/src/test/java/com/microsoft/aad/msal4j/AuthorizationRequestUrlParametersTest.java +++ b/src/test/java/com/microsoft/aad/msal4j/AuthorizationRequestUrlParametersTest.java @@ -91,6 +91,7 @@ public void testBuilder_optionalParameters() throws UnsupportedEncodingException .nonce("app_nonce") .correlationId("corr_id") .loginHint("hint") + .domainHint("domain_hint") .prompt(Prompt.SELECT_ACCOUNT) .build(); @@ -119,5 +120,6 @@ public void testBuilder_optionalParameters() throws UnsupportedEncodingException Assert.assertEquals(queryParameters.get("nonce"), "app_nonce"); Assert.assertEquals(queryParameters.get("correlation_id"), "corr_id"); Assert.assertEquals(queryParameters.get("login_hint"), "hint"); + Assert.assertEquals(queryParameters.get("domain_hint"), "domain_hint"); } } From 6c76413b6ef39fd2659197870ecfd7e8d481d5b8 Mon Sep 17 00:00:00 2001 From: sgonzalezMSFT Date: Tue, 11 Feb 2020 13:44:48 -0800 Subject: [PATCH 12/24] Add b2c test. --- .../AcquireTokenInteractiveIT.java | 26 +++++++++++++++++++ .../AuthorizationCodeIT.java | 5 ++-- .../TestConstants.java | 4 +-- ...AcquireTokenByInteractiveFlowSupplier.java | 9 +++---- .../aad/msal4j/ClientApplicationBase.java | 1 - .../microsoft/aad/msal4j/HttpListener.java | 2 -- 6 files changed, 34 insertions(+), 13 deletions(-) diff --git a/src/integrationtest/java/com.microsoft.aad.msal4j/AcquireTokenInteractiveIT.java b/src/integrationtest/java/com.microsoft.aad.msal4j/AcquireTokenInteractiveIT.java index 98181849..3125d632 100644 --- a/src/integrationtest/java/com.microsoft.aad.msal4j/AcquireTokenInteractiveIT.java +++ b/src/integrationtest/java/com.microsoft.aad.msal4j/AcquireTokenInteractiveIT.java @@ -3,7 +3,9 @@ package com.microsoft.aad.msal4j; +import labapi.B2CProvider; import labapi.FederationProvider; +import labapi.LabService; import labapi.User; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -55,6 +57,12 @@ public void acquireTokenInteractive_ADFSv2_Federated(){ assertAcquireTokenAAD(user); } + @Test + public void acquireTokenWithAuthorizationCode_B2C_Local(){ + User user = labUserProvider.getB2cUser(B2CProvider.LOCAL); + assertAcquireTokenB2C(user); + } + private void assertAcquireTokenAAD(User user){ PublicClientApplication pca; @@ -97,6 +105,24 @@ private void assertAcquireTokenADFS2019(User user){ Assert.assertEquals(user.getUpn(), result.account().username()); } + private void assertAcquireTokenB2C(User user){ + + PublicClientApplication pca; + try { + pca = PublicClientApplication.builder( + user.getAppId()). + b2cAuthority(TestConstants.B2C_AUTHORITY_SIGN_IN). + build(); + } catch(MalformedURLException ex){ + throw new RuntimeException(ex.getMessage()); + } + + IAuthenticationResult result = acquireTokenInteractive(user, pca, user.getAppId()); + Assert.assertNotNull(result); + Assert.assertNotNull(result.accessToken()); + Assert.assertNotNull(result.idToken()); + } + private IAuthenticationResult acquireTokenInteractive( User user, PublicClientApplication pca, diff --git a/src/integrationtest/java/com.microsoft.aad.msal4j/AuthorizationCodeIT.java b/src/integrationtest/java/com.microsoft.aad.msal4j/AuthorizationCodeIT.java index f0be5ba9..4b6f59f2 100644 --- a/src/integrationtest/java/com.microsoft.aad.msal4j/AuthorizationCodeIT.java +++ b/src/integrationtest/java/com.microsoft.aad.msal4j/AuthorizationCodeIT.java @@ -58,7 +58,6 @@ public void acquireTokenWithAuthorizationCode_ADFSv2_Federated(){ } @Test - // TODO Redirect URI localhost in not registered public void acquireTokenWithAuthorizationCode_B2C_Local(){ User user = labUserProvider.getB2cUser(B2CProvider.LOCAL); assertAcquireTokenB2C(user); @@ -144,8 +143,8 @@ private void assertAcquireTokenAAD(User user){ private void assertAcquireTokenB2C(User user){ - String appId = LabService.getSecret(TestConstants.B2C_LAB_APP_ID); - String appSecret = LabService.getSecret(TestConstants.B2C_LAB_APP_SECRET); + String appId = LabService.getSecret(TestConstants.B2C_CONFIDENTIAL_CLIENT_LAB_APP_ID); + String appSecret = LabService.getSecret(TestConstants.B2C_CONFIDENTIAL_CLIENT_APP_SECRET); ConfidentialClientApplication cca; try { diff --git a/src/integrationtest/java/com.microsoft.aad.msal4j/TestConstants.java b/src/integrationtest/java/com.microsoft.aad.msal4j/TestConstants.java index 7649d5f6..4f0839c2 100644 --- a/src/integrationtest/java/com.microsoft.aad.msal4j/TestConstants.java +++ b/src/integrationtest/java/com.microsoft.aad.msal4j/TestConstants.java @@ -9,8 +9,8 @@ public class TestConstants { public final static String GRAPH_DEFAULT_SCOPE = "https://graph.windows.net/.default"; public final static String USER_READ_SCOPE = "user.read"; public final static String B2C_LAB_SCOPE = "https://msidlabb2c.onmicrosoft.com/msaapp/user_impersonation"; - public final static String B2C_LAB_APP_SECRET = "MSIDLABB2C-MSAapp-AppSecret"; - public final static String B2C_LAB_APP_ID = "MSIDLABB2C-MSAapp-AppID"; + public final static String B2C_CONFIDENTIAL_CLIENT_APP_SECRET = "MSIDLABB2C-MSAapp-AppSecret"; + public final static String B2C_CONFIDENTIAL_CLIENT_LAB_APP_ID = "MSIDLABB2C-MSAapp-AppID"; public final static String MICROSOFT_AUTHORITY_HOST = "https://login.microsoftonline.com/"; public final static String ORGANIZATIONS_AUTHORITY = MICROSOFT_AUTHORITY_HOST + "organizations/"; diff --git a/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByInteractiveFlowSupplier.java b/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByInteractiveFlowSupplier.java index f3a52123..b8216d63 100644 --- a/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByInteractiveFlowSupplier.java +++ b/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByInteractiveFlowSupplier.java @@ -21,7 +21,7 @@ class AcquireTokenByInteractiveFlowSupplier extends AuthenticationResultSupplier private PublicClientApplication clientApplication; private InteractiveRequest interactiveRequest; - private BlockingQueue authorizationCodeQueue; + private BlockingQueue authorizationResultQueue; private HttpListener httpListener; AcquireTokenByInteractiveFlowSupplier(PublicClientApplication clientApplication, @@ -45,10 +45,10 @@ private AuthorizationResult getAuthorizationResult(){ SystemBrowserOptions systemBrowserOptions = interactiveRequest.interactiveRequestParameters().systemBrowserOptions(); - authorizationCodeQueue = new LinkedBlockingQueue<>(); + authorizationResultQueue = new LinkedBlockingQueue<>(); AuthorizationResponseHandler authorizationResponseHandler = new AuthorizationResponseHandler( - authorizationCodeQueue, + authorizationResultQueue, systemBrowserOptions); startHttpListener(authorizationResponseHandler); @@ -66,7 +66,6 @@ private AuthorizationResult getAuthorizationResult(){ httpListener.stopListener(); } } - return result; } @@ -125,7 +124,7 @@ private AuthorizationResult getAuthorizationResultFromHttpListener(){ while(result == null && !interactiveRequest.futureReference().get().isCancelled() && TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()) < expirationTime) { - result = authorizationCodeQueue.poll(100, TimeUnit.MILLISECONDS); + result = authorizationResultQueue.poll(100, TimeUnit.MILLISECONDS); } } catch(Exception e){ throw new MsalClientException(e); diff --git a/src/main/java/com/microsoft/aad/msal4j/ClientApplicationBase.java b/src/main/java/com/microsoft/aad/msal4j/ClientApplicationBase.java index ec9b7afd..eba54005 100644 --- a/src/main/java/com/microsoft/aad/msal4j/ClientApplicationBase.java +++ b/src/main/java/com/microsoft/aad/msal4j/ClientApplicationBase.java @@ -171,7 +171,6 @@ public URL getAuthorizationRequestUrl(AuthorizationRequestUrlParameters paramete parameters.requestParameters()); } - AuthenticationResult acquireTokenCommon(MsalRequest msalRequest, Authority requestAuthority) throws Exception { diff --git a/src/main/java/com/microsoft/aad/msal4j/HttpListener.java b/src/main/java/com/microsoft/aad/msal4j/HttpListener.java index b44d7907..81fe5fdb 100644 --- a/src/main/java/com/microsoft/aad/msal4j/HttpListener.java +++ b/src/main/java/com/microsoft/aad/msal4j/HttpListener.java @@ -27,8 +27,6 @@ void startListener(int port, HttpHandler httpHandler) { server.start(); } catch (Exception e){ - //TODO handle exception - System.out.println(e.getMessage()); throw new MsalClientException(e); } } From 2451959ddc176b7cca05a6c171a77914ef29d7a3 Mon Sep 17 00:00:00 2001 From: sgonzalezMSFT Date: Tue, 11 Feb 2020 14:55:40 -0800 Subject: [PATCH 13/24] Update credscan-exclude --- build/credscan-exclude.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/build/credscan-exclude.json b/build/credscan-exclude.json index 9f90dfe0..7752935b 100644 --- a/build/credscan-exclude.json +++ b/build/credscan-exclude.json @@ -12,6 +12,18 @@ { "placeholder": "ClientPassword", "_justification" : "credential used for testing. not associated with any tenant" + }, + { + "placeholder": "ClientPassword", + "_justification" : "credential used for testing. not associated with any tenant" + }, + { + "placeholder": "B2C_CONFIDENTIAL_CLIENT_APP_SECRET", + "_justification" : "Not a credential, just the identifier of the secret exposed by test lab API" + }, + { + "placeholder": "MSIDLABB2C-MSAapp-AppSecret", + "_justification" : "Not a credential, just the identifier of the secret exposed by test lab API" } ] } \ No newline at end of file From fe093c629437084707eca6e085bdbfe589b7893a Mon Sep 17 00:00:00 2001 From: sgonzalezMSFT Date: Tue, 11 Feb 2020 14:56:58 -0800 Subject: [PATCH 14/24] Remove duplicated exclusion in credscan-exclude --- build/credscan-exclude.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/build/credscan-exclude.json b/build/credscan-exclude.json index 7752935b..e639934e 100644 --- a/build/credscan-exclude.json +++ b/build/credscan-exclude.json @@ -13,10 +13,6 @@ "placeholder": "ClientPassword", "_justification" : "credential used for testing. not associated with any tenant" }, - { - "placeholder": "ClientPassword", - "_justification" : "credential used for testing. not associated with any tenant" - }, { "placeholder": "B2C_CONFIDENTIAL_CLIENT_APP_SECRET", "_justification" : "Not a credential, just the identifier of the secret exposed by test lab API" From 5e3091d649c4addd2f2e22557329550404063a6b Mon Sep 17 00:00:00 2001 From: sgonzalezMSFT Date: Tue, 11 Feb 2020 17:20:01 -0800 Subject: [PATCH 15/24] Update AuthorizationResponseHandler --- .../msal4j/AuthorizationResponseHandler.java | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/microsoft/aad/msal4j/AuthorizationResponseHandler.java b/src/main/java/com/microsoft/aad/msal4j/AuthorizationResponseHandler.java index 73b76626..c1bfdad0 100644 --- a/src/main/java/com/microsoft/aad/msal4j/AuthorizationResponseHandler.java +++ b/src/main/java/com/microsoft/aad/msal4j/AuthorizationResponseHandler.java @@ -52,9 +52,9 @@ public void handle(HttpExchange httpExchange) throws IOException { httpExchange.getRequestBody())).lines().collect(Collectors.joining("\n")); AuthorizationResult result = AuthorizationResult.fromResponseBody(responseBody); - authorizationResultQueue.put(result); sendResponse(httpExchange, result); - + authorizationResultQueue.put(result); + } catch (InterruptedException ex){ LOG.error("Error reading response from socket: " + ex.getMessage()); throw new MsalClientException(ex); @@ -78,18 +78,18 @@ private void sendResponse(HttpExchange httpExchange, AuthorizationResult result) } private void sendSuccessResponse(HttpExchange httpExchange, String response) throws IOException { - if (systemBrowserOptions().browserRedirectSuccess() != null) { - send302Response(httpExchange, systemBrowserOptions().browserRedirectSuccess().toString()); - } else { + if (systemBrowserOptions == null || systemBrowserOptions.browserRedirectSuccess() == null) { send200Response(httpExchange, response); + } else { + send302Response(httpExchange, systemBrowserOptions().browserRedirectSuccess().toString()); } } private void sendErrorResponse(HttpExchange httpExchange, String response) throws IOException { - if(systemBrowserOptions().browserRedirectError() != null){ - send302Response(httpExchange, systemBrowserOptions().browserRedirectError().toString()); - } else { + if(systemBrowserOptions == null || systemBrowserOptions.browserRedirectError() == null){ send200Response(httpExchange, response); + } else { + send302Response(httpExchange, systemBrowserOptions().browserRedirectError().toString()); } } @@ -107,14 +107,16 @@ private void send200Response(HttpExchange httpExchange, String response) throws } private String getSuccessfulResponseMessage(){ - return systemBrowserOptions().htmlMessageSuccess() != null ? - systemBrowserOptions().htmlMessageSuccess() : - DEFAULT_SUCCESS_MESSAGE; + if(systemBrowserOptions == null || systemBrowserOptions.htmlMessageSuccess() == null) { + return DEFAULT_SUCCESS_MESSAGE; + } + return systemBrowserOptions().htmlMessageSuccess(); } private String getErrorResponseMessage(){ - return systemBrowserOptions().htmlMessageError() != null ? - systemBrowserOptions().htmlMessageError() : - DEFAULT_FAILURE_MESSAGE; + if(systemBrowserOptions == null || systemBrowserOptions.htmlMessageError() == null) { + return DEFAULT_FAILURE_MESSAGE; + } + return systemBrowserOptions().htmlMessageSuccess(); } } From 1db4f9b5b812c4bb740461e1d34d90123fb9e044 Mon Sep 17 00:00:00 2001 From: sgonzalezMSFT Date: Thu, 13 Feb 2020 09:58:27 -0800 Subject: [PATCH 16/24] Update SystemBrowserOptions nad InteractiveRequestParameters public API --- .../AcquireTokenInteractiveIT.java | 12 +++++++----- .../AuthorizationCodeIT.java | 2 +- .../aad/msal4j/InteractiveRequestParameters.java | 15 ++++++--------- .../aad/msal4j/SystemBrowserOptions.java | 16 +++++++++++++--- 4 files changed, 27 insertions(+), 18 deletions(-) diff --git a/src/integrationtest/java/com.microsoft.aad.msal4j/AcquireTokenInteractiveIT.java b/src/integrationtest/java/com.microsoft.aad.msal4j/AcquireTokenInteractiveIT.java index 3125d632..185a4cdb 100644 --- a/src/integrationtest/java/com.microsoft.aad.msal4j/AcquireTokenInteractiveIT.java +++ b/src/integrationtest/java/com.microsoft.aad.msal4j/AcquireTokenInteractiveIT.java @@ -5,7 +5,6 @@ import labapi.B2CProvider; import labapi.FederationProvider; -import labapi.LabService; import labapi.User; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -132,12 +131,15 @@ private IAuthenticationResult acquireTokenInteractive( try { URI url = new URI("http://localhost:8080"); - SystemBrowserOptions browserOptions = new SystemBrowserOptions(); - browserOptions.openBrowserAction( - new SeleniumOpenBrowserAction(user, pca)); + SystemBrowserOptions browserOptions = + SystemBrowserOptions + .builder() + .openBrowserAction(new SeleniumOpenBrowserAction(user, pca)) + .build(); InteractiveRequestParameters parameters = InteractiveRequestParameters - .builder(Collections.singleton(scope), url) + .builder(url) + .scopes(Collections.singleton(scope)) .systemBrowserOptions(browserOptions) .build(); diff --git a/src/integrationtest/java/com.microsoft.aad.msal4j/AuthorizationCodeIT.java b/src/integrationtest/java/com.microsoft.aad.msal4j/AuthorizationCodeIT.java index 4b6f59f2..33b40d7a 100644 --- a/src/integrationtest/java/com.microsoft.aad.msal4j/AuthorizationCodeIT.java +++ b/src/integrationtest/java/com.microsoft.aad.msal4j/AuthorizationCodeIT.java @@ -210,7 +210,7 @@ private String acquireAuthorizationCodeAutomated( AuthorizationResponseHandler authorizationResponseHandler = new AuthorizationResponseHandler( authorizationCodeQueue, - new SystemBrowserOptions()); + SystemBrowserOptions.builder().build()); httpListener = new HttpListener(); httpListener.startListener(8080, authorizationResponseHandler); diff --git a/src/main/java/com/microsoft/aad/msal4j/InteractiveRequestParameters.java b/src/main/java/com/microsoft/aad/msal4j/InteractiveRequestParameters.java index 8c03dc15..6a632a23 100644 --- a/src/main/java/com/microsoft/aad/msal4j/InteractiveRequestParameters.java +++ b/src/main/java/com/microsoft/aad/msal4j/InteractiveRequestParameters.java @@ -29,12 +29,6 @@ @AllArgsConstructor(access = AccessLevel.PRIVATE) public class InteractiveRequestParameters { - /** - * Scopes that the application is requesting access to and the user will consent to. - */ - @NonNull - private Set scopes; - /** * Redirect URI where MSAL will listen to for the authorization code returned by Azure AD. * Should be a loopback URL with a port specified (for example, http://localhost:3671). If no @@ -45,6 +39,11 @@ public class InteractiveRequestParameters { @NonNull private URI redirectUri; + /** + * Scopes that the application is requesting access to and the user will consent to. + */ + private Set scopes; + /** * Indicate the type of user interaction that is required. */ @@ -73,13 +72,11 @@ private static InteractiveRequestParametersBuilder builder() { return new InteractiveRequestParametersBuilder(); } - public static InteractiveRequestParametersBuilder builder(Set scopes, URI redirectUri) { + public static InteractiveRequestParametersBuilder builder(URI redirectUri) { - validateNotEmpty("scopes", scopes); validateNotNull("redirect_uri", redirectUri); return builder() - .scopes(scopes) .redirectUri(redirectUri); } } diff --git a/src/main/java/com/microsoft/aad/msal4j/SystemBrowserOptions.java b/src/main/java/com/microsoft/aad/msal4j/SystemBrowserOptions.java index 870a8d60..00e47a78 100644 --- a/src/main/java/com/microsoft/aad/msal4j/SystemBrowserOptions.java +++ b/src/main/java/com/microsoft/aad/msal4j/SystemBrowserOptions.java @@ -3,8 +3,10 @@ package com.microsoft.aad.msal4j; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; -import lombok.Setter; import lombok.experimental.Accessors; import java.net.URI; @@ -13,11 +15,12 @@ * Options for using the default OS browser as a separate process to handle interactive authentication. * MSAL will listen for the OS browser to finish authenticating, but it cannot close the browser. * It can however response with a HTTP 200 OK message or a 302 Redirect, which can be configured here. - * For more details, see https://aka.ms/msal4j-os-browser + * For more details, see https://aka.ms/msal4j-interactive-request */ +@Builder @Accessors(fluent = true) @Getter -@Setter +@AllArgsConstructor(access = AccessLevel.PRIVATE) public class SystemBrowserOptions { /** @@ -51,4 +54,11 @@ public class SystemBrowserOptions { * browser will be used. */ private OpenBrowserAction openBrowserAction; + + /** + * Builder for {@link SystemBrowserOptions} + */ + public static SystemBrowserOptionsBuilder builder() { + return new SystemBrowserOptionsBuilder(); + } } From 855a1aa0631cdd932a8200b89a57efcf8bd9f587 Mon Sep 17 00:00:00 2001 From: sgonzalezMSFT Date: Thu, 13 Feb 2020 16:13:19 -0800 Subject: [PATCH 17/24] Update dev samples to show recommended practices --- src/samples/SSLTunnelSocketFactory.java | 157 ------------------ src/samples/cache/TokenCacheAspect.java | 41 +++++ src/samples/cache/sample_cache.json | 53 ++++++ .../ClientCredentialGrant.java | 76 +++++---- .../msal-b2c-web-sample.iml | 102 ------------ src/samples/public-client/B2CFlow.java | 42 ----- src/samples/public-client/DeviceCodeFlow.java | 80 ++++++--- .../IntegratedWindowsAuthFlow.java | 36 ---- .../IntegratedWindowsAuthenticationFlow.java | 74 +++++++++ .../public-client/InteractiveFlow.java | 66 ++++++-- .../public-client/InteractiveFlowB2C.java | 75 +++++++++ src/samples/public-client/TestData.java | 22 --- .../public-client/UsernamePasswordFlow.java | 99 ++++++----- 13 files changed, 447 insertions(+), 476 deletions(-) delete mode 100644 src/samples/SSLTunnelSocketFactory.java create mode 100644 src/samples/cache/TokenCacheAspect.java create mode 100644 src/samples/cache/sample_cache.json delete mode 100644 src/samples/msal-b2c-web-sample/msal-b2c-web-sample.iml delete mode 100644 src/samples/public-client/B2CFlow.java delete mode 100644 src/samples/public-client/IntegratedWindowsAuthFlow.java create mode 100644 src/samples/public-client/IntegratedWindowsAuthenticationFlow.java create mode 100644 src/samples/public-client/InteractiveFlowB2C.java delete mode 100644 src/samples/public-client/TestData.java diff --git a/src/samples/SSLTunnelSocketFactory.java b/src/samples/SSLTunnelSocketFactory.java deleted file mode 100644 index 214c29ce..00000000 --- a/src/samples/SSLTunnelSocketFactory.java +++ /dev/null @@ -1,157 +0,0 @@ -// sample was written based on https://docs.oracle.com/javase/7/docs/technotes/guides/security/jsse/samples/sockets/client/SSLSocketClientWithTunneling.java -// here is the copyright notice: - -/* - * - * Copyright (c) 1994, 2004, Oracle and/or its affiliates. All rights reserved. - * - * Redistribution and use in source and binary forms, with or - * without modification, are permitted provided that the following - * conditions are met: - * - * -Redistribution of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * Redistribution in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in - * the documentation and/or other materials provided with the - * distribution. - * - * Neither the name of Oracle nor the names of - * contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * This software is provided "AS IS," without a warranty of any - * kind. ALL EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND - * WARRANTIES, INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY - * EXCLUDED. SUN MICROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL - * NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT - * OF USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS - * DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR - * ANY LOST REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, - * SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER - * CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF - * THE USE OF OR INABILITY TO USE THIS SOFTWARE, EVEN IF SUN HAS - * BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. - * - * You acknowledge that this software is not designed, licensed or - * intended for use in the design, construction, operation or - * maintenance of any nuclear facility. - */ - - -import com.nimbusds.jose.util.Base64; - -import java.net.*; -import java.io.*; -import javax.net.ssl.*; - - -/** - * SSLSocketFactory for tunneling ssl sockets through a proxy with Basic Authorization - */ -public class SSLTunnelSocketFactory extends SSLSocketFactory { - private SSLSocketFactory defaultFactory; - - private String tunnelHost; - - private int tunnelPort; - - private String proxyUserName; - - private String proxyPassword; - - public SSLTunnelSocketFactory(String proxyHost, String proxyPort, - String proxyUserName, String proxyPassword) { - tunnelHost = proxyHost; - tunnelPort = Integer.parseInt(proxyPort); - defaultFactory = (SSLSocketFactory) SSLSocketFactory.getDefault(); - - this.proxyUserName = proxyUserName; - this.proxyPassword = proxyPassword; - } - - public Socket createSocket(String host, int port) throws IOException { - return createSocket(null, host, port, true); - } - - public Socket createSocket(String host, int port, InetAddress clientHost, - int clientPort) throws IOException { - return createSocket(null, host, port, true); - } - - public Socket createSocket(InetAddress host, int port) throws IOException { - return createSocket(null, host.getHostName(), port, true); - } - - public Socket createSocket(InetAddress address, int port, - InetAddress clientAddress, int clientPort) throws IOException { - return createSocket(null, address.getHostName(), port, true); - } - - public Socket createSocket(Socket s, String host, int port, - boolean autoClose) throws IOException { - - Socket tunnel = new Socket(tunnelHost, tunnelPort); - - doTunnelHandshake(tunnel, host, port); - - SSLSocket result = (SSLSocket) defaultFactory.createSocket(tunnel, host, - port, autoClose); - - return result; - } - - private void doTunnelHandshake(Socket tunnel, String host, int port) - throws IOException { - OutputStream out = tunnel.getOutputStream(); - - String token = proxyUserName + ":" + proxyPassword; - String authString = "Basic " + Base64.encode(token.getBytes()); - - String msg = "CONNECT " + host + ":" + port + " HTTP/1.1\n" - + "User-Agent: " + sun.net.www.protocol.http.HttpURLConnection.userAgent + "\n" - + "Proxy-Authorization: " + authString - + "\r\n\r\n"; - - out.write(msg.getBytes("UTF-8")); - out.flush(); - - - StringBuilder replyStr = new StringBuilder(); - int newlinesSeen = 0; - boolean headerDone = false; /* Done on first newline */ - - InputStream in = tunnel.getInputStream(); - - while (newlinesSeen < 2) { - int i = in.read(); - if (i < 0) { - throw new IOException("Unexpected EOF from proxy"); - } - if (i == '\n') { - headerDone = true; - ++newlinesSeen; - } else if (i != '\r') { - newlinesSeen = 0; - if (!headerDone) { - replyStr.append((char) i); - } - } - } - - if (replyStr.toString().toLowerCase().indexOf("200 connection established") == -1) { - throw new IOException("Unable to tunnel through " + tunnelHost - + ":" + tunnelPort + ". Proxy returns \"" + replyStr + "\""); - } - } - - public String[] getDefaultCipherSuites() { - return defaultFactory.getDefaultCipherSuites(); - } - - public String[] getSupportedCipherSuites() { - return defaultFactory.getSupportedCipherSuites(); - } -} \ No newline at end of file diff --git a/src/samples/cache/TokenCacheAspect.java b/src/samples/cache/TokenCacheAspect.java new file mode 100644 index 00000000..998f52df --- /dev/null +++ b/src/samples/cache/TokenCacheAspect.java @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import com.microsoft.aad.msal4j.ITokenCacheAccessAspect; +import com.microsoft.aad.msal4j.ITokenCacheAccessContext; + +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Paths; + +public class TokenCacheAspect implements ITokenCacheAccessAspect { + + private String data; + + public TokenCacheAspect(String fileName) { + this.data = readDataFromFile(fileName); + } + + @Override + public void beforeCacheAccess(ITokenCacheAccessContext iTokenCacheAccessContext) { + iTokenCacheAccessContext.tokenCache().deserialize(data); + } + + @Override + public void afterCacheAccess(ITokenCacheAccessContext iTokenCacheAccessContext) { + data = iTokenCacheAccessContext.tokenCache().serialize(); + // you could implement logic here to write changes to file here + } + + private static String readDataFromFile(String resource) { + try { + URL path = TokenCacheAspect.class.getResource(resource); + return new String( + Files.readAllBytes( + Paths.get(path.toURI()))); + } catch (Exception ex){ + System.out.println("Error reading data from file"); + throw new RuntimeException(ex); + } + } +} diff --git a/src/samples/cache/sample_cache.json b/src/samples/cache/sample_cache.json new file mode 100644 index 00000000..6334ff43 --- /dev/null +++ b/src/samples/cache/sample_cache.json @@ -0,0 +1,53 @@ +{ + "Account": { + "uid.utid-login.windows.net-contoso": { + "username": "John Doe", + "local_account_id": "object1234", + "realm": "contoso", + "environment": "login.windows.net", + "home_account_id": "uid.utid", + "authority_type": "MSSTS" + } + }, + "RefreshToken": { + "uid.utid-login.windows.net-refreshtoken-my_client_id--s2 s1 s3": { + "target": "s2 s1 s3", + "environment": "login.windows.net", + "credential_type": "RefreshToken", + "secret": "a refresh token", + "client_id": "my_client_id", + "home_account_id": "uid.utid" + } + }, + "AccessToken": { + "uid.utid-login.windows.net-accesstoken-my_client_id-contoso-s2 s1 s3": { + "environment": "login.windows.net", + "credential_type": "AccessToken", + "secret": "an access token", + "realm": "contoso", + "target": "s2 s1 s3", + "client_id": "my_client_id", + "cached_at": "1000", + "home_account_id": "uid.utid", + "extended_expires_on": "4600", + "expires_on": "4600" + } + }, + "IdToken": { + "uid.utid-login.windows.net-idtoken-my_client_id-contoso-": { + "realm": "contoso", + "environment": "login.windows.net", + "credential_type": "IdToken", + "secret": "header.eyJvaWQiOiAib2JqZWN0MTIzNCIsICJwcmVmZXJyZWRfdXNlcm5hbWUiOiAiSm9obiBEb2UiLCAic3ViIjogInN1YiJ9.signature", + "client_id": "my_client_id", + "home_account_id": "uid.utid" + } + }, + "AppMetadata": { + "appmetadata-login.windows.net-my_client_id": { + "environment": "login.windows.net", + "family_id": null, + "client_id": "my_client_id" + } + } +} \ No newline at end of file diff --git a/src/samples/confidential-client/ClientCredentialGrant.java b/src/samples/confidential-client/ClientCredentialGrant.java index a12caa3c..ebdce865 100644 --- a/src/samples/confidential-client/ClientCredentialGrant.java +++ b/src/samples/confidential-client/ClientCredentialGrant.java @@ -5,50 +5,66 @@ import com.microsoft.aad.msal4j.ClientCredentialParameters; import com.microsoft.aad.msal4j.ConfidentialClientApplication; import com.microsoft.aad.msal4j.IAuthenticationResult; +import com.microsoft.aad.msal4j.IClientCredential; +import com.microsoft.aad.msal4j.MsalException; import com.microsoft.aad.msal4j.SilentParameters; import java.util.Collections; -import java.util.concurrent.CompletableFuture; -import java.util.function.BiConsumer; +import java.util.Set; class ClientCredentialGrant { + private final static String CLIENT_ID = ""; + private final static String AUTHORITY = "https://login.microsoftonline.com//"; + private final static String CLIENT_SECRET = ""; + private final static Set SCOPE = Collections.singleton(""); + public static void main(String args[]) throws Exception { - getAccessTokenByClientCredentialGrant(); + IAuthenticationResult result = acquireToken(); + System.out.println("Access token: " + result.accessToken()); } - private static void getAccessTokenByClientCredentialGrant() throws Exception { - - ConfidentialClientApplication app = ConfidentialClientApplication.builder( - TestData.CONFIDENTIAL_CLIENT_ID, - ClientCredentialFactory.createFromSecret(TestData.CONFIDENTIAL_CLIENT_SECRET)) - .authority(TestData.TENANT_SPECIFIC_AUTHORITY) - .build(); + private static IAuthenticationResult acquireToken() throws Exception { - ClientCredentialParameters clientCredentialParam = ClientCredentialParameters.builder( - Collections.singleton(TestData.GRAPH_DEFAULT_SCOPE)) - .build(); + // Load token cache from file and initialize token cache aspect. The token cache will have + // dummy data, so the acquireTokenSilently call will fail. + TokenCacheAspect tokenCacheAspect = new TokenCacheAspect("sample_cache.json"); - CompletableFuture future = app.acquireToken(clientCredentialParam); + // This is the secret that is created in the Azure AD portal + IClientCredential credential = ClientCredentialFactory.createFromSecret(CLIENT_SECRET); + ConfidentialClientApplication cca = + ConfidentialClientApplication + .builder(CLIENT_ID, credential) + .authority(AUTHORITY) + .setTokenCacheAccessAspect(tokenCacheAspect) + .build(); - BiConsumer processAuthResult = (res, ex) -> { - if (ex != null) { - System.out.println("Oops! We have an exception - " + ex.getMessage()); - } - System.out.println("Returned ok - " + res); - System.out.println("Access Token - " + res.accessToken()); - System.out.println("ID Token - " + res.idToken()); - }; + IAuthenticationResult result; + try { + SilentParameters silentParameters = + SilentParameters + .builder(SCOPE) + .build(); - future.whenCompleteAsync(processAuthResult); - future.join(); + // try to acquire token silently. This call will fail since the token cache does not + // have a token for the application you are requesting an access token for + result = cca.acquireTokenSilently(silentParameters).join(); + } catch (Exception ex) { + if (ex.getCause() instanceof MsalException) { - SilentParameters silentParameters = - SilentParameters.builder(Collections.singleton(TestData.GRAPH_DEFAULT_SCOPE)).build(); + ClientCredentialParameters parameters = + ClientCredentialParameters + .builder(SCOPE) + .build(); - future = app.acquireTokenSilently(silentParameters); - - future.whenCompleteAsync(processAuthResult); - future.join(); + // Try to acquire a token. If successful, you should see + // the token information printed out to console + result = cca.acquireToken(parameters).join(); + } else { + // Handle other exceptions accordingly + throw ex; + } + } + return result; } } diff --git a/src/samples/msal-b2c-web-sample/msal-b2c-web-sample.iml b/src/samples/msal-b2c-web-sample/msal-b2c-web-sample.iml deleted file mode 100644 index df646ed7..00000000 --- a/src/samples/msal-b2c-web-sample/msal-b2c-web-sample.iml +++ /dev/null @@ -1,102 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/samples/public-client/B2CFlow.java b/src/samples/public-client/B2CFlow.java deleted file mode 100644 index d8dae7fd..00000000 --- a/src/samples/public-client/B2CFlow.java +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import com.microsoft.aad.msal4j.IAuthenticationResult; -import com.microsoft.aad.msal4j.PublicClientApplication; -import com.microsoft.aad.msal4j.UserNamePasswordParameters; - - -import java.util.Collections; -import java.util.concurrent.CompletableFuture; - -public class B2CFlow { - - public static void main(String args[]) throws Exception { - getAccessTokenFromUserCredentials(); - } - - private static void getAccessTokenFromUserCredentials() throws Exception { - - PublicClientApplication app = PublicClientApplication.builder(TestData.PUBLIC_CLIENT_ID) - .b2cAuthority(TestData.B2C_AUTHORITY) - .build(); - - CompletableFuture future = app.acquireToken( - UserNamePasswordParameters.builder( - Collections.singleton(TestData.LAB_DEFAULT_B2C_SCOPE), - TestData.USER_NAME, - TestData.USER_PASSWORD.toCharArray()).build()); - - future.handle((res, ex) -> { - if(ex != null) { - System.out.println("Oops! We have an exception - " + ex.getMessage()); - return "Unknown!"; - } - System.out.println("Returned ok - " + res); - - System.out.println("Access Token - " + res.accessToken()); - System.out.println("ID Token - " + res.idToken()); - return res; - }).join(); - } -} \ No newline at end of file diff --git a/src/samples/public-client/DeviceCodeFlow.java b/src/samples/public-client/DeviceCodeFlow.java index e928db56..cfc4b26e 100644 --- a/src/samples/public-client/DeviceCodeFlow.java +++ b/src/samples/public-client/DeviceCodeFlow.java @@ -4,42 +4,68 @@ import com.microsoft.aad.msal4j.*; import java.util.Collections; -import java.util.concurrent.CompletableFuture; +import java.util.Set; import java.util.function.Consumer; public class DeviceCodeFlow { + + private final static String CLIENT_ID = ""; + private final static String AUTHORITY = "https://login.microsoftonline.com/common/"; + private final static Set SCOPE = Collections.singleton(""); + public static void main(String args[]) throws Exception { - getAccessTokenByDeviceCodeGrant(); + IAuthenticationResult result = acquireTokenDeviceCode(); + System.out.println("Access token: " + result.accessToken()); + System.out.println("Id token: " + result.idToken()); + System.out.println("Account username: " + result.account().username()); } - private static void getAccessTokenByDeviceCodeGrant() throws Exception { - PublicClientApplication app = PublicClientApplication.builder(TestData.PUBLIC_CLIENT_ID) - .authority(TestData.AUTHORITY_COMMON) + private static IAuthenticationResult acquireTokenDeviceCode() throws Exception { + + // Load token cache from file and initialize token cache aspect. The token cache will have + // dummy data, so the acquireTokenSilently call will fail. + TokenCacheAspect tokenCacheAspect = new TokenCacheAspect("sample_cache.json"); + + PublicClientApplication pca = PublicClientApplication.builder(CLIENT_ID) + .authority(AUTHORITY) + .setTokenCacheAccessAspect(tokenCacheAspect) .build(); - Consumer deviceCodeConsumer = (DeviceCode deviceCode) -> { - System.out.println(deviceCode.message()); - }; - - CompletableFuture future = app.acquireToken( - DeviceCodeFlowParameters.builder( - Collections.singleton(TestData.GRAPH_DEFAULT_SCOPE), - deviceCodeConsumer) - .build()); - - future.handle((res, ex) -> { - if(ex != null) { - System.out.println("Oops! We have an exception of type - " + ex.getClass()); - System.out.println("message - " + ex.getMessage()); - return "Unknown!"; - } - System.out.println("Returned ok - " + res); + Set accountsInCache = pca.getAccounts().join(); + // Take first account in the cache. In a production application, you would filter + // accountsInCache to get the right account for the user authenticating. + IAccount account = accountsInCache.iterator().next(); - System.out.println("Access Token - " + res.accessToken()); - System.out.println("ID Token - " + res.idToken()); - return res; - }); + IAuthenticationResult result; + try { + SilentParameters silentParameters = + SilentParameters + .builder(SCOPE, account) + .build(); - future.join(); + // try to acquire token silently. This call will fail since the token cache + // does not have any data for the user you are trying to acquire a token for + result = pca.acquireTokenSilently(silentParameters).join(); + } catch (Exception ex) { + if (ex.getCause() instanceof MsalException) { + + Consumer deviceCodeConsumer = (DeviceCode deviceCode) -> + System.out.println(deviceCode.message()); + + DeviceCodeFlowParameters parameters = + DeviceCodeFlowParameters + .builder(SCOPE, deviceCodeConsumer) + .build(); + + // Try to acquire a token via device code flow. If successful, you should see + // the token and account information printed out to console, and the sample_cache.json + // file should have been updated with the latest tokens. + result = pca.acquireToken(parameters).join(); + } else { + // Handle other exceptions accordingly + throw ex; + } + } + return result; } } diff --git a/src/samples/public-client/IntegratedWindowsAuthFlow.java b/src/samples/public-client/IntegratedWindowsAuthFlow.java deleted file mode 100644 index 342196c1..00000000 --- a/src/samples/public-client/IntegratedWindowsAuthFlow.java +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import com.microsoft.aad.msal4j.IAuthenticationResult; -import com.microsoft.aad.msal4j.IntegratedWindowsAuthenticationParameters; -import com.microsoft.aad.msal4j.PublicClientApplication; - -import java.util.Collections; -import java.util.concurrent.Future; - -public class IntegratedWindowsAuthFlow { - public static void main(String args[]) throws Exception { - - IAuthenticationResult result = getAccessTokenByIntegratedAuth(); - - System.out.println("Access Token - " + result.accessToken()); - System.out.println("ID Token - " + result.idToken()); - } - - private static IAuthenticationResult getAccessTokenByIntegratedAuth() throws Exception { - PublicClientApplication app = PublicClientApplication.builder(TestData.PUBLIC_CLIENT_ID) - .authority(TestData.AUTHORITY_ORGANIZATION) - .build(); - - IntegratedWindowsAuthenticationParameters parameters = - IntegratedWindowsAuthenticationParameters.builder( - Collections.singleton(TestData.GRAPH_DEFAULT_SCOPE), TestData.USER_NAME) - .build(); - - Future future = app.acquireToken(parameters); - - IAuthenticationResult result = future.get(); - - return result; - } -} diff --git a/src/samples/public-client/IntegratedWindowsAuthenticationFlow.java b/src/samples/public-client/IntegratedWindowsAuthenticationFlow.java new file mode 100644 index 00000000..61faddc7 --- /dev/null +++ b/src/samples/public-client/IntegratedWindowsAuthenticationFlow.java @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import com.microsoft.aad.msal4j.IAccount; +import com.microsoft.aad.msal4j.IAuthenticationResult; +import com.microsoft.aad.msal4j.IntegratedWindowsAuthenticationParameters; +import com.microsoft.aad.msal4j.MsalException; +import com.microsoft.aad.msal4j.PublicClientApplication; +import com.microsoft.aad.msal4j.SilentParameters; + +import java.util.Collections; +import java.util.Set; + +public class IntegratedWindowsAuthenticationFlow { + + private final static String CLIENT_ID = ""; + private final static String AUTHORITY = "https://login.microsoftonline.com/organizations/"; + private final static Set SCOPE = Collections.singleton(""); + private final static String USER_NAME = ""; + + public static void main(String args[]) throws Exception { + + IAuthenticationResult result = acquireTokenIwa(); + System.out.println("Access token: " + result.accessToken()); + System.out.println("Id token: " + result.idToken()); + System.out.println("Account username: " + result.account().username()); + } + + private static IAuthenticationResult acquireTokenIwa() throws Exception { + + // Load token cache from file and initialize token cache aspect. The token cache will have + // dummy data, so the acquireTokenSilently call will fail. + TokenCacheAspect tokenCacheAspect = new TokenCacheAspect("sample_cache.json"); + + PublicClientApplication pca = PublicClientApplication.builder(CLIENT_ID) + .authority(AUTHORITY) + .setTokenCacheAccessAspect(tokenCacheAspect) + .build(); + + Set accountsInCache = pca.getAccounts().join(); + // Take first account in the cache. In a production application, you would filter + // accountsInCache to get the right account for the user authenticating. + IAccount account = accountsInCache.iterator().next(); + + IAuthenticationResult result; + try { + SilentParameters silentParameters = + SilentParameters + .builder(SCOPE, account) + .build(); + + // try to acquire token silently. This call will fail since the token cache + // does not have any data for the user you are trying to acquire a token for + result = pca.acquireTokenSilently(silentParameters).join(); + } catch (Exception ex) { + if (ex.getCause() instanceof MsalException) { + + IntegratedWindowsAuthenticationParameters parameters = + IntegratedWindowsAuthenticationParameters + .builder(SCOPE, USER_NAME) + .build(); + + // Try to acquire a IWA. You will need to generate a Kerberos ticket. + // If successful, you should see the token and account information printed out to + // console + result = pca.acquireToken(parameters).join(); + } else { + // Handle other exceptions accordingly + throw ex; + } + } + return result; + } +} diff --git a/src/samples/public-client/InteractiveFlow.java b/src/samples/public-client/InteractiveFlow.java index ff307afd..806e2056 100644 --- a/src/samples/public-client/InteractiveFlow.java +++ b/src/samples/public-client/InteractiveFlow.java @@ -1,34 +1,72 @@ // Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. +//// Licensed under the MIT License. +import com.microsoft.aad.msal4j.IAccount; import com.microsoft.aad.msal4j.IAuthenticationResult; import com.microsoft.aad.msal4j.InteractiveRequestParameters; +import com.microsoft.aad.msal4j.MsalException; import com.microsoft.aad.msal4j.PublicClientApplication; +import com.microsoft.aad.msal4j.SilentParameters; import java.net.URI; import java.util.Collections; +import java.util.Set; public class InteractiveFlow { + + private final static String CLIENT_ID = ""; + private final static String AUTHORITY = ""; + private final static Set SCOPE = Collections.singleton(""); + public static void main(String[] args) throws Exception{ - IAuthenticationResult result = getAccessTokenByInteractiveFlow(); - System.out.println(result.accessToken()); - System.out.println(result.account()); - System.out.println(result.idToken()); + IAuthenticationResult result = acquireTokenInteractive(); + System.out.println("Access token: " + result.accessToken()); + System.out.println("Id token: " + result.idToken()); + System.out.println("Account username: " + result.account().username()); } - private static IAuthenticationResult getAccessTokenByInteractiveFlow() throws Exception { + private static IAuthenticationResult acquireTokenInteractive() throws Exception { - PublicClientApplication publicClientApplication = PublicClientApplication - .builder(TestData.PUBLIC_CLIENT_ID) - .authority(TestData.TENANT_SPECIFIC_AUTHORITY) - .build(); + // Load token cache from file and initialize token cache aspect. The token cache will have + // dummy data, so the acquireTokenSilently call will fail. + TokenCacheAspect tokenCacheAspect = new TokenCacheAspect("sample_cache.json"); - InteractiveRequestParameters parameters = InteractiveRequestParameters.builder() - .redirectUri(new URI("http://localhost:8080")) - //.scopes(Collections.singleton(TestData.testScope)) + PublicClientApplication pca = PublicClientApplication.builder(CLIENT_ID) + .authority(AUTHORITY) + .setTokenCacheAccessAspect(tokenCacheAspect) .build(); - IAuthenticationResult result = publicClientApplication.acquireToken(parameters).join(); + Set accountsInCache = pca.getAccounts().join(); + // Take first account in the cache. In a production application, you would filter + // accountsInCache to get the right account for the user authenticating. + IAccount account = accountsInCache.iterator().next(); + + IAuthenticationResult result; + try { + SilentParameters silentParameters = + SilentParameters + .builder(SCOPE, account) + .build(); + + // try to acquire token silently. This call will fail since the token cache + // does not have any data for the user you are trying to acquire a token for + result = pca.acquireTokenSilently(silentParameters).join(); + } catch (Exception ex) { + if (ex.getCause() instanceof MsalException) { + + InteractiveRequestParameters parameters = InteractiveRequestParameters + .builder(new URI("http://localhost")) + .scopes(SCOPE) + .build(); + + // Try to acquire a token interactively with system browser. If successful, you should see + // the token and account information printed out to console + result = pca.acquireToken(parameters).join(); + } else { + // Handle other exceptions accordingly + throw ex; + } + } return result; } } \ No newline at end of file diff --git a/src/samples/public-client/InteractiveFlowB2C.java b/src/samples/public-client/InteractiveFlowB2C.java new file mode 100644 index 00000000..8c405498 --- /dev/null +++ b/src/samples/public-client/InteractiveFlowB2C.java @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import com.microsoft.aad.msal4j.IAccount; +import com.microsoft.aad.msal4j.IAuthenticationResult; +import com.microsoft.aad.msal4j.InteractiveRequestParameters; +import com.microsoft.aad.msal4j.MsalException; +import com.microsoft.aad.msal4j.PublicClientApplication; +import com.microsoft.aad.msal4j.SilentParameters; + +import java.net.URI; +import java.util.Collections; +import java.util.Set; + +public class InteractiveFlowB2C { + + private final static String CLIENT_ID = ""; + private final static String AUTHORITY = "https://.b2clogin.com/tfp//.com/"; + private final static Set SCOPE = Collections.singleton(""); + + public static void main(String args[]) throws Exception { + IAuthenticationResult result = acquireTokenInteractiveB2C(); + System.out.println("Access token: " + result.accessToken()); + System.out.println("Id token: " + result.idToken()); + System.out.println("Account username: " + result.account().username()); + } + + private static IAuthenticationResult acquireTokenInteractiveB2C() throws Exception { + + // Load token cache from file and initialize token cache aspect. The token cache will have + // dummy data, so the acquireTokenSilently call will fail. + TokenCacheAspect tokenCacheAspect = new TokenCacheAspect("sample_cache.json"); + + PublicClientApplication pca = + PublicClientApplication + .builder(CLIENT_ID) + .b2cAuthority(AUTHORITY) + .setTokenCacheAccessAspect(tokenCacheAspect) + .build(); + + Set accountsInCache = pca.getAccounts().join(); + // Use first account in the cache. In a production application, you would filter + // accountsInCache to get the right account for the user authenticating. + IAccount account = accountsInCache.iterator().next(); + + IAuthenticationResult result; + try { + SilentParameters silentParameters = + SilentParameters + .builder(SCOPE, account) + .build(); + + // try to acquire token silently. This call will fail since the token cache + // does not have any data for the user you are trying to acquire a token for + result = pca.acquireTokenSilently(silentParameters).join(); + } catch (Exception ex) { + if (ex.getCause() instanceof MsalException) { + + // For B2C, you have to specify a port for the redirect URL + InteractiveRequestParameters parameters = InteractiveRequestParameters + .builder(new URI("http://localhost:8080")) + .scopes(SCOPE) + .build(); + + // Try to acquire a token interactively with system browser. If successful, you should see + // the token and account information printed out to console + result = pca.acquireToken(parameters).join(); + } else { + // Handle other exceptions accordingly + throw ex; + } + } + return result; + } +} \ No newline at end of file diff --git a/src/samples/public-client/TestData.java b/src/samples/public-client/TestData.java deleted file mode 100644 index af70e5b9..00000000 --- a/src/samples/public-client/TestData.java +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -public class TestData { - - final static String TENANT_SPECIFIC_AUTHORITY = "https://login.microsoftonline.com//"; - - final static String AUTHORITY_COMMON = "https://login.microsoftonline.com/common/"; - final static String AUTHORITY_ORGANIZATION = "https://login.microsoftonline.com/organizations/"; - final static String B2C_AUTHORITY = "https://.b2clogin.com/tfp//.com/"; - - final static String PUBLIC_CLIENT_ID = ""; - - final static String GRAPH_DEFAULT_SCOPE = "https://graph.windows.net/.default"; - final static String LAB_DEFAULT_B2C_SCOPE = ""; - - final static String USER_NAME = ""; - final static String USER_PASSWORD = ""; - - final static String CONFIDENTIAL_CLIENT_ID = ""; - final static String CONFIDENTIAL_CLIENT_SECRET = ""; -} diff --git a/src/samples/public-client/UsernamePasswordFlow.java b/src/samples/public-client/UsernamePasswordFlow.java index ce9f47da..aaa55a53 100644 --- a/src/samples/public-client/UsernamePasswordFlow.java +++ b/src/samples/public-client/UsernamePasswordFlow.java @@ -1,65 +1,72 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import com.microsoft.aad.msal4j.*; +import com.microsoft.aad.msal4j.IAccount; +import com.microsoft.aad.msal4j.IAuthenticationResult; +import com.microsoft.aad.msal4j.MsalException; +import com.microsoft.aad.msal4j.PublicClientApplication; +import com.microsoft.aad.msal4j.SilentParameters; +import com.microsoft.aad.msal4j.UserNamePasswordParameters; -import java.net.MalformedURLException; -import java.util.Collection; import java.util.Collections; -import java.util.concurrent.CompletableFuture; +import java.util.Set; public class UsernamePasswordFlow { + private final static String CLIENT_ID = ""; + private final static String AUTHORITY = "https://login.microsoftonline.com/organizations/"; + private final static Set SCOPE = Collections.singleton(""); + private final static String USER_NAME = ""; + private final static String USER_PASSWORD = ""; + public static void main(String args[]) throws Exception { - getAccessTokenFromUserCredentials(); - } - private static void getAccessTokenFromUserCredentials() throws Exception { - PublicClientApplication app = PublicClientApplication.builder(TestData.PUBLIC_CLIENT_ID) - .authority(TestData.AUTHORITY_ORGANIZATION) - .build(); + IAuthenticationResult result = acquireTokenUsernamePassword(); + System.out.println("Access token: " + result.accessToken()); + System.out.println("Id token: " + result.idToken()); + System.out.println("Account username: " + result.account().username()); + } - UserNamePasswordParameters parameters = UserNamePasswordParameters.builder( - Collections.singleton(TestData.GRAPH_DEFAULT_SCOPE), - TestData.USER_NAME, - TestData.USER_PASSWORD.toCharArray()) - .build(); + private static IAuthenticationResult acquireTokenUsernamePassword() throws Exception { - CompletableFuture future = app.acquireToken(parameters); + // Load token cache from file and initialize token cache aspect. The token cache will have + // dummy data, so the acquireTokenSilently call will fail. + TokenCacheAspect tokenCacheAspect = new TokenCacheAspect("sample_cache.json"); - future.handle((res, ex) -> { - if(ex != null) { - System.out.println("Oops! We have an exception - " + ex.getMessage()); - return "Unknown!"; - } + PublicClientApplication pca = PublicClientApplication.builder(CLIENT_ID) + .authority(AUTHORITY) + .setTokenCacheAccessAspect(tokenCacheAspect) + .build(); - Collection accounts = app.getAccounts().join(); + Set accountsInCache = pca.getAccounts().join(); + // Take first account in the cache. In a production application, you would filter + // accountsInCache to get the right account for the user authenticating. + IAccount account = accountsInCache.iterator().next(); - CompletableFuture future1; - try { - future1 = app.acquireTokenSilently - (SilentParameters.builder(Collections.singleton(TestData.GRAPH_DEFAULT_SCOPE), - accounts.iterator().next()) - .forceRefresh(true) - .build()); + IAuthenticationResult result; + try { + SilentParameters silentParameters = + SilentParameters + .builder(SCOPE, account) + .build(); + // try to acquire token silently. This call will fail since the token cache + // does not have any data for the user you are trying to acquire a token for + result = pca.acquireTokenSilently(silentParameters).join(); + } catch (Exception ex) { + if (ex.getCause() instanceof MsalException) { - } catch (MalformedURLException e) { - e.printStackTrace(); - throw new RuntimeException(); + UserNamePasswordParameters parameters = + UserNamePasswordParameters + .builder(SCOPE, USER_NAME, USER_PASSWORD.toCharArray()) + .build(); + // Try to acquire a token via username/password. If successful, you should see + // the token and account information printed out to console + result = pca.acquireToken(parameters).join(); + } else { + // Handle other exceptions accordingly + throw ex; } - - future1.join(); - - IAccount account = app.getAccounts().join().iterator().next(); - app.removeAccount(account).join(); - accounts = app.getAccounts().join(); - - System.out.println("Num of account - " + accounts.size()); - System.out.println("Returned ok - " + res); - System.out.println("Access Token - " + res.accessToken()); - System.out.println("ID Token - " + res.idToken()); - return res; - }).join(); - + } + return result; } } From b59188ab1566b2f07c359d720b31284476e03ffd Mon Sep 17 00:00:00 2001 From: sgonzalezMSFT Date: Tue, 18 Feb 2020 13:03:52 -0800 Subject: [PATCH 18/24] Remove static cache. Keep telemetry per app object. --- .../msal4j/AcquireTokenSilentSupplier.java | 2 +- .../msal4j/AuthenticationResultSupplier.java | 2 +- .../aad/msal4j/ServerSideTelemetry.java | 13 ++--- .../aad/msal4j/TokenRequestExecutor.java | 6 +++ .../aad/msal4j/ServerTelemetryTests.java | 47 ++++++++++--------- 5 files changed, 40 insertions(+), 30 deletions(-) diff --git a/src/main/java/com/microsoft/aad/msal4j/AcquireTokenSilentSupplier.java b/src/main/java/com/microsoft/aad/msal4j/AcquireTokenSilentSupplier.java index b7481efd..c6c6d16e 100644 --- a/src/main/java/com/microsoft/aad/msal4j/AcquireTokenSilentSupplier.java +++ b/src/main/java/com/microsoft/aad/msal4j/AcquireTokenSilentSupplier.java @@ -58,7 +58,7 @@ AuthenticationResult execute() throws Exception { throw new MsalClientException(AuthenticationErrorMessage.NO_TOKEN_IN_CACHE, AuthenticationErrorCode.CACHE_MISS); } - ServerSideTelemetry.incrementSilentSuccessfulCount(); + clientApplication.getServiceBundle().getServerSideTelemetry().incrementSilentSuccessfulCount(); return res; } } diff --git a/src/main/java/com/microsoft/aad/msal4j/AuthenticationResultSupplier.java b/src/main/java/com/microsoft/aad/msal4j/AuthenticationResultSupplier.java index f41c9790..8aa48211 100644 --- a/src/main/java/com/microsoft/aad/msal4j/AuthenticationResultSupplier.java +++ b/src/main/java/com/microsoft/aad/msal4j/AuthenticationResultSupplier.java @@ -80,7 +80,7 @@ public IAuthenticationResult get() { } } - ServerSideTelemetry.addFailedRequestTelemetry( + clientApplication.getServiceBundle().getServerSideTelemetry().addFailedRequestTelemetry( String.valueOf(msalRequest.requestContext().publicApi().getApiId()), msalRequest.requestContext().correlationId(), error); diff --git a/src/main/java/com/microsoft/aad/msal4j/ServerSideTelemetry.java b/src/main/java/com/microsoft/aad/msal4j/ServerSideTelemetry.java index 653e02d5..45748a2d 100644 --- a/src/main/java/com/microsoft/aad/msal4j/ServerSideTelemetry.java +++ b/src/main/java/com/microsoft/aad/msal4j/ServerSideTelemetry.java @@ -17,9 +17,10 @@ class ServerSideTelemetry { private final static String LAST_REQUEST_HEADER_NAME = "x-client-last-telemetry"; private CurrentRequest currentRequest; + private AtomicInteger silentSuccessfulCount = new AtomicInteger(0); - static AtomicInteger silentSuccessfulCount = new AtomicInteger(0); - static ConcurrentMap previousRequests = new ConcurrentHashMap<>(); + ConcurrentMap previousRequests = new ConcurrentHashMap<>(); + ConcurrentMap previousRequestInProgress = new ConcurrentHashMap<>(); synchronized Map getServerTelemetryHeaderMap(){ Map headerMap = new HashMap<>(); @@ -30,7 +31,7 @@ synchronized Map getServerTelemetryHeaderMap(){ return headerMap; } - static void addFailedRequestTelemetry(String publicApiId, String correlationId, String error){ + void addFailedRequestTelemetry(String publicApiId, String correlationId, String error){ String[] previousRequest = new String[]{publicApiId, error}; previousRequests.put( @@ -38,7 +39,7 @@ static void addFailedRequestTelemetry(String publicApiId, String correlationId, previousRequest); } - static void incrementSilentSuccessfulCount(){ + void incrementSilentSuccessfulCount(){ silentSuccessfulCount.incrementAndGet(); } @@ -91,8 +92,7 @@ private synchronized String buildLastRequestHeader() { // Total header size should be less than 8kb. At max, we will use 4kb for telemetry. while (it.hasNext() - && (middleSegmentBuilder.length() + errorSegmentBuilder.length()) < 3800) - { + && (middleSegmentBuilder.length() + errorSegmentBuilder.length()) < 3800) { String correlationId = it.next(); String[] previousRequest = previousRequests.get(correlationId); String apiId = (String)Array.get(previousRequest, 0); @@ -106,6 +106,7 @@ private synchronized String buildLastRequestHeader() { errorSegmentBuilder.append(SCHEMA_COMMA_DELIMITER); } + previousRequestInProgress.put(correlationId, previousRequest); it.remove(); } diff --git a/src/main/java/com/microsoft/aad/msal4j/TokenRequestExecutor.java b/src/main/java/com/microsoft/aad/msal4j/TokenRequestExecutor.java index 9bfddf1b..72225956 100644 --- a/src/main/java/com/microsoft/aad/msal4j/TokenRequestExecutor.java +++ b/src/main/java/com/microsoft/aad/msal4j/TokenRequestExecutor.java @@ -121,6 +121,12 @@ else if(type == AuthorityType.ADFS){ build(); } else { + // http codes indicating that STS did not log request + if(oauthHttpResponse.getStatusCode() == 429 || oauthHttpResponse.getStatusCode() >= 500){ + serviceBundle.getServerSideTelemetry().previousRequests.putAll( + serviceBundle.getServerSideTelemetry().previousRequestInProgress); + } + throw MsalServiceExceptionFactory.fromHttpResponse(oauthHttpResponse); } return result; diff --git a/src/test/java/com/microsoft/aad/msal4j/ServerTelemetryTests.java b/src/test/java/com/microsoft/aad/msal4j/ServerTelemetryTests.java index 8f71c51c..a4e943d6 100644 --- a/src/test/java/com/microsoft/aad/msal4j/ServerTelemetryTests.java +++ b/src/test/java/com/microsoft/aad/msal4j/ServerTelemetryTests.java @@ -1,7 +1,6 @@ package com.microsoft.aad.msal4j; import org.testng.Assert; -import org.testng.annotations.AfterMethod; import org.testng.annotations.Test; import java.nio.charset.StandardCharsets; @@ -10,12 +9,9 @@ import java.util.Map; import java.util.Random; import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.concurrent.atomic.AtomicInteger; -@Test(groups = { "checkin" }) public class ServerTelemetryTests { private static final String SCHEMA_VERSION = "2"; @@ -25,12 +21,6 @@ public class ServerTelemetryTests { private final static String PUBLIC_API_ID = String.valueOf(PublicApi.ACQUIRE_TOKEN_BY_AUTHORIZATION_CODE.getApiId()); private final static String ERROR = "invalid_grant"; - @AfterMethod - public void resetLastRequestHeader(){ - ServerSideTelemetry.previousRequests = new ConcurrentHashMap<>(); - ServerSideTelemetry.silentSuccessfulCount = new AtomicInteger(0); - } - @Test public void serverTelemetryHeaders_correctSchema(){ @@ -41,14 +31,14 @@ public void serverTelemetryHeaders_correctSchema(){ serverSideTelemetry.setCurrentRequest(currentRequest); String correlationId = "936732c6-74b9-4783-aad9-fa205eae8763"; - ServerSideTelemetry.addFailedRequestTelemetry(PUBLIC_API_ID, correlationId, ERROR); + serverSideTelemetry.addFailedRequestTelemetry(PUBLIC_API_ID, correlationId, ERROR); Map headers = serverSideTelemetry.getServerTelemetryHeaderMap(); //Current request tests List currentRequestHeader = Arrays.asList(headers.get(CURRENT_REQUEST_HEADER_NAME).split("\\|")); - // ["2", "831,false"] + // ["2", "831, false"] Assert.assertEquals(currentRequestHeader.size(), 2); Assert.assertEquals(currentRequestHeader.get(0), SCHEMA_VERSION); @@ -76,11 +66,11 @@ public void serverTelemetryHeaders_correctSchema(){ @Test public void serverTelemetryHeaders_previewsRequestNull(){ + ServerSideTelemetry serverSideTelemetry = new ServerSideTelemetry(); for(int i = 0; i < 3; i++){ - ServerSideTelemetry.incrementSilentSuccessfulCount(); + serverSideTelemetry.incrementSilentSuccessfulCount(); } - ServerSideTelemetry serverSideTelemetry = new ServerSideTelemetry(); Map headers = serverSideTelemetry.getServerTelemetryHeaderMap(); Assert.assertEquals(headers.get(LAST_REQUEST_HEADER_NAME), "2|3|||"); @@ -93,7 +83,7 @@ public void serverTelemetryHeader_testMaximumHeaderSize(){ for(int i = 0; i <100; i++){ String correlationId = UUID.randomUUID().toString(); - ServerSideTelemetry.addFailedRequestTelemetry(PUBLIC_API_ID, correlationId, ERROR); + serverSideTelemetry.addFailedRequestTelemetry(PUBLIC_API_ID, correlationId, ERROR); } Map headers = serverSideTelemetry.getServerTelemetryHeaderMap(); @@ -109,20 +99,19 @@ public void serverTelemetryHeader_testMaximumHeaderSize(){ @Test public void serverTelemetryHeaders_multipleThreadsWrite(){ + ServerSideTelemetry serverSideTelemetry = new ServerSideTelemetry(); ExecutorService executor = Executors.newFixedThreadPool(10); try{ - for ( int i=0; i < 10; i++){ - executor.execute(new FailedRequestRunnable()); - executor.execute(new SilentSuccessfulRequestRunnable()); + for (int i=0; i < 10; i++){ + executor.execute(new FailedRequestRunnable(serverSideTelemetry)); + executor.execute(new SilentSuccessfulRequestRunnable(serverSideTelemetry)); } } catch (Exception ex){ ex.printStackTrace(); } executor.shutdown(); - ServerSideTelemetry serverSideTelemetry = new ServerSideTelemetry(); - try{ Thread.sleep( 1000); } catch(InterruptedException ex){ @@ -144,6 +133,13 @@ public void serverTelemetryHeaders_multipleThreadsWrite(){ class FailedRequestRunnable implements Runnable { + ServerSideTelemetry telemetry; + + FailedRequestRunnable(ServerSideTelemetry telemetry){ + this.telemetry = telemetry; + } + + @Override public void run(){ Random rand = new Random(); @@ -154,14 +150,21 @@ public void run(){ ex.printStackTrace(); } String correlationId = UUID.randomUUID().toString(); - ServerSideTelemetry.addFailedRequestTelemetry(PUBLIC_API_ID, correlationId, ERROR); + telemetry.addFailedRequestTelemetry(PUBLIC_API_ID, correlationId, ERROR); } } class SilentSuccessfulRequestRunnable implements Runnable { + ServerSideTelemetry telemetry; + + SilentSuccessfulRequestRunnable(ServerSideTelemetry telemetry){ + this.telemetry = telemetry; + } + + @Override public void run(){ - ServerSideTelemetry.incrementSilentSuccessfulCount(); + telemetry.incrementSilentSuccessfulCount(); } } } From 7e4957253dcc41f9adcf927f5aec76a969ead218 Mon Sep 17 00:00:00 2001 From: sgonzalezMSFT Date: Tue, 18 Feb 2020 15:28:35 -0800 Subject: [PATCH 19/24] Fix failing test --- .../java/com/microsoft/aad/msal4j/TokenRequestExecutorTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/microsoft/aad/msal4j/TokenRequestExecutorTest.java b/src/test/java/com/microsoft/aad/msal4j/TokenRequestExecutorTest.java index 56cf1f4a..d6ece15c 100644 --- a/src/test/java/com/microsoft/aad/msal4j/TokenRequestExecutorTest.java +++ b/src/test/java/com/microsoft/aad/msal4j/TokenRequestExecutorTest.java @@ -298,7 +298,7 @@ public void testExecuteOAuth_Failure() throws SerializeException, .andReturn(msalOAuthHttpRequest).times(1); EasyMock.expect(msalOAuthHttpRequest.send()).andReturn(httpResponse) .times(1); - EasyMock.expect(httpResponse.getStatusCode()).andReturn(402).times(2); + EasyMock.expect(httpResponse.getStatusCode()).andReturn(402).times(3); EasyMock.expect(httpResponse.getStatusMessage()).andReturn("403 Forbidden"); EasyMock.expect(httpResponse.getHeaderMap()).andReturn(new HashMap<>()); EasyMock.expect(httpResponse.getContent()).andReturn(TestConfiguration.HTTP_ERROR_RESPONSE); From ff3644a6a76e2a96870302c44899d308d7824ca3 Mon Sep 17 00:00:00 2001 From: sgonzalezMSFT Date: Thu, 20 Feb 2020 11:14:16 -0800 Subject: [PATCH 20/24] PR feedback --- .../AcquireTokenByInteractiveFlowSupplier.java | 11 ++++++++--- .../aad/msal4j/AuthenticationErrorCode.java | 13 +++++++++---- .../java/com/microsoft/aad/msal4j/HttpListener.java | 11 +++++++++-- .../aad/msal4j/InteractiveRequestParameters.java | 5 +---- 4 files changed, 27 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByInteractiveFlowSupplier.java b/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByInteractiveFlowSupplier.java index b8216d63..768dccdc 100644 --- a/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByInteractiveFlowSupplier.java +++ b/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByInteractiveFlowSupplier.java @@ -7,6 +7,7 @@ import org.slf4j.LoggerFactory; import java.awt.*; +import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; @@ -110,15 +111,20 @@ private void openDefaultSystemBrowser(URL url){ try{ if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) { Desktop.getDesktop().browse(url.toURI()); + LOG.debug("Opened default system browser"); + } else { + throw new MsalClientException("Unable to open default system browser", + AuthenticationErrorCode.DESKTOP_BROWSER_NOT_SUPPORTED); } - } catch(Exception e){ - throw new MsalClientException(e); + } catch(URISyntaxException | IOException ex){ + throw new MsalClientException(ex); } } private AuthorizationResult getAuthorizationResultFromHttpListener(){ AuthorizationResult result = null; try { + LOG.debug("Listening for authorization result"); long expirationTime = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()) + 120; while(result == null && !interactiveRequest.futureReference().get().isCancelled() && @@ -139,7 +145,6 @@ private AuthorizationResult getAuthorizationResultFromHttpListener(){ private AuthenticationResult acquireTokenWithAuthorizationCode(AuthorizationResult authorizationResult) throws Exception{ - AuthorizationCodeParameters parameters = AuthorizationCodeParameters .builder(authorizationResult.code(), interactiveRequest.interactiveRequestParameters().redirectUri()) .scopes(interactiveRequest.interactiveRequestParameters().scopes()) diff --git a/src/main/java/com/microsoft/aad/msal4j/AuthenticationErrorCode.java b/src/main/java/com/microsoft/aad/msal4j/AuthenticationErrorCode.java index b4ced34d..c53d7d27 100644 --- a/src/main/java/com/microsoft/aad/msal4j/AuthenticationErrorCode.java +++ b/src/main/java/com/microsoft/aad/msal4j/AuthenticationErrorCode.java @@ -58,19 +58,19 @@ public class AuthenticationErrorCode { public final static String UNKNOWN = "unknown"; /** - * The current redirect URL is not a loopback URL. To use OS browser, a loopback URL must be + * The current redirect URI is not a loopback URL. To use the OS browser, a loopback URL must be * configured both during app registration as well as when initializing the InteractiveRequestParameters * object */ public final static String LOOPBACK_REDIRECT_URI = "loopback_redirect_uri"; /** - * Unable to start Http listener to the specified port. This might be because the port is unable. + * Unable to start Http listener to the specified port. This might be because the port is busy. */ public final static String UNABLE_TO_START_HTTP_LISTENER = "unable_to_start_http_listener"; /** - * Authorization result response is invalid, either because is valid or it does not contain + * Authorization result response is invalid, either because format is invalid or it does not contain * an authorization code. */ public final static String INVALID_AUTHORIZATION_RESULT = "invalid_authorization_result"; @@ -79,5 +79,10 @@ public class AuthenticationErrorCode { * Redirect URI provided to MSAL is of invalid format. Redirect URL must be a loopback URL. */ public final static String INVALID_REDIRECT_URI = "invalid_redirect_uri"; -} + /** + * MSAL was unable to open the user-default browser. This is either because the current platform + * does not support {@link java.awt.Desktop} or {@link java.awt.Desktop.Action#BROWSE} + */ + public final static String DESKTOP_BROWSER_NOT_SUPPORTED = "desktop_browser_not_supported"; +} diff --git a/src/main/java/com/microsoft/aad/msal4j/HttpListener.java b/src/main/java/com/microsoft/aad/msal4j/HttpListener.java index 81fe5fdb..7228c0c2 100644 --- a/src/main/java/com/microsoft/aad/msal4j/HttpListener.java +++ b/src/main/java/com/microsoft/aad/msal4j/HttpListener.java @@ -8,12 +8,16 @@ import lombok.AccessLevel; import lombok.Getter; import lombok.experimental.Accessors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.net.InetSocketAddress; @Accessors(fluent = true) class HttpListener { + private final static Logger LOG = LoggerFactory.getLogger(HttpListener.class); + private HttpServer server; @Getter(AccessLevel.PACKAGE) @@ -25,15 +29,18 @@ void startListener(int port, HttpHandler httpHandler) { server.createContext("/", httpHandler); this.port = server.getAddress().getPort(); server.start(); - + LOG.debug("Http listener started. Listening on port: " + port); } catch (Exception e){ - throw new MsalClientException(e); + throw new MsalClientException(e.getMessage(), + AuthenticationErrorCode.UNABLE_TO_START_HTTP_LISTENER); } } void stopListener(){ if(server != null){ server.stop(0); + LOG.debug("Http listener stopped"); + } } } diff --git a/src/main/java/com/microsoft/aad/msal4j/InteractiveRequestParameters.java b/src/main/java/com/microsoft/aad/msal4j/InteractiveRequestParameters.java index 6a632a23..bf9578cf 100644 --- a/src/main/java/com/microsoft/aad/msal4j/InteractiveRequestParameters.java +++ b/src/main/java/com/microsoft/aad/msal4j/InteractiveRequestParameters.java @@ -12,11 +12,8 @@ import lombok.experimental.Accessors; import java.net.URI; -import java.net.URL; import java.util.Set; -import static com.microsoft.aad.msal4j.ParameterValidationUtils.validateNotBlank; -import static com.microsoft.aad.msal4j.ParameterValidationUtils.validateNotEmpty; import static com.microsoft.aad.msal4j.ParameterValidationUtils.validateNotNull; /** @@ -31,7 +28,7 @@ public class InteractiveRequestParameters { /** * Redirect URI where MSAL will listen to for the authorization code returned by Azure AD. - * Should be a loopback URL with a port specified (for example, http://localhost:3671). If no + * Should be a loopback address with a port specified (for example, http://localhost:3671). If no * port is specified, MSAL will find an open port. For more information, see * https://aka.ms/msal4j-interactive-request. */ From 608a899ccc48761d88489e67e0b4127574548157 Mon Sep 17 00:00:00 2001 From: sgonzalezMSFT Date: Thu, 20 Feb 2020 11:19:16 -0800 Subject: [PATCH 21/24] Fix comment --- src/samples/cache/TokenCacheAspect.java | 4 ++-- src/samples/confidential-client/ClientCredentialGrant.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/samples/cache/TokenCacheAspect.java b/src/samples/cache/TokenCacheAspect.java index 998f52df..cd26f54e 100644 --- a/src/samples/cache/TokenCacheAspect.java +++ b/src/samples/cache/TokenCacheAspect.java @@ -24,7 +24,7 @@ public void beforeCacheAccess(ITokenCacheAccessContext iTokenCacheAccessContext) @Override public void afterCacheAccess(ITokenCacheAccessContext iTokenCacheAccessContext) { data = iTokenCacheAccessContext.tokenCache().serialize(); - // you could implement logic here to write changes to file here + // you could implement logic here to write changes to file } private static String readDataFromFile(String resource) { @@ -34,7 +34,7 @@ private static String readDataFromFile(String resource) { Files.readAllBytes( Paths.get(path.toURI()))); } catch (Exception ex){ - System.out.println("Error reading data from file"); + System.out.println("Error reading data from file: " + ex.getMessage()); throw new RuntimeException(ex); } } diff --git a/src/samples/confidential-client/ClientCredentialGrant.java b/src/samples/confidential-client/ClientCredentialGrant.java index ebdce865..a510cd47 100644 --- a/src/samples/confidential-client/ClientCredentialGrant.java +++ b/src/samples/confidential-client/ClientCredentialGrant.java @@ -30,7 +30,7 @@ private static IAuthenticationResult acquireToken() throws Exception { // dummy data, so the acquireTokenSilently call will fail. TokenCacheAspect tokenCacheAspect = new TokenCacheAspect("sample_cache.json"); - // This is the secret that is created in the Azure AD portal + // This is the secret that is created in the Azure portal when registering the application IClientCredential credential = ClientCredentialFactory.createFromSecret(CLIENT_SECRET); ConfidentialClientApplication cca = ConfidentialClientApplication From ef3078b7af247e51f093d81d0420e72154193dec Mon Sep 17 00:00:00 2001 From: sgonzalezMSFT Date: Thu, 20 Feb 2020 14:58:23 -0800 Subject: [PATCH 22/24] Update JavaDoc comments --- .../aad/msal4j/AuthorizationCodeParameters.java | 13 ++++++++++++- .../microsoft/aad/msal4j/ClientApplicationBase.java | 3 ++- .../aad/msal4j/ClientCredentialFactory.java | 3 ++- .../aad/msal4j/ClientCredentialParameters.java | 3 +++ .../aad/msal4j/ConfidentialClientApplication.java | 3 +++ .../aad/msal4j/DeviceCodeFlowParameters.java | 13 ++++++++++++- .../java/com/microsoft/aad/msal4j/HttpMethod.java | 2 +- .../java/com/microsoft/aad/msal4j/IAccount.java | 1 + .../com/microsoft/aad/msal4j/IClientAssertion.java | 2 ++ .../microsoft/aad/msal4j/IClientCertificate.java | 2 ++ .../com/microsoft/aad/msal4j/IClientCredential.java | 2 ++ .../com/microsoft/aad/msal4j/IClientSecret.java | 2 ++ .../java/com/microsoft/aad/msal4j/IHttpClient.java | 2 ++ .../java/com/microsoft/aad/msal4j/ITokenCache.java | 2 ++ .../aad/msal4j/ITokenCacheAccessAspect.java | 2 ++ .../aad/msal4j/ITokenCacheAccessContext.java | 2 ++ .../IntegratedWindowsAuthenticationParameters.java | 8 ++++++++ .../aad/msal4j/InteractiveRequestParameters.java | 2 ++ .../microsoft/aad/msal4j/OnBehalfOfParameters.java | 2 ++ .../aad/msal4j/PublicClientApplication.java | 3 +++ .../aad/msal4j/RefreshTokenParameters.java | 9 +++++++++ .../com/microsoft/aad/msal4j/SilentParameters.java | 1 + .../java/com/microsoft/aad/msal4j/TokenCache.java | 4 +++- .../aad/msal4j/TokenCacheAccessContext.java | 2 ++ .../aad/msal4j/UserNamePasswordParameters.java | 11 +++++++++++ 25 files changed, 93 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/microsoft/aad/msal4j/AuthorizationCodeParameters.java b/src/main/java/com/microsoft/aad/msal4j/AuthorizationCodeParameters.java index 520b54c2..48ea6a2a 100644 --- a/src/main/java/com/microsoft/aad/msal4j/AuthorizationCodeParameters.java +++ b/src/main/java/com/microsoft/aad/msal4j/AuthorizationCodeParameters.java @@ -22,16 +22,27 @@ @AllArgsConstructor(access = AccessLevel.PRIVATE) public class AuthorizationCodeParameters { + /** + * Authorization code acquired in the first step of OAuth2.0 authorization code flow. For more + * details, see https://aka.ms/msal4j-authorization-code-flow + */ @NonNull private String authorizationCode; + /** + * Redirect URI registered in the Azure portal, and which was used in the first step of OAuth2.0 + * authorization code flow. For more details, see https://aka.ms/msal4j-authorization-code-flow + */ @NonNull private URI redirectUri; + /** + * Scopes to which the application is requesting access + */ private Set scopes; /** - * Code verifier used for PKCE. + * Code verifier used for PKCE. For more details, see https://tools.ietf.org/html/rfc7636 */ private String codeVerifier; diff --git a/src/main/java/com/microsoft/aad/msal4j/ClientApplicationBase.java b/src/main/java/com/microsoft/aad/msal4j/ClientApplicationBase.java index eba54005..33c7fdbc 100644 --- a/src/main/java/com/microsoft/aad/msal4j/ClientApplicationBase.java +++ b/src/main/java/com/microsoft/aad/msal4j/ClientApplicationBase.java @@ -22,7 +22,8 @@ import static com.microsoft.aad.msal4j.ParameterValidationUtils.validateNotNull; /** - * Abstract class containing common API methods and properties. + * Abstract class containing common methods and properties to both {@link PublicClientApplication} + * and {@link ConfidentialClientApplication}. */ abstract class ClientApplicationBase implements IClientApplicationBase { diff --git a/src/main/java/com/microsoft/aad/msal4j/ClientCredentialFactory.java b/src/main/java/com/microsoft/aad/msal4j/ClientCredentialFactory.java index bd5bf3e4..02799067 100644 --- a/src/main/java/com/microsoft/aad/msal4j/ClientCredentialFactory.java +++ b/src/main/java/com/microsoft/aad/msal4j/ClientCredentialFactory.java @@ -10,7 +10,8 @@ import java.security.cert.X509Certificate; /** - * Factory for creating client credentials used in confidential client flows + * Factory for creating client credentials used in confidential client flows. For more details, see + * https://aka.ms/msal4j-client-credentials */ public class ClientCredentialFactory { diff --git a/src/main/java/com/microsoft/aad/msal4j/ClientCredentialParameters.java b/src/main/java/com/microsoft/aad/msal4j/ClientCredentialParameters.java index 8c3094f3..9da25412 100644 --- a/src/main/java/com/microsoft/aad/msal4j/ClientCredentialParameters.java +++ b/src/main/java/com/microsoft/aad/msal4j/ClientCredentialParameters.java @@ -20,6 +20,9 @@ @AllArgsConstructor(access = AccessLevel.PRIVATE) public class ClientCredentialParameters { + /** + * Scopes for which the application is requesting access to. + */ @NonNull private Set scopes; diff --git a/src/main/java/com/microsoft/aad/msal4j/ConfidentialClientApplication.java b/src/main/java/com/microsoft/aad/msal4j/ConfidentialClientApplication.java index 13bf74a1..a988cb5a 100644 --- a/src/main/java/com/microsoft/aad/msal4j/ConfidentialClientApplication.java +++ b/src/main/java/com/microsoft/aad/msal4j/ConfidentialClientApplication.java @@ -22,6 +22,9 @@ /** * Class to be used to acquire tokens for confidential client applications (Web Apps, Web APIs, * and daemon applications). + * For details see {@link IConfidentialClientApplication} + * + * Conditionally thread-safe */ public class ConfidentialClientApplication extends ClientApplicationBase implements IConfidentialClientApplication { diff --git a/src/main/java/com/microsoft/aad/msal4j/DeviceCodeFlowParameters.java b/src/main/java/com/microsoft/aad/msal4j/DeviceCodeFlowParameters.java index f27d0863..b8378d70 100644 --- a/src/main/java/com/microsoft/aad/msal4j/DeviceCodeFlowParameters.java +++ b/src/main/java/com/microsoft/aad/msal4j/DeviceCodeFlowParameters.java @@ -13,7 +13,8 @@ /** * Object containing parameters for device code flow. Can be used as parameter to - * {@link PublicClientApplication#acquireToken(DeviceCodeFlowParameters)} + * {@link PublicClientApplication#acquireToken(DeviceCodeFlowParameters)}. For more details, + * see https://aka.ms/msal4j-device-code */ @Builder @Accessors(fluent = true) @@ -21,9 +22,19 @@ @AllArgsConstructor(access = AccessLevel.PRIVATE) public class DeviceCodeFlowParameters { + /** + * Scopes to which the application is requesting access to. + */ @NonNull private Set scopes; + /** + * Receives the device code returned from the first step of Oauth2.0 device code flow. The + * {@link DeviceCode#verificationUri} and the {@link DeviceCode#userCode} should be shown + * to the end user. + * + * For more details, see https://aka.ms/msal4j-device-code + */ @NonNull private Consumer deviceCodeConsumer; diff --git a/src/main/java/com/microsoft/aad/msal4j/HttpMethod.java b/src/main/java/com/microsoft/aad/msal4j/HttpMethod.java index f18b7d05..64af4605 100644 --- a/src/main/java/com/microsoft/aad/msal4j/HttpMethod.java +++ b/src/main/java/com/microsoft/aad/msal4j/HttpMethod.java @@ -4,7 +4,7 @@ package com.microsoft.aad.msal4j; /** - * Http request method + * Http request method. */ public enum HttpMethod { diff --git a/src/main/java/com/microsoft/aad/msal4j/IAccount.java b/src/main/java/com/microsoft/aad/msal4j/IAccount.java index 9814d944..7c43b5af 100644 --- a/src/main/java/com/microsoft/aad/msal4j/IAccount.java +++ b/src/main/java/com/microsoft/aad/msal4j/IAccount.java @@ -8,6 +8,7 @@ /** * Interface representing a single user account. An IAccount is returned in the {@link IAuthenticationResult} * property, and is used as parameter in {@link SilentParameters#builder(Set, IAccount)} )} + * */ public interface IAccount { diff --git a/src/main/java/com/microsoft/aad/msal4j/IClientAssertion.java b/src/main/java/com/microsoft/aad/msal4j/IClientAssertion.java index d64c7c6e..e82de558 100644 --- a/src/main/java/com/microsoft/aad/msal4j/IClientAssertion.java +++ b/src/main/java/com/microsoft/aad/msal4j/IClientAssertion.java @@ -6,6 +6,8 @@ /** * Credential type containing an assertion of type * "urn:ietf:params:oauth:token-type:jwt". + * + * For more details, see https://aka.ms/msal4j-client-credentials */ public interface IClientAssertion extends IClientCredential{ diff --git a/src/main/java/com/microsoft/aad/msal4j/IClientCertificate.java b/src/main/java/com/microsoft/aad/msal4j/IClientCertificate.java index 3225f984..cec06682 100644 --- a/src/main/java/com/microsoft/aad/msal4j/IClientCertificate.java +++ b/src/main/java/com/microsoft/aad/msal4j/IClientCertificate.java @@ -9,6 +9,8 @@ /** * Credential type containing X509 public certificate and RSA private key. + * + * For more details, see https://aka.ms/msal4j-client-credentials */ public interface IClientCertificate extends IClientCredential{ diff --git a/src/main/java/com/microsoft/aad/msal4j/IClientCredential.java b/src/main/java/com/microsoft/aad/msal4j/IClientCredential.java index 09053adf..e1b6d959 100644 --- a/src/main/java/com/microsoft/aad/msal4j/IClientCredential.java +++ b/src/main/java/com/microsoft/aad/msal4j/IClientCredential.java @@ -5,6 +5,8 @@ /** * Interface representing an application credential + * + * For more details, see https://aka.ms/msal4j-client-credentials */ public interface IClientCredential { diff --git a/src/main/java/com/microsoft/aad/msal4j/IClientSecret.java b/src/main/java/com/microsoft/aad/msal4j/IClientSecret.java index d8ed914d..568a8786 100644 --- a/src/main/java/com/microsoft/aad/msal4j/IClientSecret.java +++ b/src/main/java/com/microsoft/aad/msal4j/IClientSecret.java @@ -5,6 +5,8 @@ /** * Representation of client credential containing a secret in string format + * + * For more details, see https://aka.ms/msal4j-client-credentials */ public interface IClientSecret extends IClientCredential{ diff --git a/src/main/java/com/microsoft/aad/msal4j/IHttpClient.java b/src/main/java/com/microsoft/aad/msal4j/IHttpClient.java index 75bd8677..b14001d4 100644 --- a/src/main/java/com/microsoft/aad/msal4j/IHttpClient.java +++ b/src/main/java/com/microsoft/aad/msal4j/IHttpClient.java @@ -6,6 +6,8 @@ /** * Interface to be implemented when configuring http client for {@link IPublicClientApplication} or * {@link IConfidentialClientApplication}. + * + * For more details, see https://aka.ms/msal4j-http-client */ public interface IHttpClient { diff --git a/src/main/java/com/microsoft/aad/msal4j/ITokenCache.java b/src/main/java/com/microsoft/aad/msal4j/ITokenCache.java index 9329a7f9..52abbf1c 100644 --- a/src/main/java/com/microsoft/aad/msal4j/ITokenCache.java +++ b/src/main/java/com/microsoft/aad/msal4j/ITokenCache.java @@ -5,6 +5,8 @@ /** * Interface representing security token cache persistence + * + * For more details, see https://aka.ms/msal4j-token-cache */ public interface ITokenCache { diff --git a/src/main/java/com/microsoft/aad/msal4j/ITokenCacheAccessAspect.java b/src/main/java/com/microsoft/aad/msal4j/ITokenCacheAccessAspect.java index 3659c57f..3181a929 100644 --- a/src/main/java/com/microsoft/aad/msal4j/ITokenCacheAccessAspect.java +++ b/src/main/java/com/microsoft/aad/msal4j/ITokenCacheAccessAspect.java @@ -5,6 +5,8 @@ /** * Interface representing operation of executing code before and after cache access. + * + * For more details, see https://aka.ms/msal4j-token-cache */ public interface ITokenCacheAccessAspect { diff --git a/src/main/java/com/microsoft/aad/msal4j/ITokenCacheAccessContext.java b/src/main/java/com/microsoft/aad/msal4j/ITokenCacheAccessContext.java index 3c386334..71ccee89 100644 --- a/src/main/java/com/microsoft/aad/msal4j/ITokenCacheAccessContext.java +++ b/src/main/java/com/microsoft/aad/msal4j/ITokenCacheAccessContext.java @@ -5,6 +5,8 @@ /** * Interface representing context in which the token cache is accessed + * + * For more details, see https://aka.ms/msal4j-token-cache */ public interface ITokenCacheAccessContext { diff --git a/src/main/java/com/microsoft/aad/msal4j/IntegratedWindowsAuthenticationParameters.java b/src/main/java/com/microsoft/aad/msal4j/IntegratedWindowsAuthenticationParameters.java index c1c2e5a2..8abe408d 100644 --- a/src/main/java/com/microsoft/aad/msal4j/IntegratedWindowsAuthenticationParameters.java +++ b/src/main/java/com/microsoft/aad/msal4j/IntegratedWindowsAuthenticationParameters.java @@ -14,6 +14,8 @@ /** * Object containing parameters for Integrated Windows Authentication. Can be used as parameter to * {@link PublicClientApplication#acquireToken(IntegratedWindowsAuthenticationParameters)}` + * + * For more details, see https://aka.ms/msal4j-iwa */ @Builder @Accessors(fluent = true) @@ -21,9 +23,15 @@ @AllArgsConstructor(access = AccessLevel.PRIVATE) public class IntegratedWindowsAuthenticationParameters { + /** + * Scopes that the application is requesting access to + */ @NonNull private Set scopes; + /** + * Identifier of user account for which to acquire tokens for + */ @NonNull private String username; diff --git a/src/main/java/com/microsoft/aad/msal4j/InteractiveRequestParameters.java b/src/main/java/com/microsoft/aad/msal4j/InteractiveRequestParameters.java index bf9578cf..16eeba45 100644 --- a/src/main/java/com/microsoft/aad/msal4j/InteractiveRequestParameters.java +++ b/src/main/java/com/microsoft/aad/msal4j/InteractiveRequestParameters.java @@ -19,6 +19,8 @@ /** * Object containing parameters for interactive requests. Can be used as parameter to * {@link PublicClientApplication#acquireToken(InteractiveRequestParameters)}. + * + * For more details, see https://aka.ms/msal4j-interactive-request. */ @Builder @Accessors(fluent = true) diff --git a/src/main/java/com/microsoft/aad/msal4j/OnBehalfOfParameters.java b/src/main/java/com/microsoft/aad/msal4j/OnBehalfOfParameters.java index 39b84e0d..6f181e08 100644 --- a/src/main/java/com/microsoft/aad/msal4j/OnBehalfOfParameters.java +++ b/src/main/java/com/microsoft/aad/msal4j/OnBehalfOfParameters.java @@ -13,6 +13,8 @@ /** * Object containing parameters for On-Behalf-Of flow. Can be used as parameter to * {@link ConfidentialClientApplication#acquireToken(OnBehalfOfParameters)} + * + * For more details, see https://aka.ms/msal4j-on-behalf-of */ @Builder @Accessors(fluent = true) diff --git a/src/main/java/com/microsoft/aad/msal4j/PublicClientApplication.java b/src/main/java/com/microsoft/aad/msal4j/PublicClientApplication.java index d42c4871..83be5937 100644 --- a/src/main/java/com/microsoft/aad/msal4j/PublicClientApplication.java +++ b/src/main/java/com/microsoft/aad/msal4j/PublicClientApplication.java @@ -18,6 +18,9 @@ /** * Class to be used to acquire tokens for public client applications (Desktop, Mobile). + * For details see {@link IPublicClientApplication} + * + * Conditionally thread-safe */ public class PublicClientApplication extends ClientApplicationBase implements IPublicClientApplication { diff --git a/src/main/java/com/microsoft/aad/msal4j/RefreshTokenParameters.java b/src/main/java/com/microsoft/aad/msal4j/RefreshTokenParameters.java index 5a7b0488..04f6a36d 100644 --- a/src/main/java/com/microsoft/aad/msal4j/RefreshTokenParameters.java +++ b/src/main/java/com/microsoft/aad/msal4j/RefreshTokenParameters.java @@ -15,6 +15,9 @@ * Object containing parameters for refresh token request. Can be used as parameter to * {@link PublicClientApplication#acquireToken(RefreshTokenParameters)} or to * {@link ConfidentialClientApplication#acquireToken(RefreshTokenParameters)} + * + * RefreshTokenParameters should only be used for migration scenarios (when moving from ADAL to + * MSAL). To acquire tokens silently, use {@link ClientApplicationBase#acquireTokenSilently(SilentParameters)} */ @Builder @Accessors(fluent = true) @@ -22,9 +25,15 @@ @AllArgsConstructor(access = AccessLevel.PRIVATE) public class RefreshTokenParameters { + /** + * Scopes the application is requesting access to + */ @NonNull private Set scopes; + /** + * Refresh token received from the STS + */ @NonNull private String refreshToken; diff --git a/src/main/java/com/microsoft/aad/msal4j/SilentParameters.java b/src/main/java/com/microsoft/aad/msal4j/SilentParameters.java index b9bab07d..5380c4eb 100644 --- a/src/main/java/com/microsoft/aad/msal4j/SilentParameters.java +++ b/src/main/java/com/microsoft/aad/msal4j/SilentParameters.java @@ -15,6 +15,7 @@ * Object containing parameters for silent requests. Can be used as parameter to * {@link PublicClientApplication#acquireTokenSilently(SilentParameters)} or to * {@link ConfidentialClientApplication#acquireTokenSilently(SilentParameters)} + * */ @Builder @Accessors(fluent = true) diff --git a/src/main/java/com/microsoft/aad/msal4j/TokenCache.java b/src/main/java/com/microsoft/aad/msal4j/TokenCache.java index 4ee6e011..aa78926b 100644 --- a/src/main/java/com/microsoft/aad/msal4j/TokenCache.java +++ b/src/main/java/com/microsoft/aad/msal4j/TokenCache.java @@ -15,7 +15,9 @@ import java.util.stream.Collectors; /** - * Cache used for storing tokens. + * Cache used for storing tokens. For more details, see https://aka.ms/msal4j-token-cache + * + * Conditionally thread-safe */ public class TokenCache implements ITokenCache { diff --git a/src/main/java/com/microsoft/aad/msal4j/TokenCacheAccessContext.java b/src/main/java/com/microsoft/aad/msal4j/TokenCacheAccessContext.java index 9ac8d9e4..3a0d4a2a 100644 --- a/src/main/java/com/microsoft/aad/msal4j/TokenCacheAccessContext.java +++ b/src/main/java/com/microsoft/aad/msal4j/TokenCacheAccessContext.java @@ -9,6 +9,8 @@ /** * Context in which the the token cache is accessed + * + * For more details, see https://aka.ms/msal4j-token-cache */ @Builder @Accessors(fluent = true) diff --git a/src/main/java/com/microsoft/aad/msal4j/UserNamePasswordParameters.java b/src/main/java/com/microsoft/aad/msal4j/UserNamePasswordParameters.java index 398194b0..799b4c82 100644 --- a/src/main/java/com/microsoft/aad/msal4j/UserNamePasswordParameters.java +++ b/src/main/java/com/microsoft/aad/msal4j/UserNamePasswordParameters.java @@ -14,6 +14,8 @@ /** * Object containing parameters for Username/Password flow. Can be used as parameter to * {@link PublicClientApplication#acquireToken(UserNamePasswordParameters)} + * + * For more details, see https://aka.ms/msal4j-username-password */ @Builder @Accessors(fluent = true) @@ -21,12 +23,21 @@ @AllArgsConstructor(access = AccessLevel.PRIVATE) public class UserNamePasswordParameters { + /** + * Scopes application is requesting access to + */ @NonNull private Set scopes; + /** + * Username of the account + */ @NonNull private String username; + /** + * Char array containing credentials for the username + */ @NonNull private char[] password; From 76830d9706c570827e0f60a8b5ea8e88faba468f Mon Sep 17 00:00:00 2001 From: sgonzalezMSFT Date: Thu, 20 Feb 2020 15:11:17 -0800 Subject: [PATCH 23/24] Add aka.ms links to errors --- .../aad/msal4j/AuthenticationErrorCode.java | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/microsoft/aad/msal4j/AuthenticationErrorCode.java b/src/main/java/com/microsoft/aad/msal4j/AuthenticationErrorCode.java index c53d7d27..88253585 100644 --- a/src/main/java/com/microsoft/aad/msal4j/AuthenticationErrorCode.java +++ b/src/main/java/com/microsoft/aad/msal4j/AuthenticationErrorCode.java @@ -9,19 +9,21 @@ public class AuthenticationErrorCode { /** - * In the context of device code user has not yet authenticated via browser + * In the context of device code user has not yet authenticated via browser. For more details, + * see https://aka.ms/msal4j-device-code */ public final static String AUTHORIZATION_PENDING = "authorization_pending"; /** * In the context of device code, this error happens when the device code has expired before - * the user signed-in on another device (this is usually after 15 min) + * the user signed-in on another device (this is usually after 15 min). For more details, see + * https://aka.ms/msal4j-device-code */ public final static String CODE_EXPIRED = "code_expired"; /** * Standard Oauth2 protocol error code. It indicates that the application needs to expose - * the UI to the user so that user does an interactive action in order to get a new token + * the UI to the user so that user does an interactive action in order to get a new token. */ public final static String INVALID_GRANT = "invalid_grant"; @@ -32,7 +34,7 @@ public class AuthenticationErrorCode { /** * Password is required for managed user. Will typically happen when trying to do integrated windows authentication - * for managed users + * for managed users. For more information, see https://aka.ms/msal4j-iwa */ public final static String PASSWORD_REQUIRED_FOR_MANAGED_USER = "password_required_for_managed_user"; @@ -60,7 +62,7 @@ public class AuthenticationErrorCode { /** * The current redirect URI is not a loopback URL. To use the OS browser, a loopback URL must be * configured both during app registration as well as when initializing the InteractiveRequestParameters - * object + * object. For more details, see https://aka.ms/msal4j-interactive-request */ public final static String LOOPBACK_REDIRECT_URI = "loopback_redirect_uri"; @@ -70,19 +72,22 @@ public class AuthenticationErrorCode { public final static String UNABLE_TO_START_HTTP_LISTENER = "unable_to_start_http_listener"; /** - * Authorization result response is invalid, either because format is invalid or it does not contain - * an authorization code. + * Authorization result response is invalid. Authorization result must contain authorization + * code and state. */ public final static String INVALID_AUTHORIZATION_RESULT = "invalid_authorization_result"; /** * Redirect URI provided to MSAL is of invalid format. Redirect URL must be a loopback URL. + * For more details, see https://aka.ms/msal4j-interactive-request */ public final static String INVALID_REDIRECT_URI = "invalid_redirect_uri"; /** * MSAL was unable to open the user-default browser. This is either because the current platform - * does not support {@link java.awt.Desktop} or {@link java.awt.Desktop.Action#BROWSE} + * does not support {@link java.awt.Desktop} or {@link java.awt.Desktop.Action#BROWSE}. Interactive + * requests require that the client have a system default browser. For more details, see + * https://aka.ms/msal4j-interactive-request */ public final static String DESKTOP_BROWSER_NOT_SUPPORTED = "desktop_browser_not_supported"; } From a8b7330ebbb845758d9496831312ec008a2ba4c3 Mon Sep 17 00:00:00 2001 From: sgonzalezMSFT Date: Thu, 20 Feb 2020 16:00:44 -0800 Subject: [PATCH 24/24] Increment version to 1.4. Update changelog.txt --- README.md | 6 +++--- changelog.txt | 7 +++++++ pom.xml | 2 +- src/samples/msal-b2c-web-sample/pom.xml | 2 +- src/samples/msal-obo-sample/pom.xml | 2 +- src/samples/msal-web-sample/pom.xml | 2 +- 6 files changed, 14 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 0ea970a4..c10eac3b 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Quick links: The library supports the following Java environments: - Java 8 (or higher) -Current version - 1.3.0 +Current version - 1.4.0 You can find the changes for each version in the [change log](https://github.com/AzureAD/microsoft-authentication-library-for-java/blob/master/changelog.txt). @@ -28,13 +28,13 @@ Find [the latest package in the Maven repository](https://mvnrepository.com/arti com.microsoft.azure msal4j - 1.3.0 + 1.4.0 ``` ### Gradle ``` -compile group: 'com.microsoft.azure', name: 'msal4j', version: '1.3.0' +compile group: 'com.microsoft.azure', name: 'msal4j', version: '1.4.0' ``` ## Usage diff --git a/changelog.txt b/changelog.txt index edeab1d3..99823496 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,10 @@ +Version 1.4.0 +============= +- Added acquire token interactive API, using system default browser +- Added authorization code url builder +- Added OSGi support via bnd-maven-plugin +- Added server-side telemetry support + Version 1.3.0 ============= - Added option to pass in AAD instance discovery data diff --git a/pom.xml b/pom.xml index 86b3d5be..2d67ed79 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 com.microsoft.azure msal4j - 1.3.0 + 1.4.0 jar msal4j diff --git a/src/samples/msal-b2c-web-sample/pom.xml b/src/samples/msal-b2c-web-sample/pom.xml index 35209819..8f1a8499 100644 --- a/src/samples/msal-b2c-web-sample/pom.xml +++ b/src/samples/msal-b2c-web-sample/pom.xml @@ -23,7 +23,7 @@ com.microsoft.azure msal4j - 1.3.0 + 1.4.0 com.nimbusds diff --git a/src/samples/msal-obo-sample/pom.xml b/src/samples/msal-obo-sample/pom.xml index 151c4d6d..c27898fb 100644 --- a/src/samples/msal-obo-sample/pom.xml +++ b/src/samples/msal-obo-sample/pom.xml @@ -23,7 +23,7 @@ com.microsoft.azure msal4j - 1.3.0 + 1.4.0 com.nimbusds diff --git a/src/samples/msal-web-sample/pom.xml b/src/samples/msal-web-sample/pom.xml index 5891eca4..c37e3026 100644 --- a/src/samples/msal-web-sample/pom.xml +++ b/src/samples/msal-web-sample/pom.xml @@ -23,7 +23,7 @@ com.microsoft.azure msal4j - 1.3.0 + 1.4.0 com.nimbusds