From 8f00ea0db7b9ac72d0c333b9e03f97ef79247b42 Mon Sep 17 00:00:00 2001 From: Avery-Dunn <62066438+Avery-Dunn@users.noreply.github.com> Date: Thu, 26 Oct 2023 15:35:33 -0700 Subject: [PATCH] MSAL Java/MSALRuntime integration (#590) * Add IBroker implementation for MSALRuntime * Remove dll used during testing * Integrate broker steps to relevant flows in PublicClientApplication * Add logic to cancel MsalRuntimeFutures * Expand javadocs and exception handling * Address code review comments * Simplify future chaining, address code review comments * Reorganize future chaining, fix testing issues * Adjust how broker availability is checked * Create automated test * Adjust startup logic * Correct version number for interop * Correct broker versioning * Move broker tests to MSAL Java package * Remove usage of msal4j-brokers from msal4j * Add missing SLFJ dependency * Use newest msal4j * Bump javamsalruntime version number * Version changes for 1.14.0-beta release (#589) * Add missing pom info needed by sonatype * APIs for toggling MSALRuntime's logging (#608) * Add APIs for toggling MSALRuntime's logging systems * Rename logging methods to be more clear * Add support for POP tokens to MSAL Java and MSAL Java Brokers (#639) * Version changes for 1.14.0-beta release * regional endpoint change to always use login.microsoft.com * Add support for both current and legacy B2C authority formats (#594) * Add support for both current and legacy B2C authority formats * Fix B2C format test * add 2 seconds timeout while calling IMDS * Fix failing tests * Fix failing tests * delete commented out code * Use the dedicated admin consent endpoint instead of a query parameter (#599) * updated versions for release * update condition to throw exception * added test for invalid authority * Add tests for a CIAM user and reduce test code duplication (#603) * Add tests for a CIAM user and reduce code duplication in several test files * Revert changed method name * Attempt to resolve credscan flag * Resolve credscan issues * Address code review comments * Use default scope * expose extraQueryParameters * expose extraQueryParameters * ExtraQueryParameters tests * retrigger the tests * Updated an existing test case to check added parameters * Replace exception with warning * version updates for release * update json-smart version * Updated json-smart version Updated json-smart version to a 'bug-free' version * version updates for release * Initial commit * add CIAM authority file * revert authority validation changes * Fix failing tests * Fix failing tests * remove commented out line * remove unnecessary code * update exception message for device code flow * add refresh_in logic * resolve build issues + address PR comments * update tests * updated org-json version to resolve Dependabot alert * Better redirect URI error handling and dependency upgrade (#633) * Better error handling for redirect URIs * Update oauth2-oidc-sdk dependency * Address review comments Co-authored-by: Bogdan Gavril --------- Co-authored-by: Bogdan Gavril * Version updates for 1.13.8 release (#634) * Version updates for 1.13.8 release * Update changelog.txt * Point to MSAL Java reference docs onboarded to Microsoft Learn * Add support for POP tokens to MSAL Java and MSAL Java Brokers * Send extraQueryParameters to interop's AuthParameters * Avoid exposing new PopParameters class, change API to match design doc * Update msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/InteractiveRequestParameters.java Co-authored-by: Bogdan Gavril * Update msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/InteractiveRequestParameters.java Co-authored-by: Bogdan Gavril * Update change log URl in README.md (#649) Replaced broken link in change log reference with msal4j-sdk/changelog.txt * Issue 447 * Feedback incorporation * enum for os type * Use enum for HTTP methods * Add broker tests, address PR review comments * Improve PoP tests * Address code review comments * Version updates * Re-add extraQueryParameters support --------- Co-authored-by: siddhijain Co-authored-by: Bogdan Gavril Co-authored-by: Dickson Mwendia <64727760+Dickson-Mwendia@users.noreply.github.com> Co-authored-by: Tamas Csizmadia * Fix silent issue * Ensure correlation ID is never null * Broker fixes and feedback (#733) * Delete codeql.yml * Test framework update (#672) * Initial working tests * Remove CIAM extra query parameter * Fix failing tests * Remove duplicate unit tests * Remove duplicate unit tests * Update tests with mocking to use Mockito * Remove testng and powermock, add junit and mockito * Remove AbstractMsalTests and PowerMockTestCase * Fix mistaken null check * Properly scope dependency * Update CIAM tests (#673) * Bump guava from 31.1-jre to 32.0.0-jre in /msal4j-sdk (#671) Bumps [guava](https://github.com/google/guava) from 31.1-jre to 32.0.0-jre. - [Release notes](https://github.com/google/guava/releases) - [Commits](https://github.com/google/guava/commits) --- updated-dependencies: - dependency-name: com.google.guava:guava dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Avery-Dunn * Delete contributing.md (#667) Co-authored-by: Avery-Dunn * Create Contributing.md (#668) Co-authored-by: Avery-Dunn * Version changes for 1.13.9 (#674) * Add space between command and arguments when executing linux command to open browser. Refs #682 (#683) Co-authored-by: Ric Emery * Assorted fixes (#684) * Remove default timeouts and improve exception messages * Fix NPE for on-prem ADFS scenario * Log MSAL message but re-throw exception * Update vulnerable test dependency * Issue-679: Fix for Account Cache; .contains() was not possible and you had to iterate through all elements as workaround. (#681) * Version changes for 1.13.10 (#685) * Move changelog * Move changelog to root * Update issue templates (#707) * Re-add lombok source line (#705) * Version changes for release 1.13.11 (#714) * Update bug report * Delete .github/ISSUE_TEMPLATE/bug_report.md * Update bug_report.yaml * Create FeatureRequest.yaml * Update FeatureRequest.yaml * Set default throttling time to 5 sec (#721) Co-authored-by: Kulyakhtin, Alexander (Ext) * Ensure correlation ID is never null * Rename MsalRuntimeBroker and add builder pattern for better API consistency --------- Signed-off-by: dependabot[bot] Co-authored-by: Bogdan Gavril Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Ric Emery Co-authored-by: Ric Emery Co-authored-by: Maximilian Pfeffer Co-authored-by: akulyakhtin Co-authored-by: Kulyakhtin, Alexander (Ext) * Version changes for msal4j-brokers 1.0.3-beta and msal4j 1.14.3-beta (#734) * Ensure correlation ID is never null * Version changes for msal4j-brokers 1.0.3-beta and msal4j 1.14.3-beta * Ensure that builder values for supported OS's are used * Release 1.14.0/1.0.0 version changes (#736) * Delete codeql.yml * Test framework update (#672) * Initial working tests * Remove CIAM extra query parameter * Fix failing tests * Remove duplicate unit tests * Remove duplicate unit tests * Update tests with mocking to use Mockito * Remove testng and powermock, add junit and mockito * Remove AbstractMsalTests and PowerMockTestCase * Fix mistaken null check * Properly scope dependency * Update CIAM tests (#673) * Bump guava from 31.1-jre to 32.0.0-jre in /msal4j-sdk (#671) Bumps [guava](https://github.com/google/guava) from 31.1-jre to 32.0.0-jre. - [Release notes](https://github.com/google/guava/releases) - [Commits](https://github.com/google/guava/commits) --- updated-dependencies: - dependency-name: com.google.guava:guava dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Avery-Dunn * Delete contributing.md (#667) Co-authored-by: Avery-Dunn * Create Contributing.md (#668) Co-authored-by: Avery-Dunn * Version changes for 1.13.9 (#674) * Add space between command and arguments when executing linux command to open browser. Refs #682 (#683) Co-authored-by: Ric Emery * Assorted fixes (#684) * Remove default timeouts and improve exception messages * Fix NPE for on-prem ADFS scenario * Log MSAL message but re-throw exception * Update vulnerable test dependency * Issue-679: Fix for Account Cache; .contains() was not possible and you had to iterate through all elements as workaround. (#681) * Version changes for 1.13.10 (#685) * Move changelog * Move changelog to root * Update issue templates (#707) * Re-add lombok source line (#705) * Version changes for release 1.13.11 (#714) * Update bug report * Delete .github/ISSUE_TEMPLATE/bug_report.md * Update bug_report.yaml * Create FeatureRequest.yaml * Update FeatureRequest.yaml * Set default throttling time to 5 sec (#721) Co-authored-by: Kulyakhtin, Alexander (Ext) * Version changes for 1.14.0 msal4j and 1.0.0 msal4j-brokers --------- Signed-off-by: dependabot[bot] Co-authored-by: Bogdan Gavril Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Ric Emery Co-authored-by: Ric Emery Co-authored-by: Maximilian Pfeffer Co-authored-by: akulyakhtin Co-authored-by: Kulyakhtin, Alexander (Ext) --------- Signed-off-by: dependabot[bot] Co-authored-by: siddhijain Co-authored-by: Bogdan Gavril Co-authored-by: Dickson Mwendia <64727760+Dickson-Mwendia@users.noreply.github.com> Co-authored-by: Tamas Csizmadia Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Ric Emery Co-authored-by: Ric Emery Co-authored-by: Maximilian Pfeffer Co-authored-by: akulyakhtin Co-authored-by: Kulyakhtin, Alexander (Ext) --- changelog.txt | 17 +- msal4j-brokers/changelog.txt | 4 + msal4j-brokers/pom.xml | 73 ++++- .../microsoft/aad/msal4jbrokers/Broker.java | 280 ++++++++++++++++++ .../aad/msal4jbrokers/MSALRuntimeBroker.java | 31 -- .../infrastructure/SeleniumConstants.java | 21 ++ .../infrastructure/SeleniumExtensions.java | 115 +++++++ .../test/java/labapi/HttpClientHelper.java | 62 ++++ .../java/labapi/KeyVaultSecretsProvider.java | 112 +++++++ .../src/test/java/labapi/LabConstants.java | 24 ++ .../src/test/java/labapi/LabService.java | 99 +++++++ .../src/test/java/labapi/LabUserProvider.java | 46 +++ msal4j-brokers/src/test/java/labapi/User.java | 36 +++ .../test/java/labapi/UserQueryParameters.java | 13 + .../src/test/java/labapi/UserSecret.java | 14 + .../test/java/test/ProofOfPossessionTest.java | 184 ++++++++++++ msal4j-sdk/README.md | 7 +- msal4j-sdk/bnd.bnd | 2 +- msal4j-sdk/pom.xml | 2 +- .../aad/msal4j/AuthenticationErrorCode.java | 11 + .../aad/msal4j/AuthenticationResult.java | 3 + .../com/microsoft/aad/msal4j/HttpMethod.java | 42 ++- .../com/microsoft/aad/msal4j/IBroker.java | 79 +++-- .../msal4j/InteractiveRequestParameters.java | 33 +++ .../microsoft/aad/msal4j/PopParameters.java | 44 +++ .../aad/msal4j/PublicClientApplication.java | 128 +++++++- .../aad/msal4j/SilentParameters.java | 22 ++ .../msal4j/UserNamePasswordParameters.java | 18 ++ 28 files changed, 1443 insertions(+), 79 deletions(-) create mode 100644 msal4j-brokers/changelog.txt create mode 100644 msal4j-brokers/src/main/java/com/microsoft/aad/msal4jbrokers/Broker.java delete mode 100644 msal4j-brokers/src/main/java/com/microsoft/aad/msal4jbrokers/MSALRuntimeBroker.java create mode 100644 msal4j-brokers/src/test/java/infrastructure/SeleniumConstants.java create mode 100644 msal4j-brokers/src/test/java/infrastructure/SeleniumExtensions.java create mode 100644 msal4j-brokers/src/test/java/labapi/HttpClientHelper.java create mode 100644 msal4j-brokers/src/test/java/labapi/KeyVaultSecretsProvider.java create mode 100644 msal4j-brokers/src/test/java/labapi/LabConstants.java create mode 100644 msal4j-brokers/src/test/java/labapi/LabService.java create mode 100644 msal4j-brokers/src/test/java/labapi/LabUserProvider.java create mode 100644 msal4j-brokers/src/test/java/labapi/User.java create mode 100644 msal4j-brokers/src/test/java/labapi/UserQueryParameters.java create mode 100644 msal4j-brokers/src/test/java/labapi/UserSecret.java create mode 100644 msal4j-brokers/src/test/java/test/ProofOfPossessionTest.java create mode 100644 msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/PopParameters.java diff --git a/changelog.txt b/changelog.txt index 24639066..81d5fd17 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,6 +1,19 @@ -Version 1.13.11 +Version 1.14.0 ============= -- Hotfix for internal docs generation issue (#705) +- GA release of MSAL Java Brokers package +- Add support for acquiring bearer and proof-of-possession tokens using WAM as the broker (#590) +- Default throttling time for password grant requests lowered to 5 seconds (#721) +- Fix internal docs generation issue (#705) + +Version 1.14.1-beta +============= +- Add proof-of-possession token support +- Add MSALRuntime logging support + +Version 1.14.0-beta +============= +- Add IBroker interface +- Add app-level parameter for enabling the use of auth brokers Version 1.13.10 ============= diff --git a/msal4j-brokers/changelog.txt b/msal4j-brokers/changelog.txt new file mode 100644 index 00000000..ff5f398a --- /dev/null +++ b/msal4j-brokers/changelog.txt @@ -0,0 +1,4 @@ +Version 1.0.0 +============= +- Initial release +- Provides the API and dependencies needed to acquire tokens via WAM \ No newline at end of file diff --git a/msal4j-brokers/pom.xml b/msal4j-brokers/pom.xml index 060d756e..646e23a7 100644 --- a/msal4j-brokers/pom.xml +++ b/msal4j-brokers/pom.xml @@ -5,12 +5,11 @@ 4.0.0 com.microsoft.azure msal4j-brokers - 0.0.1 + 1.0.0 jar msal4j-brokers - Microsoft Authentication Library for Java - Brokers helps you integrate with the broker - on windows machine to secure Access tokens and refresh tokens. + Microsoft Authentication Library for Java - Brokers is a companion package for MSAL Java that allows easy integration with authentication brokers https://github.com/AzureAD/microsoft-authentication-library-for-java @@ -22,15 +21,25 @@ https://github.com/AzureAD/microsoft-authentication-library-for-java + + + ms + Microsoft Corporation + + UTF-8 - com.microsoft.azure msal4j - 1.13.2 + 1.14.0 + + + com.microsoft.azure + javamsalruntime + 0.13.10 org.projectlombok @@ -38,6 +47,59 @@ 1.18.6 provided + + org.testng + testng + 7.1.0 + test + + + org.slf4j + slf4j-api + 1.7.36 + + + ch.qos.logback + logback-classic + 1.2.3 + test + + + commons-io + commons-io + 2.11.0 + test + + + org.seleniumhq.selenium + selenium-api + 3.14.0 + test + + + org.seleniumhq.selenium + selenium-chrome-driver + 3.14.0 + test + + + org.seleniumhq.selenium + selenium-support + 3.14.0 + test + + + com.azure + azure-core + 1.22.0 + test + + + com.azure + azure-security-keyvault-secrets + 4.3.5 + test + @@ -60,7 +122,6 @@ - ${project.build.directory}/delombok org.projectlombok diff --git a/msal4j-brokers/src/main/java/com/microsoft/aad/msal4jbrokers/Broker.java b/msal4j-brokers/src/main/java/com/microsoft/aad/msal4jbrokers/Broker.java new file mode 100644 index 00000000..da56b08f --- /dev/null +++ b/msal4j-brokers/src/main/java/com/microsoft/aad/msal4jbrokers/Broker.java @@ -0,0 +1,280 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.aad.msal4jbrokers; + +import com.microsoft.aad.msal4j.*; +import com.microsoft.azure.javamsalruntime.Account; +import com.microsoft.azure.javamsalruntime.AuthParameters; +import com.microsoft.azure.javamsalruntime.AuthResult; +import com.microsoft.azure.javamsalruntime.MsalInteropException; +import com.microsoft.azure.javamsalruntime.MsalRuntimeInterop; +import com.microsoft.azure.javamsalruntime.ReadAccountResult; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +public class Broker implements IBroker { + private static final Logger LOG = LoggerFactory.getLogger(Broker.class); + + private static MsalRuntimeInterop interop; + private static Boolean brokerAvailable; + + private boolean supportWindows; + + static { + try { + //MsalRuntimeInterop performs various initialization steps in a similar static block, + // so when an MsalRuntimeBroker is created this will cause the interop layer to initialize + interop = new MsalRuntimeInterop(); + } catch (MsalInteropException e) { + throw new MsalClientException(String.format("Could not initialize MSALRuntime: %s", e.getErrorMessage()), AuthenticationErrorCode.MSALRUNTIME_INTEROP_ERROR); + } + } + + @Override + public CompletableFuture acquireToken(PublicClientApplication application, SilentParameters parameters) { + String correlationID = application.correlationId() == null ? generateCorrelationID() : application.correlationId(); + Account accountResult = null; + + //If request has an account ID, MSALRuntime likely has data cached for that account that we can retrieve + if (parameters.account() != null) { + try { + accountResult = ((ReadAccountResult) interop.readAccountById(parameters.account().homeAccountId().split("\\.")[0], correlationID).get()).getAccount(); + } catch (InterruptedException | ExecutionException ex) { + throw new MsalClientException(String.format("MSALRuntime async operation interrupted when waiting for result: %s", ex.getMessage()), AuthenticationErrorCode.MSALRUNTIME_INTEROP_ERROR); + } + } + + try { + AuthParameters.AuthParametersBuilder authParamsBuilder = new AuthParameters. + AuthParametersBuilder(application.clientId(), + application.authority(), + String.join(" ", parameters.scopes())) + .additionalParameters(parameters.extraQueryParameters()); + + //If POP auth scheme configured, set parameters to get MSALRuntime to return POP tokens + if (parameters.proofOfPossession() != null) { + authParamsBuilder.popParameters(parameters.proofOfPossession().getHttpMethod().methodName, + parameters.proofOfPossession().getUri(), + parameters.proofOfPossession().getNonce()); + } + + AuthParameters authParameters = authParamsBuilder.build(); + + if (accountResult == null) { + return interop.signInSilently(authParameters, correlationID) + .thenCompose(acctResult -> interop.acquireTokenSilently(authParameters, correlationID, ((AuthResult) acctResult).getAccount())) + .thenApply(authResult -> parseBrokerAuthResult( + application.authority(), + ((AuthResult) authResult).getIdToken(), + ((AuthResult) authResult).getAccessToken(), + ((AuthResult) authResult).getAccount().getAccountId(), + ((AuthResult) authResult).getAccount().getClientInfo(), + ((AuthResult) authResult).getAccessTokenExpirationTime(), + ((AuthResult) authResult).isPopAuthorization())); + } else { + return interop.acquireTokenSilently(authParameters, correlationID, accountResult) + .thenApply(authResult -> parseBrokerAuthResult(application.authority(), + ((AuthResult) authResult).getIdToken(), + ((AuthResult) authResult).getAccessToken(), + ((AuthResult) authResult).getAccount().getAccountId(), + ((AuthResult) authResult).getAccount().getClientInfo(), + ((AuthResult) authResult).getAccessTokenExpirationTime(), + ((AuthResult) authResult).isPopAuthorization())); + } + } catch (MsalInteropException interopException) { + throw new MsalClientException(interopException.getErrorMessage(), AuthenticationErrorCode.MSALRUNTIME_INTEROP_ERROR); + } + } + + @Override + public CompletableFuture acquireToken(PublicClientApplication application, InteractiveRequestParameters parameters) { + String correlationID = application.correlationId() == null ? generateCorrelationID() : application.correlationId(); + + try { + AuthParameters.AuthParametersBuilder authParamsBuilder = new AuthParameters. + AuthParametersBuilder(application.clientId(), + application.authority(), + String.join(" ", parameters.scopes())) + .redirectUri(parameters.redirectUri().toString()) + .additionalParameters(parameters.extraQueryParameters()); + + //If POP auth scheme configured, set parameters to get MSALRuntime to return POP tokens + if (parameters.proofOfPossession() != null) { + authParamsBuilder.popParameters(parameters.proofOfPossession().getHttpMethod().methodName, + parameters.proofOfPossession().getUri(), + parameters.proofOfPossession().getNonce()); + } + + AuthParameters authParameters = authParamsBuilder.build(); + + return interop.signInInteractively(parameters.windowHandle(), authParameters, correlationID, parameters.loginHint()) + .thenCompose(acctResult -> interop.acquireTokenInteractively(parameters.windowHandle(), authParameters, correlationID, ((AuthResult) acctResult).getAccount())) + .thenApply(authResult -> parseBrokerAuthResult( + application.authority(), + ((AuthResult) authResult).getIdToken(), + ((AuthResult) authResult).getAccessToken(), + ((AuthResult) authResult).getAccount().getAccountId(), + ((AuthResult) authResult).getAccount().getClientInfo(), + ((AuthResult) authResult).getAccessTokenExpirationTime(), + ((AuthResult) authResult).isPopAuthorization())); + } catch (MsalInteropException interopException) { + throw new MsalClientException(interopException.getErrorMessage(), AuthenticationErrorCode.MSALRUNTIME_INTEROP_ERROR); + } + } + + /** + * @deprecated + */ + @Deprecated + @Override + public CompletableFuture acquireToken(PublicClientApplication application, UserNamePasswordParameters parameters) { + String correlationID = application.correlationId() == null ? generateCorrelationID() : application.correlationId(); + + try { + AuthParameters.AuthParametersBuilder authParamsBuilder = new AuthParameters. + AuthParametersBuilder(application.clientId(), + application.authority(), + String.join(" ", parameters.scopes())) + .additionalParameters(parameters.extraQueryParameters()); + + //If POP auth scheme configured, set parameters to get MSALRuntime to return POP tokens + if (parameters.proofOfPossession() != null) { + authParamsBuilder.popParameters(parameters.proofOfPossession().getHttpMethod().methodName, + parameters.proofOfPossession().getUri(), + parameters.proofOfPossession().getNonce()); + } + + AuthParameters authParameters = authParamsBuilder.build(); + + return interop.signInSilently(authParameters, correlationID) + .thenCompose(acctResult -> interop.acquireTokenSilently(authParameters, correlationID, ((AuthResult) acctResult).getAccount())) + .thenApply(authResult -> parseBrokerAuthResult( + application.authority(), + ((AuthResult) authResult).getIdToken(), + ((AuthResult) authResult).getAccessToken(), + ((AuthResult) authResult).getAccount().getAccountId(), + ((AuthResult) authResult).getAccount().getClientInfo(), + ((AuthResult) authResult).getAccessTokenExpirationTime(), + ((AuthResult) authResult).isPopAuthorization())); + } catch (MsalInteropException interopException) { + throw new MsalClientException(interopException.getErrorMessage(), AuthenticationErrorCode.MSALRUNTIME_INTEROP_ERROR); + } + } + + @Override + public void removeAccount(PublicClientApplication application, IAccount msalJavaAccount) { + String correlationID = application.correlationId() == null ? generateCorrelationID() : application.correlationId(); + + try { + Account msalRuntimeAccount = ((ReadAccountResult) interop.readAccountById(msalJavaAccount.homeAccountId().split("\\.")[0], correlationID).get()).getAccount(); + + if (msalRuntimeAccount != null) { + interop.signOutSilently(application.clientId(), correlationID, msalRuntimeAccount); + } + } catch (MsalInteropException interopException) { + throw new MsalClientException(interopException.getErrorMessage(), AuthenticationErrorCode.MSALRUNTIME_INTEROP_ERROR); + } catch (InterruptedException | ExecutionException ex) { + throw new MsalClientException(String.format("MSALRuntime async operation interrupted when waiting for result: %s", ex.getMessage()), AuthenticationErrorCode.MSALRUNTIME_INTEROP_ERROR); + } + } + + /** + * Calls MSALRuntime's startup API. If MSALRuntime started successfully, we can assume that the broker is available for use. + * + * If an exception is thrown when trying to start MSALRuntime, we assume that we cannot use the broker and will not make any more attempts to do so. + * + * @return boolean representing whether or not MSALRuntime started successfully + */ + @Override + public boolean isBrokerAvailable() { + //brokerAvailable is only set after the first attempt to call MSALRuntime's startup API + if (brokerAvailable == null) { + try { + interop.startupMsalRuntime(); + + LOG.info("MSALRuntime started successfully. MSAL Java will use MSALRuntime in all supported broker flows."); + + brokerAvailable = true; + } catch (MsalInteropException e) { + LOG.warn("Exception thrown when trying to start MSALRuntime: {}", e.getErrorMessage()); + LOG.warn("MSALRuntime could not be started. MSAL Java will fall back to non-broker flows."); + + brokerAvailable = false; + } + } + + return brokerAvailable; + } + + /** + * Toggles whether or not detailed MSALRuntime logs will appear in MSAL Java's normal logging framework. + * + * If enabled, you will see logs directly from MSALRuntime, containing verbose information relating to telemetry, API calls,successful/failed requests, and more. + * These logs will appear alongside MSAL Java's logs (with a message indicating they came from MSALRuntime), and will follow the same log level as MSAL Java's logs (info/debug/error/etc.). + * + * If disabled (default), MSAL Java will still produce some logs related to MSALRuntime, particularly in error messages, but will be much less verbose. + * + * @param enableLogging true to enable MSALRuntime logs, false to disable it + */ + public void enableBrokerLogging(boolean enableLogging) { + try { + MsalRuntimeInterop.enableLogging(enableLogging); + } catch (Exception ex) { + throw new MsalClientException(String.format("Error occurred when calling MSALRuntime logging API: %s", ex.getMessage()), AuthenticationErrorCode.MSALRUNTIME_INTEROP_ERROR); + } + } + + /** + * If enabled, Personal Identifiable Information (PII) can appear in logs and error messages produced by MSALRuntime. + * + * If disabled (default), PII will not be shown, and you will simply see "(PII)" or similar notes in places where PII data would have appeared. + * + * @param enablePII true to allow PII to appear in logs and error messages, false to disallow it + */ + public void enableBrokerPIILogging(boolean enablePII) { + try { + MsalRuntimeInterop.enableLoggingPii(enablePII); + } catch (Exception ex) { + throw new MsalClientException(String.format("Error occurred when calling MSALRuntime PII logging API: %s", ex.getMessage()), AuthenticationErrorCode.MSALRUNTIME_INTEROP_ERROR); + } + } + + //Generates a random correlation ID, used when a correlation ID was not set at the application level + private String generateCorrelationID() { + return UUID.randomUUID().toString(); + } + + public static class Builder { + private boolean supportWindows = false; + + public Builder() { + } + + /** + * When set to true, MSAL Java will attempt to use the broker when the application is running on a Windows OS + */ + public Builder supportWindows(boolean val) { + supportWindows = val; + return this; + } + + public Broker build() { + return new Broker(this); + } + } + + private Broker(Builder builder) { + this.supportWindows = builder.supportWindows; + + //This will be expanded to cover other OS options, but for now it is only Windows. Since Windows is the only + // option, if app developer doesn't want to use the broker on Windows then they shouldn't use the Broker at all + if (!this.supportWindows) { + throw new MsalClientException("At least one operating system support option must be used when building the Broker instance", AuthenticationErrorCode.MSALJAVA_BROKERS_ERROR); + } + } +} diff --git a/msal4j-brokers/src/main/java/com/microsoft/aad/msal4jbrokers/MSALRuntimeBroker.java b/msal4j-brokers/src/main/java/com/microsoft/aad/msal4jbrokers/MSALRuntimeBroker.java deleted file mode 100644 index 598b83ac..00000000 --- a/msal4j-brokers/src/main/java/com/microsoft/aad/msal4jbrokers/MSALRuntimeBroker.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.microsoft.aad.msal4jbrokers; - -import com.microsoft.aad.msal4j.*; -import lombok.extern.slf4j.Slf4j; - -import java.util.concurrent.CompletableFuture; - -@Slf4j -public class MSALRuntimeBroker implements IBroker { - - @Override - public IAuthenticationResult acquireToken(PublicClientApplication application, SilentParameters requestParameters) { - log.debug("Should not call this API if msal runtime init failed"); - throw new MsalClientException("Broker implementation missing", "missing_broker"); - } - - @Override - public IAuthenticationResult acquireToken(PublicClientApplication application, InteractiveRequestParameters requestParameters) { - throw new MsalClientException("Broker implementation missing", "missing_broker"); - } - - @Override - public IAuthenticationResult acquireToken(PublicClientApplication application, UserNamePasswordParameters requestParameters) { - throw new MsalClientException("Broker implementation missing", "missing_broker"); - } - - @Override - public CompletableFuture removeAccount(IAccount account) { - throw new MsalClientException("Broker implementation missing", "missing_broker"); - } -} diff --git a/msal4j-brokers/src/test/java/infrastructure/SeleniumConstants.java b/msal4j-brokers/src/test/java/infrastructure/SeleniumConstants.java new file mode 100644 index 00000000..859a9bd8 --- /dev/null +++ b/msal4j-brokers/src/test/java/infrastructure/SeleniumConstants.java @@ -0,0 +1,21 @@ +//---------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +// +//------------------------------------------------------------------------------ + +package infrastructure; + +public class SeleniumConstants { + final static String WEB_UPN_INPUT_ID = "i0116"; + final static String WEB_PASSWORD_ID = "i0118"; + final static String WEB_SUBMIT_ID = "idSIButton9"; + + // Stay signed in? + final static String STAY_SIGN_IN_NO_BUTTON_ID = "idBtn_Back"; + + // Are you trying to sign in to ... + //Only continue if you downloaded the app from a store or website that you trust. + final static String ARE_YOU_TRYING_TO_SIGN_IN_TO = "idSIButton9"; +} diff --git a/msal4j-brokers/src/test/java/infrastructure/SeleniumExtensions.java b/msal4j-brokers/src/test/java/infrastructure/SeleniumExtensions.java new file mode 100644 index 00000000..bf46d23e --- /dev/null +++ b/msal4j-brokers/src/test/java/infrastructure/SeleniumExtensions.java @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package infrastructure; + +import labapi.User; +import org.openqa.selenium.By; +import org.openqa.selenium.StaleElementReferenceException; +import org.openqa.selenium.TimeoutException; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.chrome.ChromeDriver; +import org.openqa.selenium.chrome.ChromeOptions; +import org.openqa.selenium.support.ui.ExpectedCondition; +import org.openqa.selenium.support.ui.WebDriverWait; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.TimeUnit; + +public class SeleniumExtensions { + + private final static Logger LOG = LoggerFactory.getLogger(SeleniumExtensions.class); + + private SeleniumExtensions() { + } + + public static WebDriver createDefaultWebDriver() { + ChromeOptions options = new ChromeOptions(); + //no visual rendering, remove when debugging + options.addArguments("--headless"); + + System.setProperty("webdriver.chrome.driver", "C:/Windows/chromedriver.exe"); + ChromeDriver driver = new ChromeDriver(options); + driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS); + + return driver; + } + + public static WebElement waitForElementToBeVisibleAndEnable(WebDriver driver, By by, int timeOutInSeconds) { + WebDriverWait webDriverWait = new WebDriverWait(driver, timeOutInSeconds); + return webDriverWait.until((dr) -> + { + try { + WebElement elementToBeDisplayed = driver.findElement(by); + if (elementToBeDisplayed.isDisplayed() && elementToBeDisplayed.isEnabled()) { + return elementToBeDisplayed; + } + return null; + } catch (StaleElementReferenceException e) { + return null; + } + }); + } + + public static WebElement waitForElementToBeVisibleAndEnable(WebDriver driver, By by) { + int DEFAULT_TIMEOUT_IN_SEC = 15; + + return waitForElementToBeVisibleAndEnable(driver, by, DEFAULT_TIMEOUT_IN_SEC); + } + + public static void performADLogin(WebDriver driver, User user) { + LOG.info("PerformADLogin"); + + LOG.info("Loggin in ... Entering username"); + driver.findElement(new By.ById(SeleniumConstants.WEB_UPN_INPUT_ID)).sendKeys(user.getUpn()); + + LOG.info("Loggin in ... Clicking after username"); + driver.findElement(new By.ById(SeleniumConstants.WEB_SUBMIT_ID)).click(); + + LOG.info("Loggin in ... Entering password"); + By by = new By.ById(SeleniumConstants.WEB_PASSWORD_ID); + waitForElementToBeVisibleAndEnable(driver, by).sendKeys(user.getPassword()); + + LOG.info("Loggin in ... click submit"); + waitForElementToBeVisibleAndEnable(driver, new By.ById(SeleniumConstants.WEB_SUBMIT_ID)). + click(); + + try { + checkAuthenticationCompletePage(driver); + return; + } catch (TimeoutException ex) { + } + + LOG.info("Checking optional questions"); + + try { + LOG.info("Are you trying to sign in to ... ? checking"); + waitForElementToBeVisibleAndEnable(driver, new By.ById(SeleniumConstants.ARE_YOU_TRYING_TO_SIGN_IN_TO), 3). + click(); + LOG.info("Are you trying to sign in to ... ? click Continue"); + + } catch (TimeoutException ex) { + } + + try { + LOG.info("Stay signed in? checking"); + waitForElementToBeVisibleAndEnable(driver, new By.ById(SeleniumConstants.STAY_SIGN_IN_NO_BUTTON_ID), 3). + click(); + LOG.info("Stay signed in? click NO"); + } catch (TimeoutException ex) { + } + } + + private static void checkAuthenticationCompletePage(WebDriver driver) { + (new WebDriverWait(driver, 5)).until((ExpectedCondition) d -> { + boolean condition = false; + WebElement we = d.findElement(new By.ByTagName("body")); + if (we != null && we.getText().contains("Authentication complete")) { + condition = true; + } + return condition; + }); + } +} diff --git a/msal4j-brokers/src/test/java/labapi/HttpClientHelper.java b/msal4j-brokers/src/test/java/labapi/HttpClientHelper.java new file mode 100644 index 00000000..e4d2eb1e --- /dev/null +++ b/msal4j-brokers/src/test/java/labapi/HttpClientHelper.java @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package labapi; + +import javax.net.ssl.HttpsURLConnection; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.UnsupportedEncodingException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLEncoder; +import java.util.Map; + +class HttpClientHelper { + static String sendRequestToLab(String url, Map queryMap, String accessToken) + throws IOException { + return sendRequestToLab(buildUrl(url, queryMap), accessToken); + } + + static String sendRequestToLab(URL labUrl, String accessToken) throws IOException { + HttpsURLConnection conn = (HttpsURLConnection)labUrl.openConnection(); + + conn.setRequestProperty("Authorization", "Bearer " + accessToken); + + conn.setReadTimeout(20000); + conn.setConnectTimeout(20000); + + StringBuilder content; + try (BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()))) { + String inputLine; + content = new StringBuilder(); + while ((inputLine = in.readLine()) != null) { + content.append(inputLine); + } + } + conn.disconnect(); + return content.toString(); + } + + private static URL buildUrl(String url, Map queryMap) + throws MalformedURLException, UnsupportedOperationException { + String queryParameters; + queryParameters = queryMap.entrySet() + .stream() + .map(p -> encodeUTF8(p.getKey()) + "=" + encodeUTF8(p.getValue())) + .reduce((p1, p2) -> p1 + "&" + p2) + .orElse(""); + + String urlString = url + "?" + queryParameters; + return new URL(urlString); + } + + private static String encodeUTF8(String s) { + try { + return URLEncoder.encode(s, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new IllegalArgumentException("Error: cannot encode query parameter " + s); + } + } +} diff --git a/msal4j-brokers/src/test/java/labapi/KeyVaultSecretsProvider.java b/msal4j-brokers/src/test/java/labapi/KeyVaultSecretsProvider.java new file mode 100644 index 00000000..673b550a --- /dev/null +++ b/msal4j-brokers/src/test/java/labapi/KeyVaultSecretsProvider.java @@ -0,0 +1,112 @@ +package labapi; + +import com.azure.core.credential.AccessToken; +import com.azure.core.credential.TokenCredential; +import com.azure.security.keyvault.secrets.SecretClient; +import com.azure.security.keyvault.secrets.SecretClientBuilder; +import com.azure.security.keyvault.secrets.models.KeyVaultSecretIdentifier; +import com.microsoft.aad.msal4j.ClientCredentialFactory; +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 reactor.core.publisher.Mono; + +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class KeyVaultSecretsProvider { + private final SecretClient secretClient; + + private static final String CLIENT_ID = "2afb0add-2f32-4946-ac90-81a02aa4550e"; + public static String CERTIFICATE_ALIAS = "MsalJavaAutomationRunner"; + + private static final String WIN_KEYSTORE = "Windows-MY"; + private static final String KEYSTORE_PROVIDER = "SunMSCAPI"; + + private static final String MAC_KEYSTORE = "KeychainStore"; + + static Map cache = new ConcurrentHashMap<>(); + + KeyVaultSecretsProvider() { + secretClient = getAuthenticatedSecretClient(); + } + + String getSecret(String secretUrl) { + // extract keyName from secretUrl + KeyVaultSecretIdentifier keyVaultSecretIdentifier = new KeyVaultSecretIdentifier(secretUrl); + String key = keyVaultSecretIdentifier.getName(); + + if (cache.containsKey(key)) { + return cache.get(key); + } + + String secret = secretClient.getSecret(key).getValue(); + cache.put(key, secret); + + return secret; + } + + private SecretClient getAuthenticatedSecretClient() { + return new SecretClientBuilder() + .credential(getTokenCredential()) + .vaultUrl(LabConstants.MSIDLAB_VAULT_URL) + .buildClient(); + } + + private AccessToken requestAccessTokenForAutomation() { + IAuthenticationResult result; + try { + ConfidentialClientApplication cca = + ConfidentialClientApplication + .builder(CLIENT_ID, getClientCredentialFromKeyStore()) + .authority(LabConstants.MICROSOFT_AUTHORITY) + .build(); + result = cca.acquireToken(ClientCredentialParameters + .builder(Collections.singleton( + LabConstants.KEYVAULT_DEFAULT_SCOPE)) + .build()) + .get(); + } catch (Exception e) { + throw new RuntimeException("Error acquiring token from Azure AD: " + e.getMessage()); + } + if (result != null) { + return new AccessToken( + result.accessToken(), + OffsetDateTime.ofInstant(result.expiresOnDate().toInstant(), ZoneOffset.UTC)); + } else { + throw new NullPointerException("Authentication result is null"); + } + } + + private IClientCredential getClientCredentialFromKeyStore() { + PrivateKey key; + X509Certificate publicCertificate; + try { + String os = System.getProperty("os.name"); + KeyStore keystore; + if (os.toLowerCase().contains("windows")) { + keystore = KeyStore.getInstance(WIN_KEYSTORE, KEYSTORE_PROVIDER); + } else { + keystore = KeyStore.getInstance(MAC_KEYSTORE); + } + + keystore.load(null, null); + key = (PrivateKey)keystore.getKey(CERTIFICATE_ALIAS, null); + publicCertificate = (X509Certificate)keystore.getCertificate(CERTIFICATE_ALIAS); + } catch (Exception e) { + throw new RuntimeException("Error getting certificate from keystore: " + e.getMessage()); + } + return ClientCredentialFactory.createFromCertificate(key, publicCertificate); + } + + private TokenCredential getTokenCredential() { + return tokenRequestContext -> Mono.defer(() -> Mono.just(requestAccessTokenForAutomation())); + } +} diff --git a/msal4j-brokers/src/test/java/labapi/LabConstants.java b/msal4j-brokers/src/test/java/labapi/LabConstants.java new file mode 100644 index 00000000..37e4f921 --- /dev/null +++ b/msal4j-brokers/src/test/java/labapi/LabConstants.java @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package labapi; + +public class LabConstants { + 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 MSIDLAB_VAULT_URL = "https://msidlabs.vault.azure.net/"; + + public final static String MICROSOFT_AUTHORITY = + "https://login.microsoftonline.com/microsoft.onmicrosoft.com"; + + public final static String LAB_USER_ENDPOINT = "https://msidlab.com/api/user"; + public final static String LAB_USER_SECRET_ENDPOINT = "https://msidlab.com/api/LabSecret"; + + public final static String APP_ID_KEY_VAULT_SECRET = + "https://msidlabs.vault.azure.net/secrets/LabVaultAppID"; + public final static String APP_PASSWORD_KEY_VAULT_SECRET = + "https://msidlabs.vault.azure.net/secrets/LabVaultAppSecret"; + + public final static String AZURE_ENVIRONMENT = "azurecloud"; + public final static String FEDERATION_PROVIDER_NONE = "none"; +} diff --git a/msal4j-brokers/src/test/java/labapi/LabService.java b/msal4j-brokers/src/test/java/labapi/LabService.java new file mode 100644 index 00000000..f15690bc --- /dev/null +++ b/msal4j-brokers/src/test/java/labapi/LabService.java @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package labapi; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.microsoft.aad.msal4j.ClientCredentialFactory; +import com.microsoft.aad.msal4j.ClientCredentialParameters; +import com.microsoft.aad.msal4j.ConfidentialClientApplication; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutionException; + +public class LabService { + static ConfidentialClientApplication labApp; + + static ObjectMapper mapper = + new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + + static T convertJsonToObject(final String json, final Class clazz) { + try { + return mapper.readValue(json, clazz); + } catch (IOException e) { + throw new RuntimeException("JSON processing error: " + e.getMessage(), e); + } + } + + static void initLabApp() throws MalformedURLException { + KeyVaultSecretsProvider keyVaultSecretsProvider = new KeyVaultSecretsProvider(); + + String appID = keyVaultSecretsProvider.getSecret(LabConstants.APP_ID_KEY_VAULT_SECRET); + String appSecret = + keyVaultSecretsProvider.getSecret(LabConstants.APP_PASSWORD_KEY_VAULT_SECRET); + + labApp = ConfidentialClientApplication + .builder(appID, ClientCredentialFactory.createFromSecret(appSecret)) + .authority(LabConstants.MICROSOFT_AUTHORITY) + .build(); + } + + static String getLabAccessToken() + throws MalformedURLException, ExecutionException, InterruptedException { + if (labApp == null) { + initLabApp(); + } + return labApp + .acquireToken( + ClientCredentialParameters + .builder(Collections.singleton(LabConstants.MSIDLAB_DEFAULT_SCOPE)) + .build()) + .get() + .accessToken(); + } + + User getUser(UserQueryParameters query) { + try { + Map queryMap = query.parameters; + String result = HttpClientHelper.sendRequestToLab( + LabConstants.LAB_USER_ENDPOINT, queryMap, getLabAccessToken()); + + User[] users = convertJsonToObject(result, User[].class); + User user = users[0]; + if (user.getUserType().equals("Guest")) { + String secretId = user.getHomeDomain().split("\\.")[0]; + user.setPassword(getSecret(secretId)); + } else { + user.setPassword(getSecret(user.getLabName())); + } + if (query.parameters.containsKey(UserQueryParameters.FEDERATION_PROVIDER)) { + user.setFederationProvider( + query.parameters.get(UserQueryParameters.FEDERATION_PROVIDER)); + } else { + user.setFederationProvider(LabConstants.FEDERATION_PROVIDER_NONE); + } + return user; + } catch (Exception ex) { + throw new RuntimeException("Error getting user from lab: " + ex.getMessage()); + } + } + + public static String getSecret(String labName) { + String result; + try { + Map queryMap = new HashMap<>(); + queryMap.put("secret", labName); + result = HttpClientHelper.sendRequestToLab( + LabConstants.LAB_USER_SECRET_ENDPOINT, queryMap, getLabAccessToken()); + + return convertJsonToObject(result, UserSecret.class).value; + } catch (Exception ex) { + throw new RuntimeException("Error getting user secret from lab: " + ex.getMessage()); + } + } +} diff --git a/msal4j-brokers/src/test/java/labapi/LabUserProvider.java b/msal4j-brokers/src/test/java/labapi/LabUserProvider.java new file mode 100644 index 00000000..cc37543e --- /dev/null +++ b/msal4j-brokers/src/test/java/labapi/LabUserProvider.java @@ -0,0 +1,46 @@ +//---------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +// +//------------------------------------------------------------------------------ + +package labapi; + +import java.util.HashMap; +import java.util.Map; + +public class LabUserProvider { + private static LabUserProvider instance; + + private final LabService labService; + private Map userCache; + + private LabUserProvider() { + labService = new LabService(); + userCache = new HashMap<>(); + } + + public static synchronized LabUserProvider getInstance() { + if (instance == null) { + instance = new LabUserProvider(); + } + return instance; + } + + public User getDefaultUser() { + UserQueryParameters query = new UserQueryParameters(); + query.parameters.put(UserQueryParameters.AZURE_ENVIRONMENT, LabConstants.AZURE_ENVIRONMENT); + + return getLabUser(query); + } + + public User getLabUser(UserQueryParameters userQuery) { + if (userCache.containsKey(userQuery)) { + return userCache.get(userQuery); + } + User response = labService.getUser(userQuery); + userCache.put(userQuery, response); + return response; + } +} diff --git a/msal4j-brokers/src/test/java/labapi/User.java b/msal4j-brokers/src/test/java/labapi/User.java new file mode 100644 index 00000000..64584380 --- /dev/null +++ b/msal4j-brokers/src/test/java/labapi/User.java @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package labapi; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.Setter; + +@Getter +public class User { + @JsonProperty("appId") + private String appId; + + @JsonProperty("userType") + private String userType; + + @JsonProperty("upn") + @Setter + private String upn; + + @JsonProperty("homeDomain") + private String homeDomain; + + @JsonProperty("homeUPN") + private String homeUPN; + + @JsonProperty("labName") + private String labName; + + @Setter + private String password; + + @Setter + private String federationProvider; +} diff --git a/msal4j-brokers/src/test/java/labapi/UserQueryParameters.java b/msal4j-brokers/src/test/java/labapi/UserQueryParameters.java new file mode 100644 index 00000000..a5bafe8d --- /dev/null +++ b/msal4j-brokers/src/test/java/labapi/UserQueryParameters.java @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package labapi; + +import java.util.HashMap; +import java.util.Map; + +public class UserQueryParameters { + public static final String FEDERATION_PROVIDER = "federationprovider"; + public static final String AZURE_ENVIRONMENT = "azureenvironment"; + public Map parameters = new HashMap<>(); +} diff --git a/msal4j-brokers/src/test/java/labapi/UserSecret.java b/msal4j-brokers/src/test/java/labapi/UserSecret.java new file mode 100644 index 00000000..ff4b619a --- /dev/null +++ b/msal4j-brokers/src/test/java/labapi/UserSecret.java @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package labapi; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class UserSecret { + @JsonProperty("secret") + String secret; + + @JsonProperty("value") + String value; +} diff --git a/msal4j-brokers/src/test/java/test/ProofOfPossessionTest.java b/msal4j-brokers/src/test/java/test/ProofOfPossessionTest.java new file mode 100644 index 00000000..2f613958 --- /dev/null +++ b/msal4j-brokers/src/test/java/test/ProofOfPossessionTest.java @@ -0,0 +1,184 @@ +package test; + +import com.microsoft.aad.msal4j.*; +import com.microsoft.aad.msal4jbrokers.Broker; +import infrastructure.SeleniumExtensions; +import labapi.LabUserProvider; +import labapi.User; +import org.openqa.selenium.WebDriver; +import org.testng.Assert; + +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.Collections; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ExecutionException; + +public class ProofOfPossessionTest { + private final static String MICROSOFT_AUTHORITY_ORGANIZATIONS = + "https://login.microsoftonline.com/organizations/"; + private final static String GRAPH_DEFAULT_SCOPE = "user.read"; + + private LabUserProvider labUserProvider; + + WebDriver seleniumDriver; + + public void setUp() { + labUserProvider = LabUserProvider.getInstance(); + } + + public void acquirePopToken_WithBroker() throws Exception { + User user = labUserProvider.getDefaultUser(); + + Broker broker = new Broker.Builder().supportWindows(true).build(); + + PublicClientApplication pca = createPublicClientApp(user, broker); + + IAuthenticationResult result = acquirePoPTokenUsernamePassword(pca, user, Collections.singleton(GRAPH_DEFAULT_SCOPE)); + + //A valid PoP access token should be returned if a broker was set + assertTokenResultNotNull(result); + } + + public void acquirePopToken_WithoutBroker() throws Exception { + User user = labUserProvider.getDefaultUser(); + + PublicClientApplication pca = createPublicClientApp(user); + + //Setting UserNamePasswordParameters.proofOfPossession without enabling the broker should result in an exception when trying to get a token + IAuthenticationResult result = acquirePoPTokenUsernamePassword(pca, user, Collections.singleton(GRAPH_DEFAULT_SCOPE)); + } + + public void acquirePopToken_BrowserAndBroker() throws Exception { + User user = labUserProvider.getDefaultUser(); + + seleniumDriver = SeleniumExtensions.createDefaultWebDriver(); + + //First, get a non-PoP (bearer) token through a browser + PublicClientApplication pcaWithoutBroker = createPublicClientApp(user); + + SystemBrowserOptions browserOptions = + SystemBrowserOptions + .builder() + .openBrowserAction(new SeleniumOpenBrowserAction(user, pcaWithoutBroker)) + .build(); + + IAuthenticationResult browserResult = acquireTokenInteractive(pcaWithoutBroker, browserOptions); + + assertTokenResultNotNull(browserResult); + + seleniumDriver.quit(); + + //Then, get a PoP token silently, using the cache that contains the non-PoP token + Broker broker = new Broker.Builder().supportWindows(true).build(); + + PublicClientApplication pcaWithBroker = createPublicClientApp(user, broker, pcaWithoutBroker.tokenCache().serialize()); + + IAuthenticationResult acquireSilentResult = acquireTokenSilent(pcaWithBroker, browserResult.account()); + + //Ensure that the silent request retrieved a new PoP token, rather than the cached non-Pop token + Assert.assertNotNull(acquireSilentResult); + Assert.assertNotEquals(acquireSilentResult.accessToken(), browserResult.accessToken()); + } + + private PublicClientApplication createPublicClientApp(User user) throws MalformedURLException { + return PublicClientApplication.builder(user.getAppId()) + .authority(MICROSOFT_AUTHORITY_ORGANIZATIONS) + .correlationId(UUID.randomUUID().toString()) + .build(); + } + + private PublicClientApplication createPublicClientApp(User user, Broker broker) throws MalformedURLException { + return PublicClientApplication.builder(user.getAppId()) + .authority(MICROSOFT_AUTHORITY_ORGANIZATIONS) + .correlationId(UUID.randomUUID().toString()) + .broker(broker) + .build(); + } + + private PublicClientApplication createPublicClientApp(User user, Broker broker, String cache) throws MalformedURLException { + return PublicClientApplication.builder(user.getAppId()) + .authority(MICROSOFT_AUTHORITY_ORGANIZATIONS) + .correlationId(UUID.randomUUID().toString()) + .setTokenCacheAccessAspect(new TokenPersistence(cache)) + .broker(broker) + .build(); + } + + private IAuthenticationResult acquirePoPTokenUsernamePassword(PublicClientApplication pca, User user, Set scopes) + throws URISyntaxException, ExecutionException, InterruptedException { + UserNamePasswordParameters parameters = UserNamePasswordParameters.builder( + scopes, user.getUpn(), + user.getPassword().toCharArray()) + .proofOfPossession(HttpMethod.GET, new URI("http://localhost"), null) + .build(); + + return pca.acquireToken(parameters).get(); + } + + private IAuthenticationResult acquireTokenInteractive(PublicClientApplication pca, SystemBrowserOptions browserOptions) + throws URISyntaxException { + InteractiveRequestParameters interactiveParams = InteractiveRequestParameters + .builder(new URI("http://localhost:8080")) + .scopes(Collections.singleton(GRAPH_DEFAULT_SCOPE)) + .systemBrowserOptions(browserOptions) + .build(); + + return pca.acquireToken(interactiveParams).join(); + } + + private IAuthenticationResult acquireTokenSilent(PublicClientApplication pca, IAccount account) throws URISyntaxException, MalformedURLException, ExecutionException, InterruptedException { + SilentParameters silentParams = SilentParameters.builder(Collections.singleton(GRAPH_DEFAULT_SCOPE), account) + .proofOfPossession(HttpMethod.GET, new URI("http://localhost"), null) + .build(); + + return pca.acquireTokenSilently(silentParams).get(); + } + + private void assertTokenResultNotNull(IAuthenticationResult result) { + Assert.assertNotNull(result); + Assert.assertNotNull(result.accessToken()); + Assert.assertNotNull(result.idToken()); + } + + 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); + } + } + + void runSeleniumAutomatedLogin(User user, AbstractClientApplicationBase app) { + SeleniumExtensions.performADLogin(seleniumDriver, user); + } + + static class TokenPersistence implements ITokenCacheAccessAspect { + String data; + + TokenPersistence(String data) { + this.data = data; + } + + @Override + public void beforeCacheAccess(ITokenCacheAccessContext iTokenCacheAccessContext) { + iTokenCacheAccessContext.tokenCache().deserialize(data); + } + + @Override + public void afterCacheAccess(ITokenCacheAccessContext iTokenCacheAccessContext) { + data = iTokenCacheAccessContext.tokenCache().serialize(); + } + } +} diff --git a/msal4j-sdk/README.md b/msal4j-sdk/README.md index 474499bc..fac64d38 100644 --- a/msal4j-sdk/README.md +++ b/msal4j-sdk/README.md @@ -16,7 +16,7 @@ Quick links: The library supports the following Java environments: - Java 8 (or higher) -Current version - 1.13.11 +Current version - 1.14.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,12 @@ Find [the latest package in the Maven repository](https://mvnrepository.com/arti com.microsoft.azure msal4j - 1.13.11 - + 1.14.0 ``` ### Gradle ```gradle -compile group: 'com.microsoft.azure', name: 'msal4j', version: '1.13.11' +compile group: 'com.microsoft.azure', name: 'msal4j', version: '1.14.0' ``` ## Usage diff --git a/msal4j-sdk/bnd.bnd b/msal4j-sdk/bnd.bnd index 0a14dff8..e9acb269 100644 --- a/msal4j-sdk/bnd.bnd +++ b/msal4j-sdk/bnd.bnd @@ -1,2 +1,2 @@ -Export-Package: com.microsoft.aad.msal4j;version="1.13.11" +Export-Package: com.microsoft.aad.msal4j;version="1.14.0" Automatic-Module-Name: com.microsoft.aad.msal4j diff --git a/msal4j-sdk/pom.xml b/msal4j-sdk/pom.xml index 847b32e7..6748d4bd 100644 --- a/msal4j-sdk/pom.xml +++ b/msal4j-sdk/pom.xml @@ -3,7 +3,7 @@ 4.0.0 com.microsoft.azure msal4j - 1.13.11 + 1.14.0 jar msal4j diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AuthenticationErrorCode.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AuthenticationErrorCode.java index 22e8299f..0d3f231a 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AuthenticationErrorCode.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AuthenticationErrorCode.java @@ -130,4 +130,15 @@ public class AuthenticationErrorCode { * slow response, and this may be resolvable by increasing timeouts. For more details, see https://aka.ms/msal4j-http-client */ public final static String HTTP_TIMEOUT = "http_timeout"; + + /** + * Indicates an error from the MSAL Java/MSALRuntime interop layer used by the Java Brokers package, + * and will generally just be forwarding an error message from the interop layer or MSALRuntime itself + */ + public final static String MSALRUNTIME_INTEROP_ERROR = "interop_package_error"; + + /** + * Indicates an error related to the MSAL Java Brokers package + */ + public final static String MSALJAVA_BROKERS_ERROR = "brokers_package_error"; } diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AuthenticationResult.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AuthenticationResult.java index dcef23f5..1338467b 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AuthenticationResult.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AuthenticationResult.java @@ -90,4 +90,7 @@ private ITenantProfile getTenantProfile() { private final Date expiresOnDate = new Date(expiresOn * 1000); private final String scopes; + + @Getter(value = AccessLevel.PACKAGE) + private final Boolean isPopAuthorization; } diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/HttpMethod.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/HttpMethod.java index 64af4605..9497ec3c 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/HttpMethod.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/HttpMethod.java @@ -4,17 +4,53 @@ package com.microsoft.aad.msal4j; /** - * Http request method. + * An enumerator representing common HTTP request methods. */ public enum HttpMethod { + /** + * The HTTP CONNECT method. + */ + CONNECT("CONNECT"), + + /** + * The HTTP DELETE method. + */ + DELETE("DELETE"), + /** * The HTTP GET method. */ - GET, + GET("GET"), + + /** + * The HTTP HEAD method. + */ + HEAD("HEAD"), + + /** + * The HTTP OPTIONS method. + */ + OPTIONS("OPTIONS"), /** * The HTTP POST method. */ - POST + POST("POST"), + + /** + * The HTTP PUT method. + */ + PUT("PUT"), + + /** + * The HTTP TRACE method. + */ + TRACE("TRACE"); + + public final String methodName; + + HttpMethod(String methodName) { + this.methodName = methodName; + } } diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/IBroker.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/IBroker.java index 919a8092..ab3f0ce0 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/IBroker.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/IBroker.java @@ -3,58 +3,87 @@ package com.microsoft.aad.msal4j; -import java.util.Set; +import com.nimbusds.jwt.JWTParser; + +import java.net.URL; import java.util.concurrent.CompletableFuture; /** * Used to define the basic set of methods that all Brokers must implement - * - * All methods are so they can be referenced by MSAL Java without an implementation, and by default simply throw an - * exception saying that a broker implementation is missing + *

+ * All methods are marked as default so they can be referenced by MSAL Java without an implementation, + * and most will simply throw an exception if not overridden by an IBroker implementation */ public interface IBroker { - /** - * checks if a IBroker implementation exists - */ - - default boolean isAvailable(){ - return false; - } /** * Acquire a token silently, i.e. without direct user interaction - * + *

* This may be accomplished by returning tokens from a token cache, using cached refresh tokens to get new tokens, * or via any authentication flow where a user is not prompted to enter credentials - * - * @param requestParameters MsalRequest object which contains everything needed for the broker implementation to make a request - * @return IBroker implementations will return an AuthenticationResult object */ - default IAuthenticationResult acquireToken(PublicClientApplication application, SilentParameters requestParameters) { + default CompletableFuture acquireToken(PublicClientApplication application, SilentParameters requestParameters) { throw new MsalClientException("Broker implementation missing", AuthenticationErrorCode.MISSING_BROKER); } /** * Acquire a token interactively, by prompting users to enter their credentials in some way - * - * @param requestParameters MsalRequest object which contains everything needed for the broker implementation to make a request - * @return IBroker implementations will return an AuthenticationResult object */ - default IAuthenticationResult acquireToken(PublicClientApplication application, InteractiveRequestParameters requestParameters) { + default CompletableFuture acquireToken(PublicClientApplication application, InteractiveRequestParameters parameters) { throw new MsalClientException("Broker implementation missing", AuthenticationErrorCode.MISSING_BROKER); } /** * Acquire a token silently, i.e. without direct user interaction, using username/password authentication - * - * @param requestParameters MsalRequest object which contains everything needed for the broker implementation to make a request - * @return IBroker implementations will return an AuthenticationResult object */ - default IAuthenticationResult acquireToken(PublicClientApplication application, UserNamePasswordParameters requestParameters) { + default CompletableFuture acquireToken(PublicClientApplication application, UserNamePasswordParameters parameters) { throw new MsalClientException("Broker implementation missing", AuthenticationErrorCode.MISSING_BROKER); } - default CompletableFuture removeAccount(IAccount account) { + default void removeAccount(PublicClientApplication application, IAccount account) throws MsalClientException { + throw new MsalClientException("Broker implementation missing", AuthenticationErrorCode.MISSING_BROKER); + } + + /** + * Returns whether a broker is available and ready to use on this machine, allowing the use of the methods + * in this interface and other broker-only features in MSAL Java + */ + default boolean isBrokerAvailable() { throw new MsalClientException("Broker implementation missing", AuthenticationErrorCode.MISSING_BROKER); } + + /** + * MSAL Java's AuthenticationResult requires several package-private classes that a broker implementation can't access, + * so this helper method can be used to create AuthenticationResults from within the MSAL Java package + */ + default IAuthenticationResult parseBrokerAuthResult(String authority, String idToken, String accessToken, + String accountId, String clientInfo, + long accessTokenExpirationTime, + boolean isPopAuthorization) { + + AuthenticationResult.AuthenticationResultBuilder builder = AuthenticationResult.builder(); + + try { + if (idToken != null) { + builder.idToken(idToken); + if (accountId != null) { + String idTokenJson = + JWTParser.parse(idToken).getParsedParts()[1].decodeToString(); + builder.accountCacheEntity(AccountCacheEntity.create(clientInfo, + Authority.createAuthority(new URL(authority)), JsonHelper.convertJsonToObject(idTokenJson, + IdToken.class), null)); + } + } + if (accessToken != null) { + builder.accessToken(accessToken); + builder.expiresOn(accessTokenExpirationTime); + } + + builder.isPopAuthorization(isPopAuthorization); + + } catch (Exception e) { + throw new MsalClientException(String.format("Exception when converting broker result to MSAL Java AuthenticationResult: %s", e.getMessage()), AuthenticationErrorCode.MSALJAVA_BROKERS_ERROR); + } + return builder.build(); + } } \ No newline at end of file diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/InteractiveRequestParameters.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/InteractiveRequestParameters.java index 33e89eab..567cb280 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/InteractiveRequestParameters.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/InteractiveRequestParameters.java @@ -105,6 +105,20 @@ public class InteractiveRequestParameters implements IAcquireTokenParameters { */ private boolean instanceAware; + /** + * The parent window handle used to open UI elements with the correct parent + * + * + * For browser scenarios and Windows console applications, this value should not need to be set + * + * For Windows console applications, MSAL Java will attempt to discover the console's window handle if this parameter is not set + * + * For scenarios where MSAL Java is responsible for opening UI elements (such as when using MSALRuntime), this parameter is required and an exception will be thrown if not set + */ + private long windowHandle; + + private PopParameters proofOfPossession; + private static InteractiveRequestParametersBuilder builder() { return new InteractiveRequestParametersBuilder(); } @@ -116,4 +130,23 @@ public static InteractiveRequestParametersBuilder builder(URI redirectUri) { return builder() .redirectUri(redirectUri); } + + //This Builder class is used to override Lombok's default setter behavior for any fields defined in it + public static class InteractiveRequestParametersBuilder { + + /** + * Sets the PopParameters for this request, allowing the request to retrieve proof-of-possession tokens rather than bearer tokens + * + * For more information, see {@link PopParameters} and https://aka.ms/msal4j-pop + * + * @param httpMethod a valid HTTP method, such as "GET" or "POST" + * @param uri the URI on the downstream protected API which the application is trying to access, e.g. https://graph.microsoft.com/beta/me/profile + * @param nonce a string obtained by calling the resource (e.g. Microsoft Graph) un-authenticated and parsing the WWW-Authenticate header associated with pop authentication scheme and extracting the nonce parameter, or, on subsequent calls, by parsing the Autheticate-Info header and extracting the nextnonce parameter. + */ + public InteractiveRequestParametersBuilder proofOfPossession(HttpMethod httpMethod, URI uri, String nonce) { + this.proofOfPossession = new PopParameters(httpMethod, uri, nonce); + + return this; + } + } } diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/PopParameters.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/PopParameters.java new file mode 100644 index 00000000..d72ab5a5 --- /dev/null +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/PopParameters.java @@ -0,0 +1,44 @@ +package com.microsoft.aad.msal4j; + +import java.net.URI; + +/** + * Contains parameters used to request a Proof of Possession (PoP) token in supported flows + */ +public class PopParameters { + + HttpMethod httpMethod; + URI uri; + String nonce; + + public HttpMethod getHttpMethod() { + return httpMethod; + } + + public URI getUri() { + return uri; + } + + public String getNonce() { + return nonce; + } + + PopParameters(HttpMethod httpMethod, URI uri, String nonce) { + validatePopAuthScheme(httpMethod, uri); + + this.httpMethod = httpMethod; + this.uri = uri; + this.nonce = nonce; + } + + /** + * Performs any minimum validation to confirm this auth scheme could be valid for a POP request + */ + void validatePopAuthScheme(HttpMethod httpMethod, URI uri) { + //At a minimum HTTP method and host must be non-null + if (httpMethod == null || uri == null || uri.getHost() == null) { + throw new MsalClientException( + "HTTP method and URI host must be non-null", AuthenticationErrorCode.MSALJAVA_BROKERS_ERROR); + } + } +} diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/PublicClientApplication.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/PublicClientApplication.java index 80fa1c31..e03b9c17 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/PublicClientApplication.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/PublicClientApplication.java @@ -8,6 +8,7 @@ import com.nimbusds.oauth2.sdk.id.ClientID; import org.slf4j.LoggerFactory; +import java.net.MalformedURLException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicReference; @@ -23,6 +24,8 @@ public class PublicClientApplication extends AbstractClientApplicationBase implements IPublicClientApplication { private final ClientAuthenticationPost clientAuthentication; + private IBroker broker; + private boolean brokerEnabled; @Override public CompletableFuture acquireToken(UserNamePasswordParameters parameters) { @@ -35,12 +38,20 @@ public CompletableFuture acquireToken(UserNamePasswordPar parameters, UserIdentifier.fromUpn(parameters.username())); - UserNamePasswordRequest userNamePasswordRequest = - new UserNamePasswordRequest(parameters, - this, - context); + CompletableFuture future; + + if (validateBrokerUsage(parameters)) { + future = broker.acquireToken(this, parameters); + } else { + UserNamePasswordRequest userNamePasswordRequest = + new UserNamePasswordRequest(parameters, + this, + context); - return this.executeRequest(userNamePasswordRequest); + future = this.executeRequest(userNamePasswordRequest); + } + + return future; } @Override @@ -111,17 +122,49 @@ public CompletableFuture acquireToken(InteractiveRequestP this, context); - CompletableFuture future = executeRequest(interactiveRequest); + CompletableFuture future; + + if (validateBrokerUsage(parameters)) { + future = broker.acquireToken(this, parameters); + } else { + future = executeRequest(interactiveRequest); + } + futureReference.set(future); + return future; } + @Override + public CompletableFuture acquireTokenSilently(SilentParameters parameters) throws MalformedURLException { + CompletableFuture future; + + if (validateBrokerUsage(parameters)) { + future = broker.acquireToken(this, parameters); + } else { + future = super.acquireTokenSilently(parameters); + } + + return future; + } + + @Override + public CompletableFuture removeAccount(IAccount account) { + if (brokerEnabled) { + broker.removeAccount(this, account); + } + + return super.removeAccount(account); + } + private PublicClientApplication(Builder builder) { super(builder); validateNotBlank("clientId", clientId()); log = LoggerFactory.getLogger(PublicClientApplication.class); this.clientAuthentication = new ClientAuthenticationPost(ClientAuthenticationMethod.NONE, new ClientID(clientId())); + this.broker = builder.broker; + this.brokerEnabled = builder.brokerEnabled; } @Override @@ -145,6 +188,22 @@ private Builder(String clientId) { super(clientId); } + private IBroker broker = null; + private boolean brokerEnabled = false; + + /** + * Implementation of IBroker that will be used to retrieve tokens + *

+ * Setting this will cause MSAL Java to use the given broker implementation to retrieve tokens from a broker (such as WAM/MSALRuntime) in flows that support it + */ + public PublicClientApplication.Builder broker(IBroker val) { + this.broker = val; + + this.brokerEnabled = this.broker.isBrokerAvailable(); + + return self(); + } + @Override public PublicClientApplication build() { @@ -156,4 +215,61 @@ protected Builder self() { return this; } } + + /** + * Used to determine whether to call into an IBroker instance instead of standard MSAL Java's normal interactive flow, + * and may throw exceptions or log messages if broker-only parameters are used when a broker is not enabled/available + */ + private boolean validateBrokerUsage(InteractiveRequestParameters parameters) { + + //Check if broker-only parameters are being used when a broker is not enabled. If they are, either throw an + // exception saying a broker is required, or provide a clear log message saying the parameter will be ignored + if (!brokerEnabled) { + if (parameters.proofOfPossession() != null) { + throw new MsalClientException( + "InteractiveRequestParameters.proofOfPossession should not be used when broker is not available, see https://aka.ms/msal4j-pop for more information", + AuthenticationErrorCode.MSALJAVA_BROKERS_ERROR ); + } + } + + return brokerEnabled; + } + + /** + * Used to determine whether to call into an IBroker instance instead of standard MSAL Java's normal username/password flow, + * and may throw exceptions or log messages if broker-only parameters are used when a broker is not enabled/available + */ + private boolean validateBrokerUsage(UserNamePasswordParameters parameters) { + + //Check if broker-only parameters are being used when a broker is not enabled. If they are, either throw an + // exception saying a broker is required, or provide a clear log message saying the parameter will be ignored + if (!brokerEnabled) { + if (parameters.proofOfPossession() != null) { + throw new MsalClientException( + "UserNamePasswordParameters.proofOfPossession should not be used when broker is not available, see https://aka.ms/msal4j-pop for more information", + AuthenticationErrorCode.MSALJAVA_BROKERS_ERROR ); + } + } + + return brokerEnabled; + } + + /** + * Used to determine whether to call into an IBroker instance instead of standard MSAL Java's normal silent flow, + * and may throw exceptions or log messages if broker-only parameters are used when a broker is not enabled/available + */ + private boolean validateBrokerUsage(SilentParameters parameters) { + + //Check if broker-only parameters are being used when a broker is not enabled. If they are, either throw an + // exception saying a broker is required, or provide a clear log message saying the parameter will be ignored + if (!brokerEnabled) { + if (parameters.proofOfPossession() != null) { + throw new MsalClientException( + "UserNamePasswordParameters.proofOfPossession should not be used when broker is not available, see https://aka.ms/msal4j-pop for more information", + AuthenticationErrorCode.MSALJAVA_BROKERS_ERROR ); + } + } + + return brokerEnabled; + } } \ No newline at end of file diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/SilentParameters.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/SilentParameters.java index 429c5dbb..c4c4e9fa 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/SilentParameters.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/SilentParameters.java @@ -6,6 +6,7 @@ import lombok.*; import lombok.experimental.Accessors; +import java.net.URI; import java.util.HashSet; import java.util.Map; import java.util.Set; @@ -64,6 +65,8 @@ public class SilentParameters implements IAcquireTokenParameters { */ private String tenant; + private PopParameters proofOfPossession; + private static SilentParametersBuilder builder() { return new SilentParametersBuilder(); @@ -114,4 +117,23 @@ private static Set removeEmptyScope(Set scopes){ } return updatedScopes; } + + //This Builder class is used to override Lombok's default setter behavior for any fields defined in it + public static class SilentParametersBuilder { + + /** + * Sets the PopParameters for this request, allowing the request to retrieve proof-of-possession tokens rather than bearer tokens + * + * For more information, see {@link PopParameters} and https://aka.ms/msal4j-pop + * + * @param httpMethod a valid HTTP method, such as "GET" or "POST" + * @param uri URI to associate with the token + * @param nonce optional nonce value for the token, can be empty or null + */ + public SilentParametersBuilder proofOfPossession(HttpMethod httpMethod, URI uri, String nonce) { + this.proofOfPossession = new PopParameters(httpMethod, uri, nonce); + + return this; + } + } } diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/UserNamePasswordParameters.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/UserNamePasswordParameters.java index cc4dab0c..f9df4454 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/UserNamePasswordParameters.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/UserNamePasswordParameters.java @@ -6,6 +6,7 @@ import lombok.*; import lombok.experimental.Accessors; +import java.net.URI; import java.util.Map; import java.util.Set; @@ -63,6 +64,8 @@ public class UserNamePasswordParameters implements IAcquireTokenParameters { */ private String tenant; + private PopParameters proofOfPossession; + public char[] password() { return password.clone(); } @@ -98,5 +101,20 @@ public UserNamePasswordParametersBuilder password(char[] password) { this.password = password.clone(); return this; } + + /** + * Sets the PopParameters for this request, allowing the request to retrieve proof-of-possession tokens rather than bearer tokens + * + * For more information, see {@link PopParameters} and https://aka.ms/msal4j-pop + * + * @param httpMethod a valid HTTP method, such as "GET" or "POST" + * @param uri URI to associate with the token + * @param nonce optional nonce value for the token, can be empty or null + */ + public UserNamePasswordParametersBuilder proofOfPossession(HttpMethod httpMethod, URI uri, String nonce) { + this.proofOfPossession = new PopParameters(httpMethod, uri, nonce); + + return this; + } } }