diff --git a/.github/ISSUE_TEMPLATE/FeatureRequest.yaml b/.github/ISSUE_TEMPLATE/FeatureRequest.yaml new file mode 100644 index 00000000..a112882e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/FeatureRequest.yaml @@ -0,0 +1,48 @@ +name: Feature request +description: Suggest a new feature for MSAL Java +labels: ["feature request", "untriaged", "needs attention"] +title : '[Feature Request] ' +body: +- type: markdown + attributes: + value: | + ## Before submitting your feature request + Please make sure that your question or issue is not already covered in [MSAL documentation](https://learn.microsoft.com/entra/msal/java/) or [samples](https://learn.microsoft.com/azure/active-directory/develop/sample-v2-code?tabs=apptype). + +- type: markdown + attributes: + value: | + ## Feature request for MSAL Java + +- type: dropdown + attributes: + label: MSAL client type + description: Are you using PublicClientApplication (desktop / CLI apps), ConfidentialClientApplication (web apps, web APIs, service-to-service) or ManagedIdentityApplication? + multiple: true + options: + - "Public" + - "Confidential" + - "Managed identity" + validations: + required: true + +- type: textarea + attributes: + label: Problem Statement + description: "Describe the problem or context for this feature request." + validations: + required: true + +- type: textarea + attributes: + label: Proposed solution + description: "Describe the solution you'd like." + validations: + required: false + +- type: textarea + attributes: + label: Alternatives + description: "Describe alternatives you've considered." + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml new file mode 100644 index 00000000..439b5ef8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -0,0 +1,115 @@ +name: Bug report +description: Broken or unintended behavior with MSAL4J library +title: '[Bug] ' +labels: ["untriaged", "needs attention"] +body: +- type: markdown + attributes: + value: | + ## Before submitting your issue + Please make sure that your question or issue is not already covered in existing issues + + **Logs and network traces** + Without logs or traces, it is unlikely that the team can investigate your issue. Capturing logs is described in our [Docs](https://learn.microsoft.com/azure/active-directory/develop/msal-logging-java). + +- type: markdown + attributes: + value: | + ## Issue details + +- type: input + attributes: + label: Library version used + description: "Enter the version of the library where you ran into the issue (e.g. 1.13.10)." + validations: + required: true + +- type: input + attributes: + label: Java version + description: "Enter the Java SDK and Framework version your application is developed in." + validations: + required: true + +- type: dropdown + attributes: + label: Scenario + description: "Are you using PublicClientApplication, ConfidentialClientApplication or ManagedIdentityApplication?" + multiple: true + options: + - "PublicClient (AcquireTokenInteractive, AcquireTokenByUsernamePassword)" + - "ConfidentialClient - web site (AcquireTokenByAuthCode)" + - "ConfidentialClient - web api (AcquireTokenOnBehalfOf)" + - "ConfidentialClient - service to service (AcquireTokenForClient)" + - "ManagedIdentityClient - managed identity" + - "Other - please specify" + validations: + required: true + +- type: dropdown + attributes: + label: Is this a new or an existing app? + description: "Is this a new or existing app?" + multiple: false + options: + - "The app is in production, and I have upgraded to a new version of MSAL" + - "The app is in production, I haven't upgraded MSAL, but started seeing this issue" + - "This is a new app or experiment" + validations: + required: false + +- type: textarea + attributes: + label: Issue description and reproduction steps + description: "Briefly explain the issue you are seeing along with any error messages or stack trace. Provide a link to one of the [standard samples](https://learn.microsoft.com/azure/active-directory/develop/sample-v2-code?tabs=apptype) and steps to reproduce the behavior. Make sure to provide verbose level log messages from MSAL, if available. [Learn more](https://learn.microsoft.com/azure/active-directory/develop/msal-logging-dotnet)" + validations: + required: true + +- type: textarea + attributes: + label: Relevant code snippets + description: "Provide relevant code snippets that can be used to reproduce the issue." + render: csharp + validations: + required: false + +- type: textarea + attributes: + label: Expected behavior + description: "Describe what you expect the behavior to be." + validations: + required: false + +- type: dropdown + attributes: + label: Identity provider + options: + - Microsoft Entra ID (Work and School accounts and Personal Microsoft accounts) + - Azure B2C Basic Policy + - Azure B2C Custom Policy + - Azure Active Directory Federation Services (ADFS) + - Microsoft Entra External ID + - Other + validations: + required: true + +- type: input + attributes: + label: Regression + description: "If this behavior worked before, enter the last working version(s) of MSAL." + placeholder: "MSAL version: " + +- type: textarea + attributes: + label: Solution and workarounds + description: "Possible solution or workarounds, if you know of any." + validations: + required: false + +- type: markdown + attributes: + value: "## Security Reporting" +- type: markdown + attributes: + value: | + If you find a security issue with our libraries or services [please report it to the Microsoft Security Response Center (MSRC)](https://aka.ms/report-security-issue) with as much detail as possible. Your submission may be eligible for a bounty through the [Microsoft Bounty](http://aka.ms/bugbounty) program. Please do not post security issues to GitHub Issues or any other public site. We will contact you shortly upon receiving the information. We encourage you to get notifications of when security incidents occur by visiting [this page](https://www.microsoft.com/msrc/technical-security-notifications) and subscribing to Security Advisory Alerts. diff --git a/README.md b/README.md index 905e3aff..3b26fbb2 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.14.2-beta +Current version - 1.14.4-beta You can find the changes for each version in the [change log](https://github.com/AzureAD/microsoft-authentication-library-for-java/blob/main/msal4j-sdk/changelog.txt). @@ -28,13 +28,13 @@ Find [the latest package in the Maven repository](https://mvnrepository.com/arti com.microsoft.azure msal4j - 1.14.2-beta + 1.14.4-beta ``` ### Gradle ```gradle -implementation group: 'com.microsoft.azure', name: 'com.microsoft.aad.msal4j', version: '1.14.2-beta' +implementation group: 'com.microsoft.azure', name: 'com.microsoft.aad.msal4j', version: '1.14.4-beta' ``` ## Usage diff --git a/changelog.txt b/changelog.txt index 6dce2cab..81735d47 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,7 +1,30 @@ +Version 1.14.4-beta +============= +- Beta support for MSI in Azure Arc (#730) +- Beta support for MSI in Service Fabric (#729) +- Fix Cloud Shell parsing issue (#750) + +Version 1.14.0 +============= +- 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.2-beta ============= - Add support for Managed Identity (#712) +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 ============= - Remove default HTTP timeout (#664) 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 4f801be3..d2e0069c 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.14.2-beta +Current version - 1.14.4-beta 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.14.2-beta + 1.14.4-beta ``` ### Gradle ```gradle -compile group: 'com.microsoft.azure', name: 'msal4j', version: '1.14.2-beta' +compile group: 'com.microsoft.azure', name: 'msal4j', version: '1.14.4-beta' ``` ## Usage diff --git a/msal4j-sdk/bnd.bnd b/msal4j-sdk/bnd.bnd index c9609926..b8e2c6a8 100644 --- a/msal4j-sdk/bnd.bnd +++ b/msal4j-sdk/bnd.bnd @@ -1,2 +1,2 @@ -Export-Package: com.microsoft.aad.msal4j;version="1.14.2-beta" +Export-Package: com.microsoft.aad.msal4j;version="1.14.4-beta" Automatic-Module-Name: com.microsoft.aad.msal4j diff --git a/msal4j-sdk/pom.xml b/msal4j-sdk/pom.xml index 475a9a87..a1dda198 100644 --- a/msal4j-sdk/pom.xml +++ b/msal4j-sdk/pom.xml @@ -3,7 +3,7 @@ 4.0.0 com.microsoft.azure msal4j - 1.14.2-beta + 1.14.4-beta jar msal4j @@ -169,6 +169,7 @@ + ${project.build.directory}/delombok org.projectlombok 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 9233fa0a..5c568831 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 @@ -131,6 +131,17 @@ public class AuthenticationErrorCode { */ 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"; + /** * Indicates that a request to managed identity endpoint failed, see error message for detailed reason and correlation id. * For more information on managed identity see https://aka.ms/msal4j-managed-identity. 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 ebbe6239..ad6ceabd 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 @@ -93,4 +93,7 @@ private ITenantProfile getTenantProfile() { @Builder.Default private final AuthenticationResultMetadata metadata = new AuthenticationResultMetadata(); + + @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 dfdcc5e9..b0abc93b 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; this.tenant = this.authenticationAuthority.tenant; } @@ -146,6 +189,22 @@ private Builder(String clientId) { super(clientId); } + private IBroker broker = null; + private boolean brokerEnabled = false; + + /** + * Implementation of IBroker that will be used to retrieve tokens + *

+ * Setting this will cause MSAL Java to use the given broker implementation to retrieve tokens from a broker (such as WAM/MSALRuntime) in flows that support it + */ + public PublicClientApplication.Builder broker(IBroker val) { + this.broker = val; + + this.brokerEnabled = this.broker.isBrokerAvailable(); + + return self(); + } + @Override public PublicClientApplication build() { @@ -157,4 +216,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 37c4395a..2b8a0dea 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/ThrottlingCache.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ThrottlingCache.java index 89e1f348..9c8a0584 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ThrottlingCache.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ThrottlingCache.java @@ -12,7 +12,7 @@ class ThrottlingCache { static final int MAX_THROTTLING_TIME_SEC = 3600; - static int DEFAULT_THROTTLING_TIME_SEC = 120; + static int DEFAULT_THROTTLING_TIME_SEC = 5; static final int CACHE_SIZE_LIMIT_TO_TRIGGER_EXPIRED_ENTITIES_REMOVAL = 100; // request hash to expiration timestamp 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; + } } } diff --git a/msal4j-sdk/src/samples/msal-b2c-web-sample/pom.xml b/msal4j-sdk/src/samples/msal-b2c-web-sample/pom.xml index 923e2b35..4b3e6e24 100644 --- a/msal4j-sdk/src/samples/msal-b2c-web-sample/pom.xml +++ b/msal4j-sdk/src/samples/msal-b2c-web-sample/pom.xml @@ -23,7 +23,7 @@ com.microsoft.azure msal4j - 1.14.2-beta + 1.14.4-beta com.nimbusds @@ -33,7 +33,7 @@ org.json json - 20230227 + 20231013 diff --git a/msal4j-sdk/src/samples/msal-obo-sample/pom.xml b/msal4j-sdk/src/samples/msal-obo-sample/pom.xml index 0a475bf3..fd510aa7 100644 --- a/msal4j-sdk/src/samples/msal-obo-sample/pom.xml +++ b/msal4j-sdk/src/samples/msal-obo-sample/pom.xml @@ -23,7 +23,7 @@ com.microsoft.azure msal4j - 1.14.2-beta + 1.14.4-beta com.nimbusds @@ -33,7 +33,7 @@ org.json json - 20230227 + 20231013 org.projectlombok diff --git a/msal4j-sdk/src/samples/msal-web-sample/pom.xml b/msal4j-sdk/src/samples/msal-web-sample/pom.xml index 1be31dd1..d0f33948 100644 --- a/msal4j-sdk/src/samples/msal-web-sample/pom.xml +++ b/msal4j-sdk/src/samples/msal-web-sample/pom.xml @@ -23,7 +23,7 @@ com.microsoft.azure msal4j - 1.14.2-beta + 1.14.4-beta com.nimbusds @@ -33,7 +33,7 @@ org.json json - 20230227 + 20231013 org.apache.commons