diff --git a/README.md b/README.md index 0ea970a4..c10eac3b 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Quick links: The library supports the following Java environments: - Java 8 (or higher) -Current version - 1.3.0 +Current version - 1.4.0 You can find the changes for each version in the [change log](https://github.com/AzureAD/microsoft-authentication-library-for-java/blob/master/changelog.txt). @@ -28,13 +28,13 @@ Find [the latest package in the Maven repository](https://mvnrepository.com/arti com.microsoft.azure msal4j - 1.3.0 + 1.4.0 ``` ### Gradle ``` -compile group: 'com.microsoft.azure', name: 'msal4j', version: '1.3.0' +compile group: 'com.microsoft.azure', name: 'msal4j', version: '1.4.0' ``` ## Usage diff --git a/RELEASES.md b/RELEASES.md deleted file mode 100644 index e69de29b..00000000 diff --git a/bnd.bnd b/bnd.bnd new file mode 100644 index 00000000..9cb45beb --- /dev/null +++ b/bnd.bnd @@ -0,0 +1 @@ +Export-Package: com.microsoft.aad.msal4j diff --git a/build/credscan-exclude.json b/build/credscan-exclude.json index 9f90dfe0..e639934e 100644 --- a/build/credscan-exclude.json +++ b/build/credscan-exclude.json @@ -12,6 +12,14 @@ { "placeholder": "ClientPassword", "_justification" : "credential used for testing. not associated with any tenant" + }, + { + "placeholder": "B2C_CONFIDENTIAL_CLIENT_APP_SECRET", + "_justification" : "Not a credential, just the identifier of the secret exposed by test lab API" + }, + { + "placeholder": "MSIDLABB2C-MSAapp-AppSecret", + "_justification" : "Not a credential, just the identifier of the secret exposed by test lab API" } ] } \ No newline at end of file diff --git a/changelog.txt b/changelog.txt index edeab1d3..99823496 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,10 @@ +Version 1.4.0 +============= +- Added acquire token interactive API, using system default browser +- Added authorization code url builder +- Added OSGi support via bnd-maven-plugin +- Added server-side telemetry support + Version 1.3.0 ============= - Added option to pass in AAD instance discovery data diff --git a/pom.xml b/pom.xml index 40bb2d4b..2d67ed79 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 com.microsoft.azure msal4j - 1.3.0 + 1.4.0 jar msal4j @@ -162,6 +162,7 @@ true true + ${project.build.outputDirectory}/META-INF/MANIFEST.MF @@ -249,6 +250,18 @@ + + biz.aQute.bnd + bnd-maven-plugin + 4.3.1 + + + + bnd-process + + + + diff --git a/src/integrationtest/java/com.microsoft.aad.msal4j/AcquireTokenInteractiveIT.java b/src/integrationtest/java/com.microsoft.aad.msal4j/AcquireTokenInteractiveIT.java new file mode 100644 index 00000000..185a4cdb --- /dev/null +++ b/src/integrationtest/java/com.microsoft.aad.msal4j/AcquireTokenInteractiveIT.java @@ -0,0 +1,170 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.aad.msal4j; + +import labapi.B2CProvider; +import labapi.FederationProvider; +import labapi.User; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; +import java.util.Collections; + +public class AcquireTokenInteractiveIT extends SeleniumTest { + + private final static Logger LOG = LoggerFactory.getLogger(AuthorizationCodeIT.class); + + @Test + public void acquireTokenInteractive_ManagedUser(){ + User user = labUserProvider.getDefaultUser(); + assertAcquireTokenAAD(user); + } + + @Test + public void acquireTokenInteractive_ADFSv2019_OnPrem(){ + User user = labUserProvider.getOnPremAdfsUser(FederationProvider.ADFS_2019); + assertAcquireTokenADFS2019(user); + } + + @Test + public void acquireTokenInteractive_ADFSv2019_Federated(){ + User user = labUserProvider.getFederatedAdfsUser(FederationProvider.ADFS_2019); + assertAcquireTokenAAD(user); + } + + @Test + public void acquireTokenInteractive_ADFSv4_Federated(){ + User user = labUserProvider.getFederatedAdfsUser(FederationProvider.ADFS_4); + assertAcquireTokenAAD(user); + } + + @Test + public void acquireTokenInteractive_ADFSv3_Federated(){ + User user = labUserProvider.getFederatedAdfsUser(FederationProvider.ADFS_3); + assertAcquireTokenAAD(user); + } + + @Test + public void acquireTokenInteractive_ADFSv2_Federated(){ + User user = labUserProvider.getFederatedAdfsUser(FederationProvider.ADFS_2); + assertAcquireTokenAAD(user); + } + + @Test + public void acquireTokenWithAuthorizationCode_B2C_Local(){ + User user = labUserProvider.getB2cUser(B2CProvider.LOCAL); + assertAcquireTokenB2C(user); + } + + private void assertAcquireTokenAAD(User user){ + + PublicClientApplication pca; + try { + pca = PublicClientApplication.builder( + user.getAppId()). + authority(TestConstants.ORGANIZATIONS_AUTHORITY). + build(); + } catch(MalformedURLException ex){ + throw new RuntimeException(ex.getMessage()); + } + + IAuthenticationResult result = acquireTokenInteractive( + user, + pca, + TestConstants.GRAPH_DEFAULT_SCOPE); + + Assert.assertNotNull(result); + Assert.assertNotNull(result.accessToken()); + Assert.assertNotNull(result.idToken()); + Assert.assertEquals(user.getUpn(), result.account().username()); + } + + private void assertAcquireTokenADFS2019(User user){ + PublicClientApplication pca; + try { + pca = PublicClientApplication.builder( + TestConstants.ADFS_APP_ID). + authority(TestConstants.ADFS_AUTHORITY). + build(); + } catch(MalformedURLException ex){ + throw new RuntimeException(ex.getMessage()); + } + + IAuthenticationResult result = acquireTokenInteractive(user, pca, TestConstants.ADFS_SCOPE); + + Assert.assertNotNull(result); + Assert.assertNotNull(result.accessToken()); + Assert.assertNotNull(result.idToken()); + Assert.assertEquals(user.getUpn(), result.account().username()); + } + + private void assertAcquireTokenB2C(User user){ + + PublicClientApplication pca; + try { + pca = PublicClientApplication.builder( + user.getAppId()). + b2cAuthority(TestConstants.B2C_AUTHORITY_SIGN_IN). + build(); + } catch(MalformedURLException ex){ + throw new RuntimeException(ex.getMessage()); + } + + IAuthenticationResult result = acquireTokenInteractive(user, pca, user.getAppId()); + Assert.assertNotNull(result); + Assert.assertNotNull(result.accessToken()); + Assert.assertNotNull(result.idToken()); + } + + private IAuthenticationResult acquireTokenInteractive( + User user, + PublicClientApplication pca, + String scope){ + + IAuthenticationResult result; + try { + URI url = new URI("http://localhost:8080"); + + SystemBrowserOptions browserOptions = + SystemBrowserOptions + .builder() + .openBrowserAction(new SeleniumOpenBrowserAction(user, pca)) + .build(); + + InteractiveRequestParameters parameters = InteractiveRequestParameters + .builder(url) + .scopes(Collections.singleton(scope)) + .systemBrowserOptions(browserOptions) + .build(); + + result = pca.acquireToken(parameters).get(); + + } catch(Exception e){ + LOG.error("Error acquiring token with authCode: " + e.getMessage()); + throw new RuntimeException("Error acquiring token with authCode: " + e.getMessage()); + } + return result; + } + + class SeleniumOpenBrowserAction implements OpenBrowserAction { + + private User user; + private PublicClientApplication pca; + + SeleniumOpenBrowserAction(User user, PublicClientApplication pca){ + this.user = user; + this.pca = pca; + } + + public void openBrowser(URL url){ + seleniumDriver.navigate().to(url); + runSeleniumAutomatedLogin(user, pca); + } + } +} diff --git a/src/integrationtest/java/com.microsoft.aad.msal4j/AuthorizationCodeIT.java b/src/integrationtest/java/com.microsoft.aad.msal4j/AuthorizationCodeIT.java index c84e4a3d..33b40d7a 100644 --- a/src/integrationtest/java/com.microsoft.aad.msal4j/AuthorizationCodeIT.java +++ b/src/integrationtest/java/com.microsoft.aad.msal4j/AuthorizationCodeIT.java @@ -3,138 +3,63 @@ package com.microsoft.aad.msal4j; -import infrastructure.SeleniumExtensions; -import infrastructure.TcpListener; import labapi.*; -import org.openqa.selenium.WebDriver; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.testng.Assert; -import org.testng.annotations.AfterMethod; -import org.testng.annotations.BeforeClass; -import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; -import org.testng.util.Strings; -import java.io.UnsupportedEncodingException; +import java.net.MalformedURLException; import java.net.URI; -import java.net.URLEncoder; import java.util.Collections; import java.util.Set; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -public class AuthorizationCodeIT { +public class AuthorizationCodeIT extends SeleniumTest { private final static Logger LOG = LoggerFactory.getLogger(AuthorizationCodeIT.class); - private LabUserProvider labUserProvider; - private WebDriver seleniumDriver; - private TcpListener tcpListener; - private BlockingQueue AuthorizationCodeQueue; - - @BeforeClass - public void setUpLapUserProvider() { - labUserProvider = LabUserProvider.getInstance(); - } - - @AfterMethod - public void cleanUp(){ - seleniumDriver.quit(); - if(AuthorizationCodeQueue != null){ - AuthorizationCodeQueue.clear(); - } - tcpListener.close(); - } - - @BeforeMethod - public void startUpBrowser(){ - seleniumDriver = SeleniumExtensions.createDefaultWebDriver(); - } - @Test public void acquireTokenWithAuthorizationCode_ManagedUser(){ User user = labUserProvider.getDefaultUser(); - assertAcquireTokenAAD(user); } @Test - public void acquireTokenWithAuthorizationCode_ADFSv2019_Federated(){ - UserQueryParameters query = new UserQueryParameters(); - query.parameters.put(UserQueryParameters.FEDERATION_PROVIDER, FederationProvider.ADFS_2019); - query.parameters.put(UserQueryParameters.USER_TYPE, UserType.FEDERATED); - - User user = labUserProvider.getLabUser(query); - - assertAcquireTokenAAD(user); + public void acquireTokenWithAuthorizationCode_ADFSv2019_OnPrem(){ + User user = labUserProvider.getOnPremAdfsUser(FederationProvider.ADFS_2019); + assertAcquireTokenADFS2019(user); } @Test - public void acquireTokenWithAuthorizationCode_ADFSv2019_OnPrem(){ - UserQueryParameters query = new UserQueryParameters(); - query.parameters.put(UserQueryParameters.FEDERATION_PROVIDER, FederationProvider.ADFS_2019); - query.parameters.put(UserQueryParameters.USER_TYPE, UserType.ON_PREM); - - User user = labUserProvider.getLabUser(query); - - assertAcquireTokenADFS2019(user); + public void acquireTokenWithAuthorizationCode_ADFSv2019_Federated(){ + User user = labUserProvider.getFederatedAdfsUser(FederationProvider.ADFS_2019); + assertAcquireTokenAAD(user); } @Test public void acquireTokenWithAuthorizationCode_ADFSv4_Federated(){ - UserQueryParameters query = new UserQueryParameters(); - query.parameters.put(UserQueryParameters.FEDERATION_PROVIDER, FederationProvider.ADFS_4); - query.parameters.put(UserQueryParameters.USER_TYPE, UserType.FEDERATED); - - User user = labUserProvider.getLabUser(query); + User user = labUserProvider.getFederatedAdfsUser(FederationProvider.ADFS_4); assertAcquireTokenAAD(user); } @Test public void acquireTokenWithAuthorizationCode_ADFSv3_Federated(){ - UserQueryParameters query = new UserQueryParameters(); - query.parameters.put(UserQueryParameters.FEDERATION_PROVIDER, FederationProvider.ADFS_3); - query.parameters.put(UserQueryParameters.USER_TYPE, UserType.FEDERATED); - - User user = labUserProvider.getLabUser(query); - + User user = labUserProvider.getFederatedAdfsUser(FederationProvider.ADFS_3); assertAcquireTokenAAD(user); } @Test public void acquireTokenWithAuthorizationCode_ADFSv2_Federated(){ - UserQueryParameters query = new UserQueryParameters(); - query.parameters.put(UserQueryParameters.FEDERATION_PROVIDER, FederationProvider.ADFS_2); - query.parameters.put(UserQueryParameters.USER_TYPE, UserType.FEDERATED); - - User user = labUserProvider.getLabUser(query); - + User user = labUserProvider.getFederatedAdfsUser(FederationProvider.ADFS_2); assertAcquireTokenAAD(user); } - //@Test - // TODO Redirect URI localhost in not registered + @Test public void acquireTokenWithAuthorizationCode_B2C_Local(){ -/* User labResponse = labUserProvider.getLabUser( - B2CIdentityProvider.LOCAL, - false); - labUserProvider.getUserPassword(labResponse.getUser()); - - String b2CAppId = "b876a048-55a5-4fc5-9403-f5d90cb1c852"; - labResponse.setAppId(b2CAppId);*/ - - UserQueryParameters query = new UserQueryParameters(); - query.parameters.put(UserQueryParameters.USER_TYPE, UserType.B2C); - query.parameters.put(UserQueryParameters.B2C_PROVIDER, B2CProvider.LOCAL); - User user = labUserProvider.getLabUser(query); - - /*String b2CAppId = "b876a048-55a5-4fc5-9403-f5d90cb1c852"; - labResponse.setAppId(b2CAppId); -*/ + User user = labUserProvider.getB2cUser(B2CProvider.LOCAL); assertAcquireTokenB2C(user); } @@ -149,13 +74,7 @@ public void acquireTokenWithAuthorizationCode_B2C_Google(){ String b2CAppId = "b876a048-55a5-4fc5-9403-f5d90cb1c852"; labResponse.setAppId(b2CAppId);*/ - - UserQueryParameters query = new UserQueryParameters(); - query.parameters.put(UserQueryParameters.USER_TYPE, UserType.B2C); - query.parameters.put(UserQueryParameters.B2C_PROVIDER, B2CProvider.GOOGLE); - - User user = labUserProvider.getLabUser(query); - + User user = labUserProvider.getB2cUser(B2CProvider.GOOGLE); assertAcquireTokenB2C(user); } @@ -170,20 +89,27 @@ public void acquireTokenWithAuthorizationCode_B2C_Facebook(){ String b2CAppId = "b876a048-55a5-4fc5-9403-f5d90cb1c852"; labResponse.setAppId(b2CAppId);*/ - UserQueryParameters query = new UserQueryParameters(); - query.parameters.put(UserQueryParameters.USER_TYPE, UserType.B2C); - query.parameters.put(UserQueryParameters.B2C_PROVIDER, B2CProvider.FACEBOOK); - - User user = labUserProvider.getLabUser(query); + User user = labUserProvider.getB2cUser(B2CProvider.FACEBOOK); assertAcquireTokenB2C(user); } private void assertAcquireTokenADFS2019(User user){ - String authCode = acquireAuthorizationCodeAutomated(user, AuthorityType.ADFS); - IAuthenticationResult result = acquireTokenInteractive(authCode, - TestConstants.ADFS_AUTHORITY, Collections.singleton(TestConstants.ADFS_SCOPE), - TestConstants.ADFS_APP_ID); + PublicClientApplication pca; + try { + pca = PublicClientApplication.builder( + TestConstants.ADFS_APP_ID). + authority(TestConstants.ADFS_AUTHORITY). + build(); + } catch(MalformedURLException ex){ + throw new RuntimeException(ex.getMessage()); + } + + String authCode = acquireAuthorizationCodeAutomated(user, pca); + IAuthenticationResult result = acquireTokenAuthorizationCodeFlow( + pca, + authCode, + Collections.singleton(TestConstants.ADFS_SCOPE)); Assert.assertNotNull(result); Assert.assertNotNull(result.accessToken()); @@ -192,8 +118,22 @@ private void assertAcquireTokenADFS2019(User user){ } private void assertAcquireTokenAAD(User user){ - String authCode = acquireAuthorizationCodeAutomated(user, AuthorityType.AAD); - IAuthenticationResult result = acquireTokenInteractiveAAD(user, authCode); + + PublicClientApplication pca; + try { + pca = PublicClientApplication.builder( + user.getAppId()). + authority(TestConstants.ORGANIZATIONS_AUTHORITY). + build(); + } catch(MalformedURLException ex){ + throw new RuntimeException(ex.getMessage()); + } + + String authCode = acquireAuthorizationCodeAutomated(user, pca); + IAuthenticationResult result = acquireTokenAuthorizationCodeFlow( + pca, + authCode, + Collections.singleton(TestConstants.GRAPH_DEFAULT_SCOPE)); Assert.assertNotNull(result); Assert.assertNotNull(result.accessToken()); @@ -202,31 +142,39 @@ private void assertAcquireTokenAAD(User user){ } private void assertAcquireTokenB2C(User user){ - String authCode = acquireAuthorizationCodeAutomated(user, AuthorityType.B2C); - IAuthenticationResult result = acquireTokenInteractiveB2C(user, authCode); + + String appId = LabService.getSecret(TestConstants.B2C_CONFIDENTIAL_CLIENT_LAB_APP_ID); + String appSecret = LabService.getSecret(TestConstants.B2C_CONFIDENTIAL_CLIENT_APP_SECRET); + + ConfidentialClientApplication cca; + try { + IClientCredential credential = ClientCredentialFactory.createFromSecret(appSecret); + cca = ConfidentialClientApplication + .builder(appId, credential) + .b2cAuthority(TestConstants.B2C_AUTHORITY_SIGN_IN) + .build(); + } catch(Exception ex){ + throw new RuntimeException(ex.getMessage()); + } + + String authCode = acquireAuthorizationCodeAutomated(user, cca); + IAuthenticationResult result = acquireTokenInteractiveB2C(cca, authCode); Assert.assertNotNull(result); Assert.assertNotNull(result.accessToken()); Assert.assertNotNull(result.idToken()); - Assert.assertEquals(user.getUpn(), result.account().username()); } - private IAuthenticationResult acquireTokenInteractive( + private IAuthenticationResult acquireTokenAuthorizationCodeFlow( + PublicClientApplication pca, String authCode, - String authority, - Set scopes, - String clientId){ + Set scopes){ IAuthenticationResult result; try { - PublicClientApplication pca = PublicClientApplication.builder( - clientId). - authority(authority). - build(); - result = pca.acquireToken(AuthorizationCodeParameters .builder(authCode, - new URI(TestConstants.LOCALHOST + tcpListener.getPort())) + new URI(TestConstants.LOCALHOST + httpListener.port())) .scopes(scopes) .build()) .get(); @@ -238,29 +186,12 @@ private IAuthenticationResult acquireTokenInteractive( return result; } - private IAuthenticationResult acquireTokenInteractiveAAD( - User user, - String authCode){ - return acquireTokenInteractive(authCode, - TestConstants.ORGANIZATIONS_AUTHORITY, - Collections.singleton(TestConstants.GRAPH_DEFAULT_SCOPE), - user.getAppId()); - } - - private IAuthenticationResult acquireTokenInteractiveB2C(User user, + private IAuthenticationResult acquireTokenInteractiveB2C(ConfidentialClientApplication cca, String authCode) { IAuthenticationResult result; try{ - IClientCredential credential = ClientCredentialFactory.createFromSecret(""); - ConfidentialClientApplication cca = ConfidentialClientApplication.builder( - user.getAppId(), - credential) - .b2cAuthority(TestConstants.B2C_AUTHORITY_SIGN_IN) - .build(); - - result = cca.acquireToken(AuthorizationCodeParameters.builder( - authCode, - new URI(TestConstants.LOCALHOST + tcpListener.getPort())) + result = cca.acquireToken(AuthorizationCodeParameters + .builder(authCode, new URI(TestConstants.LOCALHOST + httpListener.port())) .scopes(Collections.singleton(TestConstants.B2C_LAB_SCOPE)) .build()) .get(); @@ -271,137 +202,68 @@ private IAuthenticationResult acquireTokenInteractiveB2C(User user, return result; } - private String acquireAuthorizationCodeAutomated( User user, - AuthorityType authorityType){ - BlockingQueue tcpStartUpNotificationQueue = new LinkedBlockingQueue<>(); - startTcpListener(tcpStartUpNotificationQueue); + ClientApplicationBase app){ - String authServerResponse; - try { - Boolean tcpListenerStarted = tcpStartUpNotificationQueue.poll( - 30, - TimeUnit.SECONDS); - if (tcpListenerStarted == null || !tcpListenerStarted){ - throw new RuntimeException("Could not start TCP listener"); - } - runSeleniumAutomatedLogin(user, authorityType); - String page = seleniumDriver.getPageSource(); - authServerResponse = getResponseFromTcpListener(); - } catch(Exception e){ - if(!Strings.isNullOrEmpty( - System.getenv(TestConstants.LOCAL_FLAG_ENV_VAR))){ - SeleniumExtensions.takeScreenShot(seleniumDriver); - } - LOG.error("Error running automated selenium login: " + e.getMessage()); - throw new RuntimeException("Error running automated selenium login: " + e.getMessage()); - } - return parseServerResponse(authServerResponse,authorityType); - } + BlockingQueue authorizationCodeQueue = new LinkedBlockingQueue<>(); - private void runSeleniumAutomatedLogin(User user, AuthorityType authorityType) - throws UnsupportedEncodingException{ - String url = buildAuthenticationCodeURL(user.getAppId(), authorityType); - seleniumDriver.navigate().to(url); - - if(authorityType == AuthorityType.B2C){ - switch(user.getB2cProvider().toLowerCase()){ - case B2CProvider.LOCAL: - SeleniumExtensions.performLocalLogin(seleniumDriver, user); - break; - case B2CProvider.GOOGLE: - SeleniumExtensions.performGoogleLogin(seleniumDriver, user); - break; - case B2CProvider.FACEBOOK: - SeleniumExtensions.performFacebookLogin(seleniumDriver, user); - break; - } - } else if (authorityType == AuthorityType.AAD) { - SeleniumExtensions.performADLogin(seleniumDriver, user); - } - else if (authorityType == AuthorityType.ADFS) { - SeleniumExtensions.performADFS2019Login(seleniumDriver, user); - } - } + AuthorizationResponseHandler authorizationResponseHandler = new AuthorizationResponseHandler( + authorizationCodeQueue, + SystemBrowserOptions.builder().build()); - private void startTcpListener(BlockingQueue tcpStartUpNotifierQueue){ - AuthorizationCodeQueue = new LinkedBlockingQueue<>(); - tcpListener = new TcpListener(AuthorizationCodeQueue, tcpStartUpNotifierQueue); - tcpListener.startServer(); - } + httpListener = new HttpListener(); + httpListener.startListener(8080, authorizationResponseHandler); - private String getResponseFromTcpListener(){ - String response; + AuthorizationResult result = null; try { - response = AuthorizationCodeQueue.poll(30, TimeUnit.SECONDS); - if (Strings.isNullOrEmpty(response)){ - LOG.error("Server response is null"); - throw new NullPointerException("Server response is null"); + String url = buildAuthenticationCodeURL(app); + seleniumDriver.navigate().to(url); + runSeleniumAutomatedLogin(user, app); + + long expirationTime = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()) + 120; + + while(result == null && + TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()) < expirationTime) { + + result = authorizationCodeQueue.poll(100, TimeUnit.MILLISECONDS); } } catch(Exception e){ - LOG.error("Error reading from server response AuthorizationCodeQueue: " + e.getMessage()); - throw new RuntimeException("Error reading from server response AuthorizationCodeQueue: " + - e.getMessage()); + throw new MsalClientException(e); + } finally { + if(httpListener != null){ + httpListener.stopListener(); + } } - return response; - } - private String parseServerResponse(String serverResponse, AuthorityType authorityType){ - // Response will be a GET request with query parameter ?code=authCode - String regexp; - if(authorityType == AuthorityType.B2C){ - regexp = "(?<=code=)(?:(?! HTTP).)*"; - } else { - regexp = "(?<=code=)(?:(?!&).)*"; + if (result == null || StringHelper.isBlank(result.code())) { + throw new MsalClientException("No Authorization code was returned from the server", + AuthenticationErrorCode.INVALID_AUTHORIZATION_RESULT); } - - Pattern pattern = Pattern.compile(regexp); - Matcher matcher = pattern.matcher(serverResponse); - - if(!matcher.find()){ - LOG.error("No authorization code in server response: " + serverResponse); - throw new IllegalStateException("No authorization code in server response: " + - serverResponse); - } - return matcher.group(0); + return result.code(); } - - private String buildAuthenticationCodeURL(String appId, AuthorityType authorityType) - throws UnsupportedEncodingException{ - String redirectUrl; - int portNumber = tcpListener.getPort(); - - String authority; + private String buildAuthenticationCodeURL(ClientApplicationBase app) { String scope; + + AuthorityType authorityType= app.authenticationAuthority.authorityType; if(authorityType == AuthorityType.AAD){ - authority = TestConstants.ORGANIZATIONS_AUTHORITY; scope = TestConstants.GRAPH_DEFAULT_SCOPE; } else if (authorityType == AuthorityType.B2C) { - authority = TestConstants.B2C_AUTHORITY_URL; scope = TestConstants.B2C_LAB_SCOPE; } else if (authorityType == AuthorityType.ADFS){ - authority = TestConstants.ADFS_AUTHORITY; scope = TestConstants.ADFS_SCOPE; } else{ - return null; + throw new RuntimeException("Authority type not recognized"); } - redirectUrl = authority + "oauth2/" + - (authorityType != AuthorityType.ADFS ? "v2.0/" : "") + - "authorize?" + - "response_type=code" + - "&response_mode=query" + - "&client_id=" + (authorityType == AuthorityType.ADFS ? TestConstants.ADFS_APP_ID : appId) + - "&redirect_uri=" + URLEncoder.encode(TestConstants.LOCALHOST + portNumber, "UTF-8") + - "&scope=" + URLEncoder.encode("openid offline_access profile " + scope, "UTF-8"); - - if(authorityType == AuthorityType.B2C){ - redirectUrl = redirectUrl + "&p=" + TestConstants.B2C_SIGN_IN_POLICY; - } + AuthorizationRequestUrlParameters parameters = + AuthorizationRequestUrlParameters + .builder(TestConstants.LOCALHOST + httpListener.port(), + Collections.singleton(scope)) + .build(); - return redirectUrl; + return app.getAuthorizationRequestUrl(parameters).toString(); } } diff --git a/src/integrationtest/java/com.microsoft.aad.msal4j/DeviceCodeIT.java b/src/integrationtest/java/com.microsoft.aad.msal4j/DeviceCodeIT.java index 2c7a02e3..dfe85c8c 100644 --- a/src/integrationtest/java/com.microsoft.aad.msal4j/DeviceCodeIT.java +++ b/src/integrationtest/java/com.microsoft.aad.msal4j/DeviceCodeIT.java @@ -39,7 +39,7 @@ public void DeviceCodeFlowTest() throws Exception { PublicClientApplication pca = PublicClientApplication.builder( user.getAppId()). authority(TestConstants.ORGANIZATIONS_AUTHORITY). - build(); + build(); Consumer deviceCodeConsumer = (DeviceCode deviceCode) -> { runAutomatedDeviceCodeFlow(deviceCode, user); @@ -56,8 +56,8 @@ public void DeviceCodeFlowTest() throws Exception { } private void runAutomatedDeviceCodeFlow(DeviceCode deviceCode, User user){ - boolean isRunningLocally = true; /*!Strings.isNullOrEmpty( - System.getenv(TestConstants.LOCAL_FLAG_ENV_VAR));*/ + boolean isRunningLocally = !Strings.isNullOrEmpty( + System.getenv(TestConstants.LOCAL_FLAG_ENV_VAR)); LOG.info("Device code running locally: " + isRunningLocally); try{ diff --git a/src/integrationtest/java/com.microsoft.aad.msal4j/SeleniumTest.java b/src/integrationtest/java/com.microsoft.aad.msal4j/SeleniumTest.java new file mode 100644 index 00000000..84634a41 --- /dev/null +++ b/src/integrationtest/java/com.microsoft.aad.msal4j/SeleniumTest.java @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.aad.msal4j; + +import infrastructure.SeleniumExtensions; +import labapi.B2CProvider; +import labapi.LabUserProvider; +import labapi.User; +import org.openqa.selenium.WebDriver; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; + +abstract class SeleniumTest { + + protected LabUserProvider labUserProvider; + WebDriver seleniumDriver; + HttpListener httpListener; + + @BeforeClass + public void setUpLapUserProvider() { + labUserProvider = LabUserProvider.getInstance(); + } + + @AfterMethod + public void cleanUp(){ + seleniumDriver.quit(); + if(httpListener != null) { + httpListener.stopListener(); + } + } + + @BeforeMethod + public void startUpBrowser(){ + seleniumDriver = SeleniumExtensions.createDefaultWebDriver(); + } + + void runSeleniumAutomatedLogin(User user, ClientApplicationBase app) { + AuthorityType authorityType = app.authenticationAuthority.authorityType; + if(authorityType == AuthorityType.B2C){ + switch(user.getB2cProvider().toLowerCase()){ + case B2CProvider.LOCAL: + SeleniumExtensions.performLocalLogin(seleniumDriver, user); + break; + case B2CProvider.GOOGLE: + SeleniumExtensions.performGoogleLogin(seleniumDriver, user); + break; + case B2CProvider.FACEBOOK: + SeleniumExtensions.performFacebookLogin(seleniumDriver, user); + break; + } + } else if (authorityType == AuthorityType.AAD) { + SeleniumExtensions.performADLogin(seleniumDriver, user); + } + else if (authorityType == AuthorityType.ADFS) { + SeleniumExtensions.performADFS2019Login(seleniumDriver, user); + } + } +} diff --git a/src/integrationtest/java/com.microsoft.aad.msal4j/TestConstants.java b/src/integrationtest/java/com.microsoft.aad.msal4j/TestConstants.java index 0fc75643..4f0839c2 100644 --- a/src/integrationtest/java/com.microsoft.aad.msal4j/TestConstants.java +++ b/src/integrationtest/java/com.microsoft.aad.msal4j/TestConstants.java @@ -3,14 +3,14 @@ package com.microsoft.aad.msal4j; -import java.util.stream.Stream; - public class TestConstants { public final static String KEYVAULT_DEFAULT_SCOPE = "https://vault.azure.net/.default"; public final static String MSIDLAB_DEFAULT_SCOPE = "https://msidlab.com/.default"; public final static String GRAPH_DEFAULT_SCOPE = "https://graph.windows.net/.default"; public final static String USER_READ_SCOPE = "user.read"; public final static String B2C_LAB_SCOPE = "https://msidlabb2c.onmicrosoft.com/msaapp/user_impersonation"; + public final static String B2C_CONFIDENTIAL_CLIENT_APP_SECRET = "MSIDLABB2C-MSAapp-AppSecret"; + public final static String B2C_CONFIDENTIAL_CLIENT_LAB_APP_ID = "MSIDLABB2C-MSAapp-AppID"; public final static String MICROSOFT_AUTHORITY_HOST = "https://login.microsoftonline.com/"; public final static String ORGANIZATIONS_AUTHORITY = MICROSOFT_AUTHORITY_HOST + "organizations/"; diff --git a/src/integrationtest/java/infrastructure/TcpListener.java b/src/integrationtest/java/infrastructure/TcpListener.java deleted file mode 100644 index 1ab83e63..00000000 --- a/src/integrationtest/java/infrastructure/TcpListener.java +++ /dev/null @@ -1,117 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package infrastructure; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.net.ServerSocket; -import java.net.Socket; -import java.util.concurrent.BlockingQueue; - -public class TcpListener implements AutoCloseable{ - - private final static Logger LOG = LoggerFactory.getLogger(SeleniumExtensions.class); - - private BlockingQueue authorizationCodeQueue; - private BlockingQueue tcpStartUpNotificationQueue; - private int port; - private Thread serverThread; - private ServerSocket serverSocket; - - public TcpListener(BlockingQueue authorizationCodeQueue, - BlockingQueue tcpStartUpNotificationQueue){ - this.authorizationCodeQueue = authorizationCodeQueue; - this.tcpStartUpNotificationQueue = tcpStartUpNotificationQueue; - } - - public void startServer(){ - Runnable serverTask = () -> { - try(ServerSocket serverSocket = createSocket()) { - this.serverSocket = serverSocket; - port = serverSocket.getLocalPort(); - tcpStartUpNotificationQueue.put(Boolean.TRUE); - - Socket clientSocket = serverSocket.accept(); - - new ClientTask(clientSocket).run(); - } catch (Exception e) { - LOG.error("Unable to process client request: " + e.getMessage()); - throw new RuntimeException("Unable to process client request: " + e.getMessage()); - } - }; - - serverThread = new Thread(serverTask); - serverThread.start(); - } - - private class ClientTask implements Runnable { - private final Socket clientSocket; - - private ClientTask(Socket clientSocket) { - this.clientSocket = clientSocket; - } - - @Override - public void run(){ - StringBuilder builder = new StringBuilder(); - try(BufferedReader in = new BufferedReader( - new InputStreamReader(clientSocket.getInputStream()))) { - String line = in.readLine(); - while(!line.equals("")){ - builder.append(line); - line = in.readLine(); - } - authorizationCodeQueue.put(builder.toString()); - } catch (Exception e) { - LOG.error("Error reading response from socket: " + e.getMessage()); - throw new RuntimeException("Error reading response from socket: " + e.getMessage()); - } finally { - try { - clientSocket.close(); - } catch (IOException e) { - LOG.error("Error closing socket: " + e.getMessage()); - } - } - } - } - - public ServerSocket createSocket() throws IOException { - //int[] ports = { 3843, 4584, 4843, 60000 }; - int[] ports = {8080}; - int tryCount = 5; - - while (tryCount > 0) { - for (int port : ports) { - try { - return new ServerSocket(port); - } catch (IOException ex) { - LOG.warn("Port: " + port + "is blocked"); - } - } - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - tryCount--; - } - throw new IOException("no free port found"); - } - - public int getPort() { - return port; - } - - public void close(){ - try { - serverSocket.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } -} diff --git a/src/integrationtest/java/labapi/LabConstants.java b/src/integrationtest/java/labapi/LabConstants.java index f9a87334..a1d2839b 100644 --- a/src/integrationtest/java/labapi/LabConstants.java +++ b/src/integrationtest/java/labapi/LabConstants.java @@ -5,7 +5,7 @@ public class LabConstants { public final static String LAB_USER_ENDPOINT = "https://msidlab.com/api/user"; - public final static String LAB_USER_SECRET_ENDPOINT = "https://msidlab.com/api/LabUserSecret"; + public final static String LAB_USER_SECRET_ENDPOINT = "https://msidlab.com/api/LabSecret"; public final static String LAB_APP_ENDPOINT = "https://msidlab.com/api/App"; public final static String LAB_LAB_ENDPOINT = "https://msidlab.com/api/Lab"; diff --git a/src/integrationtest/java/labapi/LabService.java b/src/integrationtest/java/labapi/LabService.java index 57898afd..6c74b716 100644 --- a/src/integrationtest/java/labapi/LabService.java +++ b/src/integrationtest/java/labapi/LabService.java @@ -59,7 +59,7 @@ User getUser(UserQueryParameters query){ User[] users = convertJsonToObject(result, User[].class); User user = users[0]; - user.setPassword(getUserSecret(user.getLabName())); + user.setPassword(getSecret(user.getLabName())); if (query.parameters.containsKey(UserQueryParameters.FEDERATION_PROVIDER)) { user.setFederationProvider(query.parameters.get(UserQueryParameters.FEDERATION_PROVIDER)); } else { @@ -94,7 +94,7 @@ public static Lab getLab(String labId) { } } - private String getUserSecret(String labName){ + public static String getSecret(String labName){ String result; try { Map queryMap = new HashMap<>(); diff --git a/src/integrationtest/java/labapi/LabUserProvider.java b/src/integrationtest/java/labapi/LabUserProvider.java index 02e82b80..e1c9b90e 100644 --- a/src/integrationtest/java/labapi/LabUserProvider.java +++ b/src/integrationtest/java/labapi/LabUserProvider.java @@ -39,19 +39,35 @@ public User getDefaultUser() { return getLabUser(query); } - public User getUserByAzureEnvironment(String azureEnvironment) { + public User getFederatedAdfsUser(String federationProvider){ UserQueryParameters query = new UserQueryParameters(); - query.parameters.put(UserQueryParameters.AZURE_ENVIRONMENT, azureEnvironment); + query.parameters.put(UserQueryParameters.FEDERATION_PROVIDER, federationProvider); + query.parameters.put(UserQueryParameters.USER_TYPE, UserType.FEDERATED); return getLabUser(query); } - public User getFederatedAdfsUser(String federationProvider){ + public User getOnPremAdfsUser(String federationProvider){ + UserQueryParameters query = new UserQueryParameters(); + query.parameters.put(UserQueryParameters.FEDERATION_PROVIDER, federationProvider); + query.parameters.put(UserQueryParameters.USER_TYPE, UserType.ON_PREM); + + return getLabUser(query); + } + public User getB2cUser(String b2cProvider) { UserQueryParameters query = new UserQueryParameters(); - query.parameters.put(UserQueryParameters.FEDERATION_PROVIDER, federationProvider); - query.parameters.put(UserQueryParameters.USER_TYPE, UserType.FEDERATED); + query.parameters.put(UserQueryParameters.USER_TYPE, UserType.B2C); + query.parameters.put(UserQueryParameters.B2C_PROVIDER, b2cProvider); + + return getLabUser(query); + } + + public User getUserByAzureEnvironment(String azureEnvironment) { + + UserQueryParameters query = new UserQueryParameters(); + query.parameters.put(UserQueryParameters.AZURE_ENVIRONMENT, azureEnvironment); return getLabUser(query); } diff --git a/src/main/java/com/microsoft/aad/msal4j/AADAuthority.java b/src/main/java/com/microsoft/aad/msal4j/AADAuthority.java index 6ef2ca7e..ff0af644 100644 --- a/src/main/java/com/microsoft/aad/msal4j/AADAuthority.java +++ b/src/main/java/com/microsoft/aad/msal4j/AADAuthority.java @@ -14,12 +14,14 @@ class AADAuthority extends Authority { private final static String TENANTLESS_TENANT_NAME = "common"; + private final static String AUTHORIZATION_ENDPOINT = "oauth2/v2.0/authorize"; + private final static String TOKEN_ENDPOINT = "oauth2/v2.0/token"; + final static String DEVICE_CODE_ENDPOINT = "oauth2/v2.0/devicecode"; - private final static String AADAuthorityFormat = "https://%s/%s/"; - private final static String AADtokenEndpointFormat = "https://%s/{tenant}" + TOKEN_ENDPOINT; - - final static String DEVICE_CODE_ENDPOINT = "/oauth2/v2.0/devicecode"; - private final static String deviceCodeEndpointFormat = "https://%s/{tenant}" + DEVICE_CODE_ENDPOINT; + private final static String AAD_AUTHORITY_FORMAT = "https://%s/%s/"; + private final static String AAD_AUTHORIZATION_ENDPOINT_FORMAT = AAD_AUTHORITY_FORMAT + AUTHORIZATION_ENDPOINT; + private final static String AAD_TOKEN_ENDPOINT_FORMAT = AAD_AUTHORITY_FORMAT + TOKEN_ENDPOINT; + private final static String DEVICE_CODE_ENDPOINT_FORMAT = AAD_AUTHORITY_FORMAT + DEVICE_CODE_ENDPOINT; String deviceCodeEndpoint; @@ -27,15 +29,13 @@ class AADAuthority extends Authority { super(authorityUrl); validateAuthorityUrl(); setAuthorityProperties(); - this.authority = String.format(AADAuthorityFormat, host, tenant); + this.authority = String.format(AAD_AUTHORITY_FORMAT, host, tenant); } private void setAuthorityProperties() { - this.tokenEndpoint = String.format(AADtokenEndpointFormat, host); - this.tokenEndpoint = this.tokenEndpoint.replace("{tenant}", tenant); - - this.deviceCodeEndpoint = String.format(this.deviceCodeEndpointFormat, host); - this.deviceCodeEndpoint = this.deviceCodeEndpoint.replace("{tenant}", tenant); + this.authorizationEndpoint = String.format(AAD_AUTHORIZATION_ENDPOINT_FORMAT, host, tenant); + this.tokenEndpoint = String.format(AAD_TOKEN_ENDPOINT_FORMAT, host, tenant); + this.deviceCodeEndpoint = String.format(DEVICE_CODE_ENDPOINT_FORMAT, host, tenant); this.isTenantless = TENANTLESS_TENANT_NAME.equalsIgnoreCase(tenant); this.selfSignedJwtAudience = this.tokenEndpoint; diff --git a/src/main/java/com/microsoft/aad/msal4j/ADFSAuthority.java b/src/main/java/com/microsoft/aad/msal4j/ADFSAuthority.java index f40b04c4..21f71800 100644 --- a/src/main/java/com/microsoft/aad/msal4j/ADFSAuthority.java +++ b/src/main/java/com/microsoft/aad/msal4j/ADFSAuthority.java @@ -7,13 +7,15 @@ class ADFSAuthority extends Authority{ + final static String AUTHORIZATION_ENDPOINT = "oauth2/authorize"; final static String TOKEN_ENDPOINT = "oauth2/token"; - private final static String ADFSAuthorityFormat = "https://%s/%s/"; + private final static String ADFS_AUTHORITY_FORMAT = "https://%s/%s/"; ADFSAuthority(final URL authorityUrl) { super(authorityUrl); - this.authority = String.format(ADFSAuthorityFormat, host, tenant); + this.authority = String.format(ADFS_AUTHORITY_FORMAT, host, tenant); + this.authorizationEndpoint = authority + AUTHORIZATION_ENDPOINT; this.tokenEndpoint = authority + TOKEN_ENDPOINT; this.selfSignedJwtAudience = this.tokenEndpoint; } diff --git a/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByInteractiveFlowSupplier.java b/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByInteractiveFlowSupplier.java new file mode 100644 index 00000000..768dccdc --- /dev/null +++ b/src/main/java/com/microsoft/aad/msal4j/AcquireTokenByInteractiveFlowSupplier.java @@ -0,0 +1,167 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.aad.msal4j; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.*; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +class AcquireTokenByInteractiveFlowSupplier extends AuthenticationResultSupplier { + + private final static Logger LOG = LoggerFactory.getLogger(AcquireTokenByAuthorizationGrantSupplier.class); + + private PublicClientApplication clientApplication; + private InteractiveRequest interactiveRequest; + + private BlockingQueue authorizationResultQueue; + private HttpListener httpListener; + + AcquireTokenByInteractiveFlowSupplier(PublicClientApplication clientApplication, + InteractiveRequest request){ + super(clientApplication, request); + this.clientApplication = clientApplication; + this.interactiveRequest = request; + } + + @Override + AuthenticationResult execute() throws Exception{ + AuthorizationResult authorizationResult = getAuthorizationResult(); + validateState(authorizationResult); + return acquireTokenWithAuthorizationCode(authorizationResult); + } + + private AuthorizationResult getAuthorizationResult(){ + + AuthorizationResult result; + try { + SystemBrowserOptions systemBrowserOptions = + interactiveRequest.interactiveRequestParameters().systemBrowserOptions(); + + authorizationResultQueue = new LinkedBlockingQueue<>(); + AuthorizationResponseHandler authorizationResponseHandler = + new AuthorizationResponseHandler( + authorizationResultQueue, + systemBrowserOptions); + + startHttpListener(authorizationResponseHandler); + + if (systemBrowserOptions != null && systemBrowserOptions.openBrowserAction() != null) { + interactiveRequest.interactiveRequestParameters().systemBrowserOptions().openBrowserAction() + .openBrowser(interactiveRequest.authorizationUrl()); + } else { + openDefaultSystemBrowser(interactiveRequest.authorizationUrl()); + } + + result = getAuthorizationResultFromHttpListener(); + } finally { + if(httpListener != null){ + httpListener.stopListener(); + } + } + return result; + } + + private void validateState(AuthorizationResult authorizationResult){ + if(StringHelper.isBlank(authorizationResult.state()) || + !authorizationResult.state().equals(interactiveRequest.state())){ + + throw new MsalClientException("State returned in authorization result is blank or does " + + "not match state sent on outgoing request", + AuthenticationErrorCode.INVALID_AUTHORIZATION_RESULT); + } + } + + private void startHttpListener(AuthorizationResponseHandler handler){ + // if port is unspecified, set to 0, which will cause socket to find a free port + int port = interactiveRequest.interactiveRequestParameters().redirectUri().getPort() == -1 ? + 0 : + interactiveRequest.interactiveRequestParameters().redirectUri().getPort(); + + httpListener = new HttpListener(); + httpListener.startListener(port, handler); + + //If no port is passed, http listener finds a free one. We should update redirect URL to + // point to this port. + if(port != httpListener.port()){ + updateRedirectUrl(); + } + } + + private void updateRedirectUrl(){ + try { + URI updatedRedirectUrl = new URI("http://localhost:" + httpListener.port()); + interactiveRequest.interactiveRequestParameters().redirectUri(updatedRedirectUrl); + LOG.debug("Redirect URI updated to" + updatedRedirectUrl); + } catch (URISyntaxException ex){ + throw new MsalClientException("Error updating redirect URI. Not a valid URI format", + AuthenticationErrorCode.INVALID_REDIRECT_URI); + } + } + + private void openDefaultSystemBrowser(URL url){ + try{ + if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) { + Desktop.getDesktop().browse(url.toURI()); + LOG.debug("Opened default system browser"); + } else { + throw new MsalClientException("Unable to open default system browser", + AuthenticationErrorCode.DESKTOP_BROWSER_NOT_SUPPORTED); + } + } catch(URISyntaxException | IOException ex){ + throw new MsalClientException(ex); + } + } + + private AuthorizationResult getAuthorizationResultFromHttpListener(){ + AuthorizationResult result = null; + try { + LOG.debug("Listening for authorization result"); + long expirationTime = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()) + 120; + + while(result == null && !interactiveRequest.futureReference().get().isCancelled() && + TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()) < expirationTime) { + + result = authorizationResultQueue.poll(100, TimeUnit.MILLISECONDS); + } + } catch(Exception e){ + throw new MsalClientException(e); + } + + if (result == null || StringHelper.isBlank(result.code())) { + throw new MsalClientException("No Authorization code was returned from the server", + AuthenticationErrorCode.INVALID_AUTHORIZATION_RESULT); + } + return result; + } + + private AuthenticationResult acquireTokenWithAuthorizationCode(AuthorizationResult authorizationResult) + throws Exception{ + AuthorizationCodeParameters parameters = AuthorizationCodeParameters + .builder(authorizationResult.code(), interactiveRequest.interactiveRequestParameters().redirectUri()) + .scopes(interactiveRequest.interactiveRequestParameters().scopes()) + .codeVerifier(interactiveRequest.verifier()) + .build(); + + AuthorizationCodeRequest authCodeRequest = new AuthorizationCodeRequest( + parameters, + clientApplication, + clientApplication.createRequestContext(PublicApi.ACQUIRE_TOKEN_BY_AUTHORIZATION_CODE)); + + AcquireTokenByAuthorizationGrantSupplier acquireTokenByAuthorizationGrantSupplier = + new AcquireTokenByAuthorizationGrantSupplier( + clientApplication, + authCodeRequest, + clientApplication.authenticationAuthority); + + return acquireTokenByAuthorizationGrantSupplier.execute(); + } +} \ No newline at end of file diff --git a/src/main/java/com/microsoft/aad/msal4j/AcquireTokenSilentSupplier.java b/src/main/java/com/microsoft/aad/msal4j/AcquireTokenSilentSupplier.java index 9ae01494..c6c6d16e 100644 --- a/src/main/java/com/microsoft/aad/msal4j/AcquireTokenSilentSupplier.java +++ b/src/main/java/com/microsoft/aad/msal4j/AcquireTokenSilentSupplier.java @@ -57,6 +57,8 @@ AuthenticationResult execute() throws Exception { if(res == null || StringHelper.isBlank(res.accessToken())){ throw new MsalClientException(AuthenticationErrorMessage.NO_TOKEN_IN_CACHE, AuthenticationErrorCode.CACHE_MISS); } + + clientApplication.getServiceBundle().getServerSideTelemetry().incrementSilentSuccessfulCount(); return res; } } diff --git a/src/main/java/com/microsoft/aad/msal4j/ApiEvent.java b/src/main/java/com/microsoft/aad/msal4j/ApiEvent.java index 3946c3ff..1fab6ff7 100644 --- a/src/main/java/com/microsoft/aad/msal4j/ApiEvent.java +++ b/src/main/java/com/microsoft/aad/msal4j/ApiEvent.java @@ -41,7 +41,7 @@ public void setAuthorityType(String authorityType){ public void setTenantId(String tenantId){ if(!StringHelper.isBlank(tenantId) && logPii){ - this.put(TENANT_ID_KEY, hashPii(tenantId)); + this.put(TENANT_ID_KEY, StringHelper.createBase64EncodedSha256Hash(tenantId)); } else { this.put(TENANT_ID_KEY, null); } @@ -49,7 +49,7 @@ public void setTenantId(String tenantId){ public void setAccountId(String accountId){ if(!StringHelper.isBlank(accountId) && logPii){ - this.put(USER_ID_KEY, hashPii(accountId)); + this.put(USER_ID_KEY, StringHelper.createBase64EncodedSha256Hash(accountId)); } else { this.put(USER_ID_KEY, null); } diff --git a/src/main/java/com/microsoft/aad/msal4j/AuthenticationErrorCode.java b/src/main/java/com/microsoft/aad/msal4j/AuthenticationErrorCode.java index d4cb0158..88253585 100644 --- a/src/main/java/com/microsoft/aad/msal4j/AuthenticationErrorCode.java +++ b/src/main/java/com/microsoft/aad/msal4j/AuthenticationErrorCode.java @@ -9,19 +9,21 @@ public class AuthenticationErrorCode { /** - * In the context of device code user has not yet authenticated via browser + * In the context of device code user has not yet authenticated via browser. For more details, + * see https://aka.ms/msal4j-device-code */ public final static String AUTHORIZATION_PENDING = "authorization_pending"; /** * In the context of device code, this error happens when the device code has expired before - * the user signed-in on another device (this is usually after 15 min) + * the user signed-in on another device (this is usually after 15 min). For more details, see + * https://aka.ms/msal4j-device-code */ public final static String CODE_EXPIRED = "code_expired"; /** * Standard Oauth2 protocol error code. It indicates that the application needs to expose - * the UI to the user so that user does an interactive action in order to get a new token + * the UI to the user so that user does an interactive action in order to get a new token. */ public final static String INVALID_GRANT = "invalid_grant"; @@ -32,7 +34,7 @@ public class AuthenticationErrorCode { /** * Password is required for managed user. Will typically happen when trying to do integrated windows authentication - * for managed users + * for managed users. For more information, see https://aka.ms/msal4j-iwa */ public final static String PASSWORD_REQUIRED_FOR_MANAGED_USER = "password_required_for_managed_user"; @@ -56,5 +58,36 @@ public class AuthenticationErrorCode { * Unknown error occurred */ public final static String UNKNOWN = "unknown"; -} + /** + * The current redirect URI is not a loopback URL. To use the OS browser, a loopback URL must be + * configured both during app registration as well as when initializing the InteractiveRequestParameters + * object. For more details, see https://aka.ms/msal4j-interactive-request + */ + public final static String LOOPBACK_REDIRECT_URI = "loopback_redirect_uri"; + + /** + * Unable to start Http listener to the specified port. This might be because the port is busy. + */ + public final static String UNABLE_TO_START_HTTP_LISTENER = "unable_to_start_http_listener"; + + /** + * Authorization result response is invalid. Authorization result must contain authorization + * code and state. + */ + public final static String INVALID_AUTHORIZATION_RESULT = "invalid_authorization_result"; + + /** + * Redirect URI provided to MSAL is of invalid format. Redirect URL must be a loopback URL. + * For more details, see https://aka.ms/msal4j-interactive-request + */ + public final static String INVALID_REDIRECT_URI = "invalid_redirect_uri"; + + /** + * MSAL was unable to open the user-default browser. This is either because the current platform + * does not support {@link java.awt.Desktop} or {@link java.awt.Desktop.Action#BROWSE}. Interactive + * requests require that the client have a system default browser. For more details, see + * https://aka.ms/msal4j-interactive-request + */ + public final static String DESKTOP_BROWSER_NOT_SUPPORTED = "desktop_browser_not_supported"; +} diff --git a/src/main/java/com/microsoft/aad/msal4j/AuthenticationResultSupplier.java b/src/main/java/com/microsoft/aad/msal4j/AuthenticationResultSupplier.java index 60c1887e..8aa48211 100644 --- a/src/main/java/com/microsoft/aad/msal4j/AuthenticationResultSupplier.java +++ b/src/main/java/com/microsoft/aad/msal4j/AuthenticationResultSupplier.java @@ -68,10 +68,23 @@ public IAuthenticationResult get() { } } catch(Exception ex) { + String error; if (ex instanceof MsalServiceException) { - apiEvent.setApiErrorCode(((MsalServiceException) ex).errorCode()); + error = ((MsalServiceException) ex).errorCode(); + apiEvent.setApiErrorCode(error); + } else { + if(ex.getCause() != null){ + error = ex.getCause().toString(); + } else { + error = StringHelper.EMPTY_STRING; + } } + clientApplication.getServiceBundle().getServerSideTelemetry().addFailedRequestTelemetry( + String.valueOf(msalRequest.requestContext().publicApi().getApiId()), + msalRequest.requestContext().correlationId(), + error); + clientApplication.log.error( LogHelper.createMessage( "Execution of " + this.getClass() + " failed.", @@ -83,8 +96,7 @@ public IAuthenticationResult get() { return result; } - void logResult(AuthenticationResult result, HttpHeaders headers) - { + private void logResult(AuthenticationResult result, HttpHeaders headers) { if (!StringHelper.isBlank(result.accessToken())) { String accessTokenHash = this.computeSha256Hash(result diff --git a/src/main/java/com/microsoft/aad/msal4j/Authority.java b/src/main/java/com/microsoft/aad/msal4j/Authority.java index 0c09a395..6bc1c21d 100644 --- a/src/main/java/com/microsoft/aad/msal4j/Authority.java +++ b/src/main/java/com/microsoft/aad/msal4j/Authority.java @@ -21,7 +21,6 @@ abstract class Authority { private static final String ADFS_PATH_SEGMENT = "adfs"; private static final String B2C_PATH_SEGMENT = "tfp"; - final static String TOKEN_ENDPOINT = "/oauth2/v2.0/token"; private final static String USER_REALM_ENDPOINT = "common/userrealm"; private final static String userRealmEndpointFormat = "https://%s/" + USER_REALM_ENDPOINT + "/%s?api-version=1.0"; @@ -34,6 +33,7 @@ abstract class Authority { String tenant; boolean isTenantless; + String authorizationEndpoint; String tokenEndpoint; URL tokenEndpointUrl() throws MalformedURLException { diff --git a/src/main/java/com/microsoft/aad/msal4j/AuthorizationCodeParameters.java b/src/main/java/com/microsoft/aad/msal4j/AuthorizationCodeParameters.java index f8ecdeca..48ea6a2a 100644 --- a/src/main/java/com/microsoft/aad/msal4j/AuthorizationCodeParameters.java +++ b/src/main/java/com/microsoft/aad/msal4j/AuthorizationCodeParameters.java @@ -22,14 +22,30 @@ @AllArgsConstructor(access = AccessLevel.PRIVATE) public class AuthorizationCodeParameters { - private Set scopes; - + /** + * Authorization code acquired in the first step of OAuth2.0 authorization code flow. For more + * details, see https://aka.ms/msal4j-authorization-code-flow + */ @NonNull private String authorizationCode; + /** + * Redirect URI registered in the Azure portal, and which was used in the first step of OAuth2.0 + * authorization code flow. For more details, see https://aka.ms/msal4j-authorization-code-flow + */ @NonNull private URI redirectUri; + /** + * Scopes to which the application is requesting access + */ + private Set scopes; + + /** + * Code verifier used for PKCE. For more details, see https://tools.ietf.org/html/rfc7636 + */ + private String codeVerifier; + private static AuthorizationCodeParametersBuilder builder() { return new AuthorizationCodeParametersBuilder(); diff --git a/src/main/java/com/microsoft/aad/msal4j/AuthorizationCodeRequest.java b/src/main/java/com/microsoft/aad/msal4j/AuthorizationCodeRequest.java index 96ea38c2..70e60f34 100644 --- a/src/main/java/com/microsoft/aad/msal4j/AuthorizationCodeRequest.java +++ b/src/main/java/com/microsoft/aad/msal4j/AuthorizationCodeRequest.java @@ -7,6 +7,7 @@ import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant; import com.nimbusds.oauth2.sdk.AuthorizationGrant; import com.nimbusds.oauth2.sdk.auth.ClientAuthentication; +import com.nimbusds.oauth2.sdk.pkce.CodeVerifier; import lombok.Builder; import java.net.URI; @@ -22,8 +23,17 @@ class AuthorizationCodeRequest extends MsalRequest { private static AbstractMsalAuthorizationGrant createMsalGrant(AuthorizationCodeParameters parameters){ - AuthorizationGrant authorizationGrant = new AuthorizationCodeGrant( - new AuthorizationCode(parameters.authorizationCode()), parameters.redirectUri()); + AuthorizationGrant authorizationGrant; + if(parameters.codeVerifier() != null){ + authorizationGrant = new AuthorizationCodeGrant( + new AuthorizationCode(parameters.authorizationCode()), + parameters.redirectUri(), + new CodeVerifier(parameters.codeVerifier())); + + } else { + authorizationGrant = new AuthorizationCodeGrant( + new AuthorizationCode(parameters.authorizationCode()),parameters.redirectUri()); + } return new OAuthAuthorizationGrant(authorizationGrant, parameters.scopes()); } diff --git a/src/main/java/com/microsoft/aad/msal4j/AuthorizationRequestUrlParameters.java b/src/main/java/com/microsoft/aad/msal4j/AuthorizationRequestUrlParameters.java new file mode 100644 index 00000000..d8e7da5b --- /dev/null +++ b/src/main/java/com/microsoft/aad/msal4j/AuthorizationRequestUrlParameters.java @@ -0,0 +1,280 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.aad.msal4j; + +import com.nimbusds.oauth2.sdk.util.URLUtils; +import lombok.Getter; +import lombok.NonNull; +import lombok.experimental.Accessors; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; + +/** + * Parameters for {@link ClientApplicationBase#getAuthorizationRequestUrl(AuthorizationRequestUrlParameters)} + */ +@Accessors(fluent = true) +@Getter +public class AuthorizationRequestUrlParameters { + + @NonNull + private String redirectUri; + @NonNull + private Set scopes; + private String codeChallenge; + private String codeChallengeMethod; + private String state; + private String nonce; + private ResponseMode responseMode; + private String loginHint; + private String domainHint; + private Prompt prompt; + private String correlationId; + + Map> requestParameters = new HashMap<>(); + + public static Builder builder(String redirectUri, + Set scopes) { + + ParameterValidationUtils.validateNotBlank("redirect_uri", redirectUri); + ParameterValidationUtils.validateNotEmpty("scopes", scopes); + + return builder() + .redirectUri(redirectUri) + .scopes(scopes); + } + + private static Builder builder() { + return new Builder(); + } + + private AuthorizationRequestUrlParameters(Builder builder){ + //required parameters + this.redirectUri = builder.redirectUri; + requestParameters.put("redirect_uri", Collections.singletonList(this.redirectUri)); + this.scopes = builder.scopes; + + Set scopesParam = new TreeSet<>(builder.scopes); + String[] commonScopes = AbstractMsalAuthorizationGrant.COMMON_SCOPES_PARAM.split(" "); + scopesParam.addAll(Arrays.asList(commonScopes)); + + this.scopes = scopesParam; + requestParameters.put("scope", Collections.singletonList(String.join(" ", scopesParam))); + requestParameters.put("response_type",Collections.singletonList("code")); + + // Optional parameters + if(builder.claims != null){ + String claimsParam = String.join(" ", builder.claims); + requestParameters.put("claims", Collections.singletonList(claimsParam)); + } + + if(builder.codeChallenge != null){ + this.codeChallenge = builder.codeChallenge; + requestParameters.put("code_challenge", Collections.singletonList(builder.codeChallenge)); + } + + if(builder.codeChallengeMethod != null){ + this.codeChallengeMethod = builder.codeChallengeMethod; + requestParameters.put("code_challenge_method", Collections.singletonList(builder.codeChallengeMethod)); + } + + if(builder.state != null){ + this.state = builder.state; + requestParameters.put("state", Collections.singletonList(builder.state)); + } + + if(builder.nonce != null){ + this.nonce = builder.nonce; + requestParameters.put("nonce", Collections.singletonList(builder.nonce)); + } + + if(builder.responseMode != null){ + this.responseMode = builder.responseMode; + requestParameters.put("response_mode", Collections.singletonList( + builder.responseMode.toString())); + } else { + this.responseMode = ResponseMode.FORM_POST; + requestParameters.put("response_mode", Collections.singletonList( + ResponseMode.FORM_POST.toString())); + } + + if(builder.loginHint != null){ + this.loginHint = loginHint(); + requestParameters.put("login_hint", Collections.singletonList(builder.loginHint)); + } + + if(builder.domainHint != null){ + this.domainHint = domainHint(); + requestParameters.put("domain_hint", Collections.singletonList(builder.domainHint)); + } + + if(builder.prompt != null){ + this.prompt = builder.prompt; + requestParameters.put("prompt", Collections.singletonList(builder.prompt.toString())); + } + + if(builder.correlationId != null){ + this.correlationId = builder.correlationId; + requestParameters.put("correlation_id", Collections.singletonList(builder.correlationId)); + } + } + + URL createAuthorizationURL(Authority authority, + Map> requestParameters){ + URL authorizationRequestUrl; + try { + String authorizationCodeEndpoint = authority.authorizationEndpoint(); + String uriString = authorizationCodeEndpoint + "?" + + URLUtils.serializeParameters(requestParameters); + + authorizationRequestUrl = new URL(uriString); + } catch(MalformedURLException ex){ + throw new MsalClientException(ex); + } + return authorizationRequestUrl; + } + + public static class Builder { + + private String redirectUri; + private Set scopes; + private Set claims; + private String codeChallenge; + private String codeChallengeMethod; + private String state; + private String nonce; + private ResponseMode responseMode; + private String loginHint; + private String domainHint; + private Prompt prompt; + private String correlationId; + + public AuthorizationRequestUrlParameters build(){ + return new AuthorizationRequestUrlParameters(this); + } + + private Builder self() { + return this; + } + + /** + * The redirect URI where authentication responses can be received by your application. It + * must exactly match one of the redirect URIs registered in the Azure portal. + */ + public Builder redirectUri(String val){ + this.redirectUri = val; + return self(); + } + + /** + * Scopes which the application is requesting access to and the user will consent to. + */ + public Builder scopes(Set val){ + this.scopes = val; + return self(); + } + + /** + * In cases where Azure AD tenant admin has enabled conditional access policies, and the + * policy has not been met,{@link MsalServiceException} will contain claims that need be + * consented to. + */ + public Builder claims(Set val){ + this.claims = val; + return self(); + } + + /** + * Used to secure authorization code grant via Proof of Key for Code Exchange (PKCE). + * Required if codeChallenge is included. For more information, see the PKCE RCF: + * https://tools.ietf.org/html/rfc7636 + */ + public Builder codeChallenge(String val){ + this.codeChallenge = val; + return self(); + } + + /** + * The method used to encode the code verifier for the code challenge parameter. Can be one + * of plain or S256. If excluded, code challenge is assumed to be plaintext. For more + * information, see the PKCE RCF: https://tools.ietf.org/html/rfc7636 + */ + public Builder codeChallengeMethod(String val){ + this.codeChallengeMethod = val; + return self(); + } + + /** + * A value included in the request that is also returned in the token response. A randomly + * generated unique value is typically used for preventing cross site request forgery attacks. + * The state is also used to encode information about the user's state in the app before the + * authentication request occurred. + * */ + public Builder state(String val){ + this.state = val; + return self(); + } + + /** + * A value included in the request that is also returned in the token response. A randomly + * generated unique value is typically used for preventing cross site request forgery attacks. + */ + public Builder nonce(String val){ + this.nonce = val; + return self(); + } + + /** + * Specifies the method that should be used to send the authentication result to your app. + */ + public Builder responseMode(ResponseMode val){ + this.responseMode = val; + return self(); + } + + /** + * Can be used to pre-fill the username/email address field of the sign-in page for the user, + * if you know the username/email address ahead of time. Often apps use this parameter during + * re-authentication, having already extracted the username from a previous sign-in using the + * preferred_username claim. + */ + public Builder loginHint(String val){ + this.loginHint = val; + return self(); + } + + /** + * Provides a hint about the tenant or domain that the user should use to sign in. The value + * of the domain hint is a registered domain for the tenant. + **/ + public Builder domainHint(String val){ + this.domainHint = val; + return self(); + } + + /** + * Indicates the type of user interaction that is required. Possible values are + * {@link Prompt} + */ + public Builder prompt(Prompt val){ + this.prompt = val; + return self(); + } + + /** + * Identifier used to correlate requests for telemetry purposes. Usually a GUID. + */ + public Builder correlationId(String val){ + this.correlationId = val; + return self(); + } + } +} diff --git a/src/main/java/com/microsoft/aad/msal4j/AuthorizationResponseHandler.java b/src/main/java/com/microsoft/aad/msal4j/AuthorizationResponseHandler.java new file mode 100644 index 00000000..c1bfdad0 --- /dev/null +++ b/src/main/java/com/microsoft/aad/msal4j/AuthorizationResponseHandler.java @@ -0,0 +1,122 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.aad.msal4j; + +import com.sun.net.httpserver.Headers; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import lombok.Getter; +import lombok.experimental.Accessors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.util.concurrent.BlockingQueue; +import java.util.stream.Collectors; + +@Getter +@Accessors(fluent = true) +class AuthorizationResponseHandler implements HttpHandler { + + private final static Logger LOG = LoggerFactory.getLogger(AuthorizationResponseHandler.class); + + private final static String DEFAULT_SUCCESS_MESSAGE = "Authentication Complete"+ + " Authentication complete. You can close the browser and return to the application."+ + " " ; + + private final static String DEFAULT_FAILURE_MESSAGE = "Authentication Failed " + + " Authentication failed. You can return to the application. Feel free to close this browser tab. " + + "



Error details: error {0} error_description: {1} "; + + private BlockingQueue authorizationResultQueue; + private SystemBrowserOptions systemBrowserOptions; + + AuthorizationResponseHandler(BlockingQueue authorizationResultQueue, + SystemBrowserOptions systemBrowserOptions){ + this.authorizationResultQueue = authorizationResultQueue; + this.systemBrowserOptions = systemBrowserOptions; + } + + @Override + public void handle(HttpExchange httpExchange) throws IOException { + try{ + if(!httpExchange.getRequestURI().getPath().equalsIgnoreCase("/")){ + httpExchange.sendResponseHeaders(200, 0); + return; + } + String responseBody = new BufferedReader(new InputStreamReader( + httpExchange.getRequestBody())).lines().collect(Collectors.joining("\n")); + + AuthorizationResult result = AuthorizationResult.fromResponseBody(responseBody); + sendResponse(httpExchange, result); + authorizationResultQueue.put(result); + + } catch (InterruptedException ex){ + LOG.error("Error reading response from socket: " + ex.getMessage()); + throw new MsalClientException(ex); + } finally { + httpExchange.close(); + } + } + + private void sendResponse(HttpExchange httpExchange, AuthorizationResult result) + throws IOException{ + + switch (result.status()){ + case Success: + sendSuccessResponse(httpExchange, getSuccessfulResponseMessage()); + break; + case ProtocolError: + case UnknownError: + sendErrorResponse(httpExchange, getErrorResponseMessage()); + break; + } + } + + private void sendSuccessResponse(HttpExchange httpExchange, String response) throws IOException { + if (systemBrowserOptions == null || systemBrowserOptions.browserRedirectSuccess() == null) { + send200Response(httpExchange, response); + } else { + send302Response(httpExchange, systemBrowserOptions().browserRedirectSuccess().toString()); + } + } + + private void sendErrorResponse(HttpExchange httpExchange, String response) throws IOException { + if(systemBrowserOptions == null || systemBrowserOptions.browserRedirectError() == null){ + send200Response(httpExchange, response); + } else { + send302Response(httpExchange, systemBrowserOptions().browserRedirectError().toString()); + } + } + + private void send302Response(HttpExchange httpExchange, String redirectUri) throws IOException{ + Headers responseHeaders = httpExchange.getResponseHeaders(); + responseHeaders.set("Location", redirectUri); + httpExchange.sendResponseHeaders(302, 0); + } + + private void send200Response(HttpExchange httpExchange, String response) throws IOException{ + httpExchange.sendResponseHeaders(200, response.length()); + OutputStream os = httpExchange.getResponseBody(); + os.write(response.getBytes()); + os.close(); + } + + private String getSuccessfulResponseMessage(){ + if(systemBrowserOptions == null || systemBrowserOptions.htmlMessageSuccess() == null) { + return DEFAULT_SUCCESS_MESSAGE; + } + return systemBrowserOptions().htmlMessageSuccess(); + } + + private String getErrorResponseMessage(){ + if(systemBrowserOptions == null || systemBrowserOptions.htmlMessageError() == null) { + return DEFAULT_FAILURE_MESSAGE; + } + return systemBrowserOptions().htmlMessageSuccess(); + } +} diff --git a/src/main/java/com/microsoft/aad/msal4j/AuthorizationResult.java b/src/main/java/com/microsoft/aad/msal4j/AuthorizationResult.java new file mode 100644 index 00000000..0aa13102 --- /dev/null +++ b/src/main/java/com/microsoft/aad/msal4j/AuthorizationResult.java @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.aad.msal4j; + +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + +import java.net.URLDecoder; +import java.util.LinkedHashMap; +import java.util.Map; + +@Getter +@Setter +@Accessors(fluent = true) +class AuthorizationResult { + + private String code; + private String state; + private AuthorizationStatus status; + private String error; + private String errorDescription; + + enum AuthorizationStatus { + Success, + ProtocolError, + UnknownError + } + + static AuthorizationResult fromResponseBody(String responseBody){ + + if(StringHelper.isBlank(responseBody)){ + return new AuthorizationResult( + AuthorizationStatus.UnknownError, + AuthenticationErrorCode.INVALID_AUTHORIZATION_RESULT, + "The authorization server returned an invalid response: response " + + "is null or empty"); + } + + Map queryParameters = parseParameters(responseBody); + + if(queryParameters.containsKey("error")){ + return new AuthorizationResult( + AuthorizationStatus.ProtocolError, + queryParameters.get("error"), + !StringHelper.isBlank(queryParameters.get("error_description")) ? + queryParameters.get("error_description") : + null); + } + + if(!queryParameters.containsKey("code")){ + return new AuthorizationResult( + AuthorizationStatus.UnknownError, + AuthenticationErrorCode.INVALID_AUTHORIZATION_RESULT, + "Authorization result response does not contain authorization code"); + } + + AuthorizationResult result = new AuthorizationResult(); + result.code = queryParameters.get("code"); + result.status = AuthorizationStatus.Success; + + if(queryParameters.containsKey("state")){ + result.state = queryParameters.get("state"); + } + + return result; + } + + private AuthorizationResult(){ + } + + private AuthorizationResult(AuthorizationStatus status, String error, String errorDescription){ + this.status = status; + this.error = error; + this.errorDescription = errorDescription; + } + + private static Map parseParameters(String serverResponse) { + Map query_pairs = new LinkedHashMap<>(); + try { + String[] pairs = serverResponse.split("&"); + for (String pair : pairs) { + int idx = pair.indexOf("="); + String key = URLDecoder.decode(pair.substring(0, idx), "UTF-8"); + String value = URLDecoder.decode(pair.substring(idx + 1), "UTF-8"); + query_pairs.put(key, value); + } + } catch(Exception ex){ + throw new MsalClientException( + AuthenticationErrorCode.INVALID_AUTHORIZATION_RESULT, + String.format("Error parsing authorization result: %s", ex.getMessage())); + } + + return query_pairs; + } +} diff --git a/src/main/java/com/microsoft/aad/msal4j/B2CAuthority.java b/src/main/java/com/microsoft/aad/msal4j/B2CAuthority.java index 86e51d95..6c93092b 100644 --- a/src/main/java/com/microsoft/aad/msal4j/B2CAuthority.java +++ b/src/main/java/com/microsoft/aad/msal4j/B2CAuthority.java @@ -13,8 +13,12 @@ @Getter(AccessLevel.PACKAGE) class B2CAuthority extends Authority{ - final static String B2CTokenEndpointFormat = "https://%s/{tenant}" + TOKEN_ENDPOINT + "?p={policy}"; - String policy; + private final static String AUTHORIZATION_ENDPOINT = "/oauth2/v2.0/authorize"; + private final static String TOKEN_ENDPOINT = "/oauth2/v2.0/token"; + + private final static String B2C_AUTHORIZATION_ENDPOINT_FORMAT = "https://%s/%s/%s" + AUTHORIZATION_ENDPOINT; + private final static String B2C_TOKEN_ENDPOINT_FORMAT = "https://%s/%s" + TOKEN_ENDPOINT + "?p=%s"; + private String policy; B2CAuthority(final URL authorityUrl){ super(authorityUrl); @@ -40,9 +44,8 @@ private void setAuthorityProperties() { segments[1], segments[2]); - this.tokenEndpoint = String.format(B2CTokenEndpointFormat, host); - this.tokenEndpoint = this.tokenEndpoint.replace("{tenant}", tenant); - this.tokenEndpoint = this.tokenEndpoint.replace("{policy}", policy); + this.authorizationEndpoint = String.format(B2C_AUTHORIZATION_ENDPOINT_FORMAT, host, tenant, policy); + this.tokenEndpoint = String.format(B2C_TOKEN_ENDPOINT_FORMAT, host, tenant, policy); this.selfSignedJwtAudience = this.tokenEndpoint; } } diff --git a/src/main/java/com/microsoft/aad/msal4j/ClientApplicationBase.java b/src/main/java/com/microsoft/aad/msal4j/ClientApplicationBase.java index 4b344e54..ce458560 100644 --- a/src/main/java/com/microsoft/aad/msal4j/ClientApplicationBase.java +++ b/src/main/java/com/microsoft/aad/msal4j/ClientApplicationBase.java @@ -22,7 +22,8 @@ import static com.microsoft.aad.msal4j.ParameterValidationUtils.validateNotNull; /** - * Abstract class containing common API methods and properties. + * Abstract class containing common methods and properties to both {@link PublicClientApplication} + * and {@link ConfidentialClientApplication}. */ abstract class ClientApplicationBase implements IClientApplicationBase { @@ -159,6 +160,18 @@ public CompletableFuture removeAccount(IAccount account) { return future; } + @Override + public URL getAuthorizationRequestUrl(AuthorizationRequestUrlParameters parameters) { + + validateNotNull("parameters", parameters); + + parameters.requestParameters.put("client_id", Collections.singletonList(this.clientId)); + + return parameters.createAuthorizationURL( + this.authenticationAuthority, + parameters.requestParameters()); + } + AuthenticationResult acquireTokenCommon(MsalRequest msalRequest, Authority requestAuthority) throws Exception { @@ -202,6 +215,10 @@ private AuthenticationResultSupplier getAuthenticationResultSupplier(MsalRequest (DeviceCodeFlowRequest) msalRequest); } else if (msalRequest instanceof SilentRequest) { supplier = new AcquireTokenSilentSupplier(this, (SilentRequest) msalRequest); + } else if(msalRequest instanceof InteractiveRequest){ + supplier = new AcquireTokenByInteractiveFlowSupplier( + (PublicClientApplication) this, + (InteractiveRequest) msalRequest); } else { supplier = new AcquireTokenByAuthorizationGrantSupplier( this, @@ -235,7 +252,7 @@ abstract static class Builder> { private String authority = DEFAULT_AUTHORITY; private Authority authenticationAuthority = createDefaultAADAuthority(); private boolean validateAuthority = true; - private String correlationId = UUID.randomUUID().toString(); + private String correlationId; private boolean logPii = false; private ExecutorService executorService; private Proxy proxy; diff --git a/src/main/java/com/microsoft/aad/msal4j/ClientCredentialFactory.java b/src/main/java/com/microsoft/aad/msal4j/ClientCredentialFactory.java index bd5bf3e4..02799067 100644 --- a/src/main/java/com/microsoft/aad/msal4j/ClientCredentialFactory.java +++ b/src/main/java/com/microsoft/aad/msal4j/ClientCredentialFactory.java @@ -10,7 +10,8 @@ import java.security.cert.X509Certificate; /** - * Factory for creating client credentials used in confidential client flows + * Factory for creating client credentials used in confidential client flows. For more details, see + * https://aka.ms/msal4j-client-credentials */ public class ClientCredentialFactory { diff --git a/src/main/java/com/microsoft/aad/msal4j/ClientCredentialParameters.java b/src/main/java/com/microsoft/aad/msal4j/ClientCredentialParameters.java index 8c3094f3..9da25412 100644 --- a/src/main/java/com/microsoft/aad/msal4j/ClientCredentialParameters.java +++ b/src/main/java/com/microsoft/aad/msal4j/ClientCredentialParameters.java @@ -20,6 +20,9 @@ @AllArgsConstructor(access = AccessLevel.PRIVATE) public class ClientCredentialParameters { + /** + * Scopes for which the application is requesting access to. + */ @NonNull private Set scopes; diff --git a/src/main/java/com/microsoft/aad/msal4j/ConfidentialClientApplication.java b/src/main/java/com/microsoft/aad/msal4j/ConfidentialClientApplication.java index 13bf74a1..a988cb5a 100644 --- a/src/main/java/com/microsoft/aad/msal4j/ConfidentialClientApplication.java +++ b/src/main/java/com/microsoft/aad/msal4j/ConfidentialClientApplication.java @@ -22,6 +22,9 @@ /** * Class to be used to acquire tokens for confidential client applications (Web Apps, Web APIs, * and daemon applications). + * For details see {@link IConfidentialClientApplication} + * + * Conditionally thread-safe */ public class ConfidentialClientApplication extends ClientApplicationBase implements IConfidentialClientApplication { diff --git a/src/main/java/com/microsoft/aad/msal4j/CurrentRequest.java b/src/main/java/com/microsoft/aad/msal4j/CurrentRequest.java new file mode 100644 index 00000000..a5ef5085 --- /dev/null +++ b/src/main/java/com/microsoft/aad/msal4j/CurrentRequest.java @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.aad.msal4j; + +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + +@Getter +@Accessors(fluent = true) +class CurrentRequest { + + private final PublicApi publicApi; + + @Setter + private boolean forceRefresh = false; + + CurrentRequest(PublicApi publicApi){ + this.publicApi = publicApi; + } +} \ No newline at end of file diff --git a/src/main/java/com/microsoft/aad/msal4j/DefaultHttpClient.java b/src/main/java/com/microsoft/aad/msal4j/DefaultHttpClient.java index c1932dc7..6fca83b0 100644 --- a/src/main/java/com/microsoft/aad/msal4j/DefaultHttpClient.java +++ b/src/main/java/com/microsoft/aad/msal4j/DefaultHttpClient.java @@ -99,6 +99,7 @@ private HttpResponse readResponseFromConnection(final HttpsURLConnection conn) t if (responseCode != HttpURLConnection.HTTP_OK) { is = conn.getErrorStream(); if (is != null) { + httpResponse.headers(conn.getHeaderFields()); httpResponse.body(inputStreamToString(is)); } return httpResponse; diff --git a/src/main/java/com/microsoft/aad/msal4j/DeviceCodeFlowParameters.java b/src/main/java/com/microsoft/aad/msal4j/DeviceCodeFlowParameters.java index f27d0863..b8378d70 100644 --- a/src/main/java/com/microsoft/aad/msal4j/DeviceCodeFlowParameters.java +++ b/src/main/java/com/microsoft/aad/msal4j/DeviceCodeFlowParameters.java @@ -13,7 +13,8 @@ /** * Object containing parameters for device code flow. Can be used as parameter to - * {@link PublicClientApplication#acquireToken(DeviceCodeFlowParameters)} + * {@link PublicClientApplication#acquireToken(DeviceCodeFlowParameters)}. For more details, + * see https://aka.ms/msal4j-device-code */ @Builder @Accessors(fluent = true) @@ -21,9 +22,19 @@ @AllArgsConstructor(access = AccessLevel.PRIVATE) public class DeviceCodeFlowParameters { + /** + * Scopes to which the application is requesting access to. + */ @NonNull private Set scopes; + /** + * Receives the device code returned from the first step of Oauth2.0 device code flow. The + * {@link DeviceCode#verificationUri} and the {@link DeviceCode#userCode} should be shown + * to the end user. + * + * For more details, see https://aka.ms/msal4j-device-code + */ @NonNull private Consumer deviceCodeConsumer; diff --git a/src/main/java/com/microsoft/aad/msal4j/Event.java b/src/main/java/com/microsoft/aad/msal4j/Event.java index 330ae60f..6861073b 100644 --- a/src/main/java/com/microsoft/aad/msal4j/Event.java +++ b/src/main/java/com/microsoft/aad/msal4j/Event.java @@ -64,16 +64,4 @@ static String scrubTenant(URI uri){ String scrubbedPath = String.join("/", segment); return uri.getScheme() + "://" + uri.getAuthority() + scrubbedPath; } - - static String hashPii(String stringToHash){ - String base64EncodedSha256Hash; - try { - MessageDigest digest = MessageDigest.getInstance("SHA-256"); - byte[] hashedString = digest.digest(stringToHash.getBytes(StandardCharsets.UTF_8)); - base64EncodedSha256Hash = new String(Base64.getEncoder().encode(hashedString), StandardCharsets.UTF_8); - } catch(NoSuchAlgorithmException e){ - base64EncodedSha256Hash = null; - } - return base64EncodedSha256Hash; - } } diff --git a/src/main/java/com/microsoft/aad/msal4j/HttpListener.java b/src/main/java/com/microsoft/aad/msal4j/HttpListener.java new file mode 100644 index 00000000..7228c0c2 --- /dev/null +++ b/src/main/java/com/microsoft/aad/msal4j/HttpListener.java @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.aad.msal4j; + +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.experimental.Accessors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.InetSocketAddress; + +@Accessors(fluent = true) +class HttpListener { + + private final static Logger LOG = LoggerFactory.getLogger(HttpListener.class); + + private HttpServer server; + + @Getter(AccessLevel.PACKAGE) + private int port; + + void startListener(int port, HttpHandler httpHandler) { + try { + server = HttpServer.create(new InetSocketAddress(port), 0); + server.createContext("/", httpHandler); + this.port = server.getAddress().getPort(); + server.start(); + LOG.debug("Http listener started. Listening on port: " + port); + } catch (Exception e){ + throw new MsalClientException(e.getMessage(), + AuthenticationErrorCode.UNABLE_TO_START_HTTP_LISTENER); + } + } + + void stopListener(){ + if(server != null){ + server.stop(0); + LOG.debug("Http listener stopped"); + + } + } +} diff --git a/src/main/java/com/microsoft/aad/msal4j/HttpMethod.java b/src/main/java/com/microsoft/aad/msal4j/HttpMethod.java index f18b7d05..64af4605 100644 --- a/src/main/java/com/microsoft/aad/msal4j/HttpMethod.java +++ b/src/main/java/com/microsoft/aad/msal4j/HttpMethod.java @@ -4,7 +4,7 @@ package com.microsoft.aad.msal4j; /** - * Http request method + * Http request method. */ public enum HttpMethod { diff --git a/src/main/java/com/microsoft/aad/msal4j/IAccount.java b/src/main/java/com/microsoft/aad/msal4j/IAccount.java index 9814d944..7c43b5af 100644 --- a/src/main/java/com/microsoft/aad/msal4j/IAccount.java +++ b/src/main/java/com/microsoft/aad/msal4j/IAccount.java @@ -8,6 +8,7 @@ /** * Interface representing a single user account. An IAccount is returned in the {@link IAuthenticationResult} * property, and is used as parameter in {@link SilentParameters#builder(Set, IAccount)} )} + * */ public interface IAccount { diff --git a/src/main/java/com/microsoft/aad/msal4j/IClientApplicationBase.java b/src/main/java/com/microsoft/aad/msal4j/IClientApplicationBase.java index 8caa8be8..58f5b152 100644 --- a/src/main/java/com/microsoft/aad/msal4j/IClientApplicationBase.java +++ b/src/main/java/com/microsoft/aad/msal4j/IClientApplicationBase.java @@ -6,6 +6,7 @@ import javax.net.ssl.SSLSocketFactory; import java.net.MalformedURLException; import java.net.Proxy; +import java.net.URL; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; @@ -66,6 +67,19 @@ interface IClientApplicationBase { // */ // java.util.function.Consumer>> telemetryConsumer(); + /** + * Computes the URL of the authorization request letting the user sign-in and consent to the + * application. The URL target the /authorize endpoint of the authority configured in the + * application object. + * + * Once the user successfully authenticates, the response should contain an authorization code, + * which can then be passed in to{@link ClientApplicationBase#acquireToken(AuthorizationCodeParameters)} + * to be exchanged for a token + * @param parameters {@link AuthorizationRequestUrlParameters} + * @return url of the authorization endpoint where the user can sign-in and consent to the application. + */ + URL getAuthorizationRequestUrl(AuthorizationRequestUrlParameters parameters); + /** * Acquires security token from the authority using an authorization code previously received. * diff --git a/src/main/java/com/microsoft/aad/msal4j/IClientAssertion.java b/src/main/java/com/microsoft/aad/msal4j/IClientAssertion.java index d64c7c6e..e82de558 100644 --- a/src/main/java/com/microsoft/aad/msal4j/IClientAssertion.java +++ b/src/main/java/com/microsoft/aad/msal4j/IClientAssertion.java @@ -6,6 +6,8 @@ /** * Credential type containing an assertion of type * "urn:ietf:params:oauth:token-type:jwt". + * + * For more details, see https://aka.ms/msal4j-client-credentials */ public interface IClientAssertion extends IClientCredential{ diff --git a/src/main/java/com/microsoft/aad/msal4j/IClientCertificate.java b/src/main/java/com/microsoft/aad/msal4j/IClientCertificate.java index 3225f984..cec06682 100644 --- a/src/main/java/com/microsoft/aad/msal4j/IClientCertificate.java +++ b/src/main/java/com/microsoft/aad/msal4j/IClientCertificate.java @@ -9,6 +9,8 @@ /** * Credential type containing X509 public certificate and RSA private key. + * + * For more details, see https://aka.ms/msal4j-client-credentials */ public interface IClientCertificate extends IClientCredential{ diff --git a/src/main/java/com/microsoft/aad/msal4j/IClientCredential.java b/src/main/java/com/microsoft/aad/msal4j/IClientCredential.java index 09053adf..e1b6d959 100644 --- a/src/main/java/com/microsoft/aad/msal4j/IClientCredential.java +++ b/src/main/java/com/microsoft/aad/msal4j/IClientCredential.java @@ -5,6 +5,8 @@ /** * Interface representing an application credential + * + * For more details, see https://aka.ms/msal4j-client-credentials */ public interface IClientCredential { diff --git a/src/main/java/com/microsoft/aad/msal4j/IClientSecret.java b/src/main/java/com/microsoft/aad/msal4j/IClientSecret.java index d8ed914d..568a8786 100644 --- a/src/main/java/com/microsoft/aad/msal4j/IClientSecret.java +++ b/src/main/java/com/microsoft/aad/msal4j/IClientSecret.java @@ -5,6 +5,8 @@ /** * Representation of client credential containing a secret in string format + * + * For more details, see https://aka.ms/msal4j-client-credentials */ public interface IClientSecret extends IClientCredential{ diff --git a/src/main/java/com/microsoft/aad/msal4j/IHttpClient.java b/src/main/java/com/microsoft/aad/msal4j/IHttpClient.java index 75bd8677..b14001d4 100644 --- a/src/main/java/com/microsoft/aad/msal4j/IHttpClient.java +++ b/src/main/java/com/microsoft/aad/msal4j/IHttpClient.java @@ -6,6 +6,8 @@ /** * Interface to be implemented when configuring http client for {@link IPublicClientApplication} or * {@link IConfidentialClientApplication}. + * + * For more details, see https://aka.ms/msal4j-http-client */ public interface IHttpClient { diff --git a/src/main/java/com/microsoft/aad/msal4j/IPublicClientApplication.java b/src/main/java/com/microsoft/aad/msal4j/IPublicClientApplication.java index 88bed012..aed245bf 100644 --- a/src/main/java/com/microsoft/aad/msal4j/IPublicClientApplication.java +++ b/src/main/java/com/microsoft/aad/msal4j/IPublicClientApplication.java @@ -44,4 +44,15 @@ public interface IPublicClientApplication extends IClientApplicationBase { * SHOULD wait between polling requests to the token endpoint */ CompletableFuture acquireToken(DeviceCodeFlowParameters parameters); + + /** + * Acquires tokens from the authority using authorization code grant. Will attempt to open the + * default system browser where the user can input the credentials interactively, consent to scopes, + * and do multi-factor authentication if such a policy is enabled on the Azure AD tenant. + * System browser can behavior can be customized via {@link InteractiveRequestParameters#systemBrowserOptions}. + * For more information, see https://aka.ms/msal4j-interactive-request + * @param parameters instance of {@link InteractiveRequestParameters} + * @return {@link CompletableFuture} containing an {@link IAuthenticationResult} + */ + CompletableFuture acquireToken(InteractiveRequestParameters parameters); } diff --git a/src/main/java/com/microsoft/aad/msal4j/ITokenCache.java b/src/main/java/com/microsoft/aad/msal4j/ITokenCache.java index 9329a7f9..52abbf1c 100644 --- a/src/main/java/com/microsoft/aad/msal4j/ITokenCache.java +++ b/src/main/java/com/microsoft/aad/msal4j/ITokenCache.java @@ -5,6 +5,8 @@ /** * Interface representing security token cache persistence + * + * For more details, see https://aka.ms/msal4j-token-cache */ public interface ITokenCache { diff --git a/src/main/java/com/microsoft/aad/msal4j/ITokenCacheAccessAspect.java b/src/main/java/com/microsoft/aad/msal4j/ITokenCacheAccessAspect.java index 3659c57f..3181a929 100644 --- a/src/main/java/com/microsoft/aad/msal4j/ITokenCacheAccessAspect.java +++ b/src/main/java/com/microsoft/aad/msal4j/ITokenCacheAccessAspect.java @@ -5,6 +5,8 @@ /** * Interface representing operation of executing code before and after cache access. + * + * For more details, see https://aka.ms/msal4j-token-cache */ public interface ITokenCacheAccessAspect { diff --git a/src/main/java/com/microsoft/aad/msal4j/ITokenCacheAccessContext.java b/src/main/java/com/microsoft/aad/msal4j/ITokenCacheAccessContext.java index 3c386334..71ccee89 100644 --- a/src/main/java/com/microsoft/aad/msal4j/ITokenCacheAccessContext.java +++ b/src/main/java/com/microsoft/aad/msal4j/ITokenCacheAccessContext.java @@ -5,6 +5,8 @@ /** * Interface representing context in which the token cache is accessed + * + * For more details, see https://aka.ms/msal4j-token-cache */ public interface ITokenCacheAccessContext { diff --git a/src/main/java/com/microsoft/aad/msal4j/IntegratedWindowsAuthenticationParameters.java b/src/main/java/com/microsoft/aad/msal4j/IntegratedWindowsAuthenticationParameters.java index df9fb12c..8abe408d 100644 --- a/src/main/java/com/microsoft/aad/msal4j/IntegratedWindowsAuthenticationParameters.java +++ b/src/main/java/com/microsoft/aad/msal4j/IntegratedWindowsAuthenticationParameters.java @@ -13,7 +13,9 @@ /** * Object containing parameters for Integrated Windows Authentication. Can be used as parameter to - * {@link PublicClientApplication#acquireToken(IntegratedWindowsAuthenticationParameters)} + * {@link PublicClientApplication#acquireToken(IntegratedWindowsAuthenticationParameters)}` + * + * For more details, see https://aka.ms/msal4j-iwa */ @Builder @Accessors(fluent = true) @@ -21,9 +23,15 @@ @AllArgsConstructor(access = AccessLevel.PRIVATE) public class IntegratedWindowsAuthenticationParameters { + /** + * Scopes that the application is requesting access to + */ @NonNull private Set scopes; + /** + * Identifier of user account for which to acquire tokens for + */ @NonNull private String username; diff --git a/src/main/java/com/microsoft/aad/msal4j/InteractiveRequest.java b/src/main/java/com/microsoft/aad/msal4j/InteractiveRequest.java new file mode 100644 index 00000000..e76492f1 --- /dev/null +++ b/src/main/java/com/microsoft/aad/msal4j/InteractiveRequest.java @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.aad.msal4j; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.experimental.Accessors; + +import java.net.InetAddress; +import java.net.URI; +import java.net.URL; +import java.security.SecureRandom; +import java.util.Base64; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicReference; + +@Accessors(fluent = true) +class InteractiveRequest extends MsalRequest{ + + @Getter(AccessLevel.PACKAGE) + private AtomicReference> futureReference; + + @Getter(AccessLevel.PACKAGE) + private InteractiveRequestParameters interactiveRequestParameters; + + @Getter(AccessLevel.PACKAGE) + private String verifier; + + @Getter(AccessLevel.PACKAGE) + private String state; + + private PublicClientApplication publicClientApplication; + private URL authorizationUrl; + + InteractiveRequest(InteractiveRequestParameters parameters, + AtomicReference> futureReference, + PublicClientApplication publicClientApplication, + RequestContext requestContext){ + + super(publicClientApplication, null, requestContext); + + this.interactiveRequestParameters = parameters; + this.futureReference = futureReference; + this.publicClientApplication = publicClientApplication; + validateRedirectUrl(parameters.redirectUri()); + } + + URL authorizationUrl(){ + if(this.authorizationUrl == null) { + authorizationUrl = createAuthorizationUrl(); + } + return authorizationUrl; + } + + private void validateRedirectUrl(URI redirectUri) { + try { + if (!InetAddress.getByName(redirectUri.getHost()).isLoopbackAddress()) { + throw new MsalClientException(String.format( + "Only loopback redirect uri is supported, but %s was found " + + "Configure http://localhost or http://localhost:port both during app registration" + + "and when you create the create the InteractiveRequestParameters object", redirectUri.getHost()), + AuthenticationErrorCode.LOOPBACK_REDIRECT_URI); + } + + if (!redirectUri.getScheme().equals("http")) { + throw new MsalClientException(String.format( + "Only http uri scheme is supported but %s was found. Configure http://localhost" + + "or http://localhost:port both during app registration and when you create" + + " the create the InteractiveRequestParameters object", redirectUri.toString()), + AuthenticationErrorCode.LOOPBACK_REDIRECT_URI); + } + } catch (Exception exception){ + throw new MsalClientException(exception); + } + } + + private URL createAuthorizationUrl(){ + + AuthorizationRequestUrlParameters.Builder authorizationRequestUrlBuilder = + AuthorizationRequestUrlParameters + .builder(interactiveRequestParameters.redirectUri().toString(), + interactiveRequestParameters.scopes()) + .prompt(interactiveRequestParameters.prompt()) + .loginHint(interactiveRequestParameters.loginHint()) + .domainHint(interactiveRequestParameters.domainHint()) + .correlationId(publicClientApplication.correlationId()); + + addPkceAndState(authorizationRequestUrlBuilder); + return publicClientApplication.getAuthorizationRequestUrl( + authorizationRequestUrlBuilder.build()); + } + + private void addPkceAndState(AuthorizationRequestUrlParameters.Builder builder) { + + // Create code verifier and code challenge as described in https://tools.ietf.org/html/rfc7636 + SecureRandom secureRandom = new SecureRandom(); + byte[] randomBytes = new byte[32]; + secureRandom.nextBytes(randomBytes); + + verifier = Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes); + state = UUID.randomUUID().toString() + UUID.randomUUID().toString(); + + builder.codeChallenge(StringHelper.createBase64EncodedSha256Hash(verifier)) + .codeChallengeMethod("S256") + .state(state); + } +} diff --git a/src/main/java/com/microsoft/aad/msal4j/InteractiveRequestParameters.java b/src/main/java/com/microsoft/aad/msal4j/InteractiveRequestParameters.java new file mode 100644 index 00000000..16eeba45 --- /dev/null +++ b/src/main/java/com/microsoft/aad/msal4j/InteractiveRequestParameters.java @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.aad.msal4j; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NonNull; +import lombok.Setter; +import lombok.experimental.Accessors; + +import java.net.URI; +import java.util.Set; + +import static com.microsoft.aad.msal4j.ParameterValidationUtils.validateNotNull; + +/** + * Object containing parameters for interactive requests. Can be used as parameter to + * {@link PublicClientApplication#acquireToken(InteractiveRequestParameters)}. + * + * For more details, see https://aka.ms/msal4j-interactive-request. + */ +@Builder +@Accessors(fluent = true) +@Getter +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class InteractiveRequestParameters { + + /** + * Redirect URI where MSAL will listen to for the authorization code returned by Azure AD. + * Should be a loopback address with a port specified (for example, http://localhost:3671). If no + * port is specified, MSAL will find an open port. For more information, see + * https://aka.ms/msal4j-interactive-request. + */ + @Setter(AccessLevel.PACKAGE) + @NonNull + private URI redirectUri; + + /** + * Scopes that the application is requesting access to and the user will consent to. + */ + private Set scopes; + + /** + * Indicate the type of user interaction that is required. + */ + private Prompt prompt; + + /** + * Can be used to pre-fill the username/email address field of the sign-in page for the user, + * if you know the username/email address ahead of time. Often apps use this parameter during + * re-authentication, having already extracted the username from a previous sign-in using the + * preferred_username claim. + */ + private String loginHint; + + /** + * Provides a hint about the tenant or domain that the user should use to sign in. The value + * of the domain hint is a registered domain for the tenant. + **/ + private String domainHint; + + /** + * Sets {@link SystemBrowserOptions} to be used by the PublicClientApplication + */ + private SystemBrowserOptions systemBrowserOptions; + + private static InteractiveRequestParametersBuilder builder() { + return new InteractiveRequestParametersBuilder(); + } + + public static InteractiveRequestParametersBuilder builder(URI redirectUri) { + + validateNotNull("redirect_uri", redirectUri); + + return builder() + .redirectUri(redirectUri); + } +} diff --git a/src/main/java/com/microsoft/aad/msal4j/MsalRequest.java b/src/main/java/com/microsoft/aad/msal4j/MsalRequest.java index 65a5f79e..ed16c327 100644 --- a/src/main/java/com/microsoft/aad/msal4j/MsalRequest.java +++ b/src/main/java/com/microsoft/aad/msal4j/MsalRequest.java @@ -12,14 +12,25 @@ @Getter(AccessLevel.PACKAGE) @AllArgsConstructor abstract class MsalRequest { - private final ClientApplicationBase application; AbstractMsalAuthorizationGrant msalAuthorizationGrant; + private final ClientApplicationBase application; + private final RequestContext requestContext; @Getter(value = AccessLevel.PACKAGE, lazy = true) private final HttpHeaders headers = new HttpHeaders(requestContext); -} + MsalRequest(ClientApplicationBase clientApplicationBase, + AbstractMsalAuthorizationGrant abstractMsalAuthorizationGrant, + RequestContext requestContext){ + + this.application = clientApplicationBase; + this.msalAuthorizationGrant = abstractMsalAuthorizationGrant; + this.requestContext = requestContext; + CurrentRequest currentRequest = new CurrentRequest(requestContext.publicApi()); + application.getServiceBundle().getServerSideTelemetry().setCurrentRequest(currentRequest); + } +} diff --git a/src/main/java/com/microsoft/aad/msal4j/OAuthHttpRequest.java b/src/main/java/com/microsoft/aad/msal4j/OAuthHttpRequest.java index 192b9b58..8e81e784 100644 --- a/src/main/java/com/microsoft/aad/msal4j/OAuthHttpRequest.java +++ b/src/main/java/com/microsoft/aad/msal4j/OAuthHttpRequest.java @@ -62,6 +62,11 @@ private Map configureHttpHeaders(){ if (this.getAuthorization() != null) { httpHeaders.put("Authorization", this.getAuthorization()); } + + Map telemetryHeaders = + serviceBundle.getServerSideTelemetry().getServerTelemetryHeaderMap(); + httpHeaders.putAll(telemetryHeaders); + return httpHeaders; } diff --git a/src/main/java/com/microsoft/aad/msal4j/OnBehalfOfParameters.java b/src/main/java/com/microsoft/aad/msal4j/OnBehalfOfParameters.java index 39b84e0d..6f181e08 100644 --- a/src/main/java/com/microsoft/aad/msal4j/OnBehalfOfParameters.java +++ b/src/main/java/com/microsoft/aad/msal4j/OnBehalfOfParameters.java @@ -13,6 +13,8 @@ /** * Object containing parameters for On-Behalf-Of flow. Can be used as parameter to * {@link ConfidentialClientApplication#acquireToken(OnBehalfOfParameters)} + * + * For more details, see https://aka.ms/msal4j-on-behalf-of */ @Builder @Accessors(fluent = true) diff --git a/src/main/java/com/microsoft/aad/msal4j/OpenBrowserAction.java b/src/main/java/com/microsoft/aad/msal4j/OpenBrowserAction.java new file mode 100644 index 00000000..ee558227 --- /dev/null +++ b/src/main/java/com/microsoft/aad/msal4j/OpenBrowserAction.java @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.aad.msal4j; + +import java.net.URL; + +/** + * Interface to be implemented to override system browser initialization logic. Otherwise, + * PublicClientApplication defaults to using default system browser + */ +public interface OpenBrowserAction { + + /** + * Override for providing custom browser initialization logic. Method that is called by MSAL + * when doing {@link IPublicClientApplication#acquireToken(InteractiveRequestParameters)}. If + * not overridden, MSAL will attempt to open URL in default system browser. + * @param url URL to the /authorize endpoint which should opened up in a browser so the user can + * provide their credentials and consent to scopes. + */ + void openBrowser(URL url); +} diff --git a/src/main/java/com/microsoft/aad/msal4j/Prompt.java b/src/main/java/com/microsoft/aad/msal4j/Prompt.java new file mode 100644 index 00000000..73c67079 --- /dev/null +++ b/src/main/java/com/microsoft/aad/msal4j/Prompt.java @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.aad.msal4j; + +/** + * Indicate the type of user interaction that is required when sending authorization code request. + */ +public enum Prompt { + + /** + * The user should be prompted to reauthenticate. + */ + LOGIN ("login"), + + /** + *The user is prompted to select an account, interrupting single sign on. The user may select + * an existing signed-in account, enter their credentials for a remembered account, + * or choose to use a different account altogether. + */ + SELECT_ACCOUNT ("select_account"), + + /** + * User consent has been granted, but needs to be updated. The user should be prompted to consent. + */ + CONSENT ("consent"), + + /** + * An administrator should be prompted to consent on behalf of all users in their organization. + */ + ADMING_CONSENT ("admin_consent"); + + private String prompt; + + Prompt(String prompt){ + this.prompt = prompt; + } + + @Override + public String toString(){ + return prompt; + } +} diff --git a/src/main/java/com/microsoft/aad/msal4j/PublicClientApplication.java b/src/main/java/com/microsoft/aad/msal4j/PublicClientApplication.java index b8e0c35e..83be5937 100644 --- a/src/main/java/com/microsoft/aad/msal4j/PublicClientApplication.java +++ b/src/main/java/com/microsoft/aad/msal4j/PublicClientApplication.java @@ -5,6 +5,9 @@ import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod; import com.nimbusds.oauth2.sdk.id.ClientID; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.experimental.Accessors; import org.slf4j.LoggerFactory; import java.util.concurrent.CompletableFuture; @@ -15,6 +18,9 @@ /** * Class to be used to acquire tokens for public client applications (Desktop, Mobile). + * For details see {@link IPublicClientApplication} + * + * Conditionally thread-safe */ public class PublicClientApplication extends ClientApplicationBase implements IPublicClientApplication { @@ -70,6 +76,24 @@ public CompletableFuture acquireToken(DeviceCodeFlowParam return future; } + @Override + public CompletableFuture acquireToken(InteractiveRequestParameters parameters){ + + validateNotNull("parameters", parameters); + + AtomicReference> futureReference = new AtomicReference<>(); + + InteractiveRequest interactiveRequest = new InteractiveRequest( + parameters, + futureReference, + this, + createRequestContext(PublicApi.ACQUIRE_TOKEN_BY_AUTHORIZATION_CODE)); + + CompletableFuture future = executeRequest(interactiveRequest); + futureReference.set(future); + return future; + } + private PublicClientApplication(Builder builder) { super(builder); diff --git a/src/main/java/com/microsoft/aad/msal4j/RefreshTokenParameters.java b/src/main/java/com/microsoft/aad/msal4j/RefreshTokenParameters.java index 5a7b0488..04f6a36d 100644 --- a/src/main/java/com/microsoft/aad/msal4j/RefreshTokenParameters.java +++ b/src/main/java/com/microsoft/aad/msal4j/RefreshTokenParameters.java @@ -15,6 +15,9 @@ * Object containing parameters for refresh token request. Can be used as parameter to * {@link PublicClientApplication#acquireToken(RefreshTokenParameters)} or to * {@link ConfidentialClientApplication#acquireToken(RefreshTokenParameters)} + * + * RefreshTokenParameters should only be used for migration scenarios (when moving from ADAL to + * MSAL). To acquire tokens silently, use {@link ClientApplicationBase#acquireTokenSilently(SilentParameters)} */ @Builder @Accessors(fluent = true) @@ -22,9 +25,15 @@ @AllArgsConstructor(access = AccessLevel.PRIVATE) public class RefreshTokenParameters { + /** + * Scopes the application is requesting access to + */ @NonNull private Set scopes; + /** + * Refresh token received from the STS + */ @NonNull private String refreshToken; diff --git a/src/main/java/com/microsoft/aad/msal4j/ResponseMode.java b/src/main/java/com/microsoft/aad/msal4j/ResponseMode.java new file mode 100644 index 00000000..43767bc0 --- /dev/null +++ b/src/main/java/com/microsoft/aad/msal4j/ResponseMode.java @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.aad.msal4j; + +/** + * Values for possible methods in which AAD can send the authorization result back to the calling + * application + */ +public enum ResponseMode { + + /** + * Authorization result is encoded as HTML form values that are transmitted via a HTTP POST + * to the redirect URL + */ + FORM_POST("form_post"), + + /** + * Authorization result returned as query string in the redirect URL when redirecting back to the + * client application. + */ + QUERY("query"), + + /** + * Authorization result is returned in the fragment added to the redirect URL when redirecting + * back to the client application + */ + FRAGMENT("fragment"); + + private String responseMode; + + ResponseMode(String responseMode){ + this.responseMode = responseMode; + } + + @Override + public String toString(){ + return this.responseMode; + } +} diff --git a/src/main/java/com/microsoft/aad/msal4j/ServerSideTelemetry.java b/src/main/java/com/microsoft/aad/msal4j/ServerSideTelemetry.java new file mode 100644 index 00000000..45748a2d --- /dev/null +++ b/src/main/java/com/microsoft/aad/msal4j/ServerSideTelemetry.java @@ -0,0 +1,117 @@ +package com.microsoft.aad.msal4j; + +import java.lang.reflect.Array; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicInteger; + +class ServerSideTelemetry { + + private final static String SCHEMA_VERSION = "2"; + private final static String SCHEMA_PIPE_DELIMITER = "|"; + private final static String SCHEMA_COMMA_DELIMITER = ","; + private final static String CURRENT_REQUEST_HEADER_NAME = "x-client-current-telemetry"; + private final static String LAST_REQUEST_HEADER_NAME = "x-client-last-telemetry"; + + private CurrentRequest currentRequest; + private AtomicInteger silentSuccessfulCount = new AtomicInteger(0); + + ConcurrentMap previousRequests = new ConcurrentHashMap<>(); + ConcurrentMap previousRequestInProgress = new ConcurrentHashMap<>(); + + synchronized Map getServerTelemetryHeaderMap(){ + Map headerMap = new HashMap<>(); + + headerMap.put(CURRENT_REQUEST_HEADER_NAME, buildCurrentRequestHeader()); + headerMap.put(LAST_REQUEST_HEADER_NAME, buildLastRequestHeader()); + + return headerMap; + } + + void addFailedRequestTelemetry(String publicApiId, String correlationId, String error){ + + String[] previousRequest = new String[]{publicApiId, error}; + previousRequests.put( + correlationId, + previousRequest); + } + + void incrementSilentSuccessfulCount(){ + silentSuccessfulCount.incrementAndGet(); + } + + synchronized CurrentRequest getCurrentRequest() { + return currentRequest; + } + + synchronized void setCurrentRequest(CurrentRequest currentRequest) { + this.currentRequest = currentRequest; + } + + private synchronized String buildCurrentRequestHeader(){ + if(currentRequest == null){ + return StringHelper.EMPTY_STRING; + } + + return SCHEMA_VERSION + + SCHEMA_PIPE_DELIMITER + + currentRequest.publicApi().getApiId() + + SCHEMA_COMMA_DELIMITER + + currentRequest.forceRefresh() + + SCHEMA_PIPE_DELIMITER; + } + + private synchronized String buildLastRequestHeader() { + + // LastRequest header schema: + // schema_version|silent_successful_count|api_id1,correlation_id1|error1| + StringBuilder lastRequestBuilder = new StringBuilder(); + + lastRequestBuilder + .append(SCHEMA_VERSION) + .append(SCHEMA_PIPE_DELIMITER) + .append(silentSuccessfulCount.getAndSet(0)); + + if (previousRequests.isEmpty()) { + // Kusto queries always expect all delimiters so return + // "schema_version|silent_successful_count|||" + return lastRequestBuilder + .append(SCHEMA_PIPE_DELIMITER) + .append(SCHEMA_PIPE_DELIMITER) + .append(SCHEMA_PIPE_DELIMITER) + .toString(); + } + + StringBuilder middleSegmentBuilder = new StringBuilder(SCHEMA_PIPE_DELIMITER); + StringBuilder errorSegmentBuilder = new StringBuilder(SCHEMA_PIPE_DELIMITER); + + Iterator it = previousRequests.keySet().iterator(); + + // Total header size should be less than 8kb. At max, we will use 4kb for telemetry. + while (it.hasNext() + && (middleSegmentBuilder.length() + errorSegmentBuilder.length()) < 3800) { + String correlationId = it.next(); + String[] previousRequest = previousRequests.get(correlationId); + String apiId = (String)Array.get(previousRequest, 0); + String error = (String)Array.get(previousRequest, 1); + + middleSegmentBuilder.append(apiId).append(SCHEMA_COMMA_DELIMITER).append(correlationId); + errorSegmentBuilder.append(error); + + if(it.hasNext()){ + middleSegmentBuilder.append(SCHEMA_COMMA_DELIMITER); + errorSegmentBuilder.append(SCHEMA_COMMA_DELIMITER); + } + + previousRequestInProgress.put(correlationId, previousRequest); + it.remove(); + } + + errorSegmentBuilder.append(SCHEMA_PIPE_DELIMITER); + + return lastRequestBuilder.append(middleSegmentBuilder).append(errorSegmentBuilder).toString(); + } +} diff --git a/src/main/java/com/microsoft/aad/msal4j/ServiceBundle.java b/src/main/java/com/microsoft/aad/msal4j/ServiceBundle.java index a7d30f3c..704544e1 100644 --- a/src/main/java/com/microsoft/aad/msal4j/ServiceBundle.java +++ b/src/main/java/com/microsoft/aad/msal4j/ServiceBundle.java @@ -10,12 +10,15 @@ class ServiceBundle { private ExecutorService executorService; private TelemetryManager telemetryManager; private IHttpClient httpClient; + private ServerSideTelemetry serverSideTelemetry; ServiceBundle(ExecutorService executorService, IHttpClient httpClient, TelemetryManager telemetryManager){ this.executorService = executorService; this.telemetryManager = telemetryManager; this.httpClient = httpClient; + + serverSideTelemetry = new ServerSideTelemetry(); } ExecutorService getExecutorService() { @@ -29,4 +32,8 @@ TelemetryManager getTelemetryManager(){ IHttpClient getHttpClient(){ return httpClient; } + + ServerSideTelemetry getServerSideTelemetry(){ + return serverSideTelemetry; + } } \ No newline at end of file diff --git a/src/main/java/com/microsoft/aad/msal4j/SilentParameters.java b/src/main/java/com/microsoft/aad/msal4j/SilentParameters.java index b9bab07d..5380c4eb 100644 --- a/src/main/java/com/microsoft/aad/msal4j/SilentParameters.java +++ b/src/main/java/com/microsoft/aad/msal4j/SilentParameters.java @@ -15,6 +15,7 @@ * Object containing parameters for silent requests. Can be used as parameter to * {@link PublicClientApplication#acquireTokenSilently(SilentParameters)} or to * {@link ConfidentialClientApplication#acquireTokenSilently(SilentParameters)} + * */ @Builder @Accessors(fluent = true) diff --git a/src/main/java/com/microsoft/aad/msal4j/SilentRequest.java b/src/main/java/com/microsoft/aad/msal4j/SilentRequest.java index 371d0e67..3c43d2bc 100644 --- a/src/main/java/com/microsoft/aad/msal4j/SilentRequest.java +++ b/src/main/java/com/microsoft/aad/msal4j/SilentRequest.java @@ -27,5 +27,8 @@ class SilentRequest extends MsalRequest { this.requestAuthority = StringHelper.isBlank(parameters.authorityUrl()) ? application.authenticationAuthority : Authority.createAuthority(new URL(parameters.authorityUrl())); + + application.getServiceBundle().getServerSideTelemetry().getCurrentRequest().forceRefresh( + parameters.forceRefresh()); } } diff --git a/src/main/java/com/microsoft/aad/msal4j/StringHelper.java b/src/main/java/com/microsoft/aad/msal4j/StringHelper.java index 50d93311..857bd7a2 100644 --- a/src/main/java/com/microsoft/aad/msal4j/StringHelper.java +++ b/src/main/java/com/microsoft/aad/msal4j/StringHelper.java @@ -3,9 +3,28 @@ package com.microsoft.aad.msal4j; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; + final class StringHelper { - public static boolean isBlank(final String str) { + static String EMPTY_STRING = ""; + + static boolean isBlank(final String str) { return str == null || str.trim().length() == 0; } + + static String createBase64EncodedSha256Hash(String stringToHash){ + String base64EncodedSha256Hash; + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hashedString = digest.digest(stringToHash.getBytes(StandardCharsets.UTF_8)); + base64EncodedSha256Hash = Base64.getUrlEncoder().withoutPadding().encodeToString(hashedString); + } catch(NoSuchAlgorithmException e){ + base64EncodedSha256Hash = null; + } + return base64EncodedSha256Hash; + } } diff --git a/src/main/java/com/microsoft/aad/msal4j/SystemBrowserOptions.java b/src/main/java/com/microsoft/aad/msal4j/SystemBrowserOptions.java new file mode 100644 index 00000000..00e47a78 --- /dev/null +++ b/src/main/java/com/microsoft/aad/msal4j/SystemBrowserOptions.java @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.aad.msal4j; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.experimental.Accessors; + +import java.net.URI; + +/** + * Options for using the default OS browser as a separate process to handle interactive authentication. + * MSAL will listen for the OS browser to finish authenticating, but it cannot close the browser. + * It can however response with a HTTP 200 OK message or a 302 Redirect, which can be configured here. + * For more details, see https://aka.ms/msal4j-interactive-request + */ +@Builder +@Accessors(fluent = true) +@Getter +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class SystemBrowserOptions { + + /** + * When the user finishes authenticating, MSAL will respond with a Http 200 OK message, which the + * browser will show to the user + */ + private String htmlMessageSuccess; + + /** + * WHen the user finishes authenticating, but an error occurred, MSAL will respond with a + * Http 200 Ok message, which the browser will show to the user. + */ + private String htmlMessageError; + + /** + * When the user finishes authenticating, MSAL will redirect the browser to the given URI. + * Takes precedence over htmlMessageSuccess + */ + private URI browserRedirectSuccess; + + /** + * When the the user finishes authenticating, but an error occurred, MSAL will redirect the + * browser to the given URI. + * Takes precedence over htmlMessageError + */ + private URI browserRedirectError; + + /** + * Allows developers to implement their own logic for starting a browser and navigating to a + * specific Uri. Msal will use this when opening the browser. If not set, the user configured + * browser will be used. + */ + private OpenBrowserAction openBrowserAction; + + /** + * Builder for {@link SystemBrowserOptions} + */ + public static SystemBrowserOptionsBuilder builder() { + return new SystemBrowserOptionsBuilder(); + } +} diff --git a/src/main/java/com/microsoft/aad/msal4j/TokenCache.java b/src/main/java/com/microsoft/aad/msal4j/TokenCache.java index 4ee6e011..aa78926b 100644 --- a/src/main/java/com/microsoft/aad/msal4j/TokenCache.java +++ b/src/main/java/com/microsoft/aad/msal4j/TokenCache.java @@ -15,7 +15,9 @@ import java.util.stream.Collectors; /** - * Cache used for storing tokens. + * Cache used for storing tokens. For more details, see https://aka.ms/msal4j-token-cache + * + * Conditionally thread-safe */ public class TokenCache implements ITokenCache { diff --git a/src/main/java/com/microsoft/aad/msal4j/TokenCacheAccessContext.java b/src/main/java/com/microsoft/aad/msal4j/TokenCacheAccessContext.java index 9ac8d9e4..3a0d4a2a 100644 --- a/src/main/java/com/microsoft/aad/msal4j/TokenCacheAccessContext.java +++ b/src/main/java/com/microsoft/aad/msal4j/TokenCacheAccessContext.java @@ -9,6 +9,8 @@ /** * Context in which the the token cache is accessed + * + * For more details, see https://aka.ms/msal4j-token-cache */ @Builder @Accessors(fluent = true) diff --git a/src/main/java/com/microsoft/aad/msal4j/TokenRequestExecutor.java b/src/main/java/com/microsoft/aad/msal4j/TokenRequestExecutor.java index 9bfddf1b..8b98afb7 100644 --- a/src/main/java/com/microsoft/aad/msal4j/TokenRequestExecutor.java +++ b/src/main/java/com/microsoft/aad/msal4j/TokenRequestExecutor.java @@ -94,7 +94,7 @@ private AuthenticationResult createAuthenticationResultFromOauthHttpResponse( response.getClientInfo(), requestAuthority, idToken, - authority.policy); + authority.policy()); } else { accountCacheEntity = AccountCacheEntity.create( response.getClientInfo(), @@ -121,6 +121,12 @@ else if(type == AuthorityType.ADFS){ build(); } else { + // http codes indicating that STS did not log request + if(oauthHttpResponse.getStatusCode() == 429 || oauthHttpResponse.getStatusCode() >= 500){ + serviceBundle.getServerSideTelemetry().previousRequests.putAll( + serviceBundle.getServerSideTelemetry().previousRequestInProgress); + } + throw MsalServiceExceptionFactory.fromHttpResponse(oauthHttpResponse); } return result; diff --git a/src/main/java/com/microsoft/aad/msal4j/UserNamePasswordParameters.java b/src/main/java/com/microsoft/aad/msal4j/UserNamePasswordParameters.java index 398194b0..799b4c82 100644 --- a/src/main/java/com/microsoft/aad/msal4j/UserNamePasswordParameters.java +++ b/src/main/java/com/microsoft/aad/msal4j/UserNamePasswordParameters.java @@ -14,6 +14,8 @@ /** * Object containing parameters for Username/Password flow. Can be used as parameter to * {@link PublicClientApplication#acquireToken(UserNamePasswordParameters)} + * + * For more details, see https://aka.ms/msal4j-username-password */ @Builder @Accessors(fluent = true) @@ -21,12 +23,21 @@ @AllArgsConstructor(access = AccessLevel.PRIVATE) public class UserNamePasswordParameters { + /** + * Scopes application is requesting access to + */ @NonNull private Set scopes; + /** + * Username of the account + */ @NonNull private String username; + /** + * Char array containing credentials for the username + */ @NonNull private char[] password; diff --git a/src/samples/SSLTunnelSocketFactory.java b/src/samples/SSLTunnelSocketFactory.java deleted file mode 100644 index 214c29ce..00000000 --- a/src/samples/SSLTunnelSocketFactory.java +++ /dev/null @@ -1,157 +0,0 @@ -// sample was written based on https://docs.oracle.com/javase/7/docs/technotes/guides/security/jsse/samples/sockets/client/SSLSocketClientWithTunneling.java -// here is the copyright notice: - -/* - * - * Copyright (c) 1994, 2004, Oracle and/or its affiliates. All rights reserved. - * - * Redistribution and use in source and binary forms, with or - * without modification, are permitted provided that the following - * conditions are met: - * - * -Redistribution of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * Redistribution in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in - * the documentation and/or other materials provided with the - * distribution. - * - * Neither the name of Oracle nor the names of - * contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * This software is provided "AS IS," without a warranty of any - * kind. ALL EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND - * WARRANTIES, INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY - * EXCLUDED. SUN MICROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL - * NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT - * OF USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS - * DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR - * ANY LOST REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, - * SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER - * CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF - * THE USE OF OR INABILITY TO USE THIS SOFTWARE, EVEN IF SUN HAS - * BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. - * - * You acknowledge that this software is not designed, licensed or - * intended for use in the design, construction, operation or - * maintenance of any nuclear facility. - */ - - -import com.nimbusds.jose.util.Base64; - -import java.net.*; -import java.io.*; -import javax.net.ssl.*; - - -/** - * SSLSocketFactory for tunneling ssl sockets through a proxy with Basic Authorization - */ -public class SSLTunnelSocketFactory extends SSLSocketFactory { - private SSLSocketFactory defaultFactory; - - private String tunnelHost; - - private int tunnelPort; - - private String proxyUserName; - - private String proxyPassword; - - public SSLTunnelSocketFactory(String proxyHost, String proxyPort, - String proxyUserName, String proxyPassword) { - tunnelHost = proxyHost; - tunnelPort = Integer.parseInt(proxyPort); - defaultFactory = (SSLSocketFactory) SSLSocketFactory.getDefault(); - - this.proxyUserName = proxyUserName; - this.proxyPassword = proxyPassword; - } - - public Socket createSocket(String host, int port) throws IOException { - return createSocket(null, host, port, true); - } - - public Socket createSocket(String host, int port, InetAddress clientHost, - int clientPort) throws IOException { - return createSocket(null, host, port, true); - } - - public Socket createSocket(InetAddress host, int port) throws IOException { - return createSocket(null, host.getHostName(), port, true); - } - - public Socket createSocket(InetAddress address, int port, - InetAddress clientAddress, int clientPort) throws IOException { - return createSocket(null, address.getHostName(), port, true); - } - - public Socket createSocket(Socket s, String host, int port, - boolean autoClose) throws IOException { - - Socket tunnel = new Socket(tunnelHost, tunnelPort); - - doTunnelHandshake(tunnel, host, port); - - SSLSocket result = (SSLSocket) defaultFactory.createSocket(tunnel, host, - port, autoClose); - - return result; - } - - private void doTunnelHandshake(Socket tunnel, String host, int port) - throws IOException { - OutputStream out = tunnel.getOutputStream(); - - String token = proxyUserName + ":" + proxyPassword; - String authString = "Basic " + Base64.encode(token.getBytes()); - - String msg = "CONNECT " + host + ":" + port + " HTTP/1.1\n" - + "User-Agent: " + sun.net.www.protocol.http.HttpURLConnection.userAgent + "\n" - + "Proxy-Authorization: " + authString - + "\r\n\r\n"; - - out.write(msg.getBytes("UTF-8")); - out.flush(); - - - StringBuilder replyStr = new StringBuilder(); - int newlinesSeen = 0; - boolean headerDone = false; /* Done on first newline */ - - InputStream in = tunnel.getInputStream(); - - while (newlinesSeen < 2) { - int i = in.read(); - if (i < 0) { - throw new IOException("Unexpected EOF from proxy"); - } - if (i == '\n') { - headerDone = true; - ++newlinesSeen; - } else if (i != '\r') { - newlinesSeen = 0; - if (!headerDone) { - replyStr.append((char) i); - } - } - } - - if (replyStr.toString().toLowerCase().indexOf("200 connection established") == -1) { - throw new IOException("Unable to tunnel through " + tunnelHost - + ":" + tunnelPort + ". Proxy returns \"" + replyStr + "\""); - } - } - - public String[] getDefaultCipherSuites() { - return defaultFactory.getDefaultCipherSuites(); - } - - public String[] getSupportedCipherSuites() { - return defaultFactory.getSupportedCipherSuites(); - } -} \ No newline at end of file diff --git a/src/samples/cache/TokenCacheAspect.java b/src/samples/cache/TokenCacheAspect.java new file mode 100644 index 00000000..cd26f54e --- /dev/null +++ b/src/samples/cache/TokenCacheAspect.java @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import com.microsoft.aad.msal4j.ITokenCacheAccessAspect; +import com.microsoft.aad.msal4j.ITokenCacheAccessContext; + +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Paths; + +public class TokenCacheAspect implements ITokenCacheAccessAspect { + + private String data; + + public TokenCacheAspect(String fileName) { + this.data = readDataFromFile(fileName); + } + + @Override + public void beforeCacheAccess(ITokenCacheAccessContext iTokenCacheAccessContext) { + iTokenCacheAccessContext.tokenCache().deserialize(data); + } + + @Override + public void afterCacheAccess(ITokenCacheAccessContext iTokenCacheAccessContext) { + data = iTokenCacheAccessContext.tokenCache().serialize(); + // you could implement logic here to write changes to file + } + + private static String readDataFromFile(String resource) { + try { + URL path = TokenCacheAspect.class.getResource(resource); + return new String( + Files.readAllBytes( + Paths.get(path.toURI()))); + } catch (Exception ex){ + System.out.println("Error reading data from file: " + ex.getMessage()); + throw new RuntimeException(ex); + } + } +} diff --git a/src/samples/cache/sample_cache.json b/src/samples/cache/sample_cache.json new file mode 100644 index 00000000..6334ff43 --- /dev/null +++ b/src/samples/cache/sample_cache.json @@ -0,0 +1,53 @@ +{ + "Account": { + "uid.utid-login.windows.net-contoso": { + "username": "John Doe", + "local_account_id": "object1234", + "realm": "contoso", + "environment": "login.windows.net", + "home_account_id": "uid.utid", + "authority_type": "MSSTS" + } + }, + "RefreshToken": { + "uid.utid-login.windows.net-refreshtoken-my_client_id--s2 s1 s3": { + "target": "s2 s1 s3", + "environment": "login.windows.net", + "credential_type": "RefreshToken", + "secret": "a refresh token", + "client_id": "my_client_id", + "home_account_id": "uid.utid" + } + }, + "AccessToken": { + "uid.utid-login.windows.net-accesstoken-my_client_id-contoso-s2 s1 s3": { + "environment": "login.windows.net", + "credential_type": "AccessToken", + "secret": "an access token", + "realm": "contoso", + "target": "s2 s1 s3", + "client_id": "my_client_id", + "cached_at": "1000", + "home_account_id": "uid.utid", + "extended_expires_on": "4600", + "expires_on": "4600" + } + }, + "IdToken": { + "uid.utid-login.windows.net-idtoken-my_client_id-contoso-": { + "realm": "contoso", + "environment": "login.windows.net", + "credential_type": "IdToken", + "secret": "header.eyJvaWQiOiAib2JqZWN0MTIzNCIsICJwcmVmZXJyZWRfdXNlcm5hbWUiOiAiSm9obiBEb2UiLCAic3ViIjogInN1YiJ9.signature", + "client_id": "my_client_id", + "home_account_id": "uid.utid" + } + }, + "AppMetadata": { + "appmetadata-login.windows.net-my_client_id": { + "environment": "login.windows.net", + "family_id": null, + "client_id": "my_client_id" + } + } +} \ No newline at end of file diff --git a/src/samples/confidential-client/ClientCredentialGrant.java b/src/samples/confidential-client/ClientCredentialGrant.java index a12caa3c..a510cd47 100644 --- a/src/samples/confidential-client/ClientCredentialGrant.java +++ b/src/samples/confidential-client/ClientCredentialGrant.java @@ -5,50 +5,66 @@ import com.microsoft.aad.msal4j.ClientCredentialParameters; import com.microsoft.aad.msal4j.ConfidentialClientApplication; import com.microsoft.aad.msal4j.IAuthenticationResult; +import com.microsoft.aad.msal4j.IClientCredential; +import com.microsoft.aad.msal4j.MsalException; import com.microsoft.aad.msal4j.SilentParameters; import java.util.Collections; -import java.util.concurrent.CompletableFuture; -import java.util.function.BiConsumer; +import java.util.Set; class ClientCredentialGrant { + private final static String CLIENT_ID = ""; + private final static String AUTHORITY = "https://login.microsoftonline.com//"; + private final static String CLIENT_SECRET = ""; + private final static Set SCOPE = Collections.singleton(""); + public static void main(String args[]) throws Exception { - getAccessTokenByClientCredentialGrant(); + IAuthenticationResult result = acquireToken(); + System.out.println("Access token: " + result.accessToken()); } - private static void getAccessTokenByClientCredentialGrant() throws Exception { - - ConfidentialClientApplication app = ConfidentialClientApplication.builder( - TestData.CONFIDENTIAL_CLIENT_ID, - ClientCredentialFactory.createFromSecret(TestData.CONFIDENTIAL_CLIENT_SECRET)) - .authority(TestData.TENANT_SPECIFIC_AUTHORITY) - .build(); + private static IAuthenticationResult acquireToken() throws Exception { - ClientCredentialParameters clientCredentialParam = ClientCredentialParameters.builder( - Collections.singleton(TestData.GRAPH_DEFAULT_SCOPE)) - .build(); + // Load token cache from file and initialize token cache aspect. The token cache will have + // dummy data, so the acquireTokenSilently call will fail. + TokenCacheAspect tokenCacheAspect = new TokenCacheAspect("sample_cache.json"); - CompletableFuture future = app.acquireToken(clientCredentialParam); + // This is the secret that is created in the Azure portal when registering the application + IClientCredential credential = ClientCredentialFactory.createFromSecret(CLIENT_SECRET); + ConfidentialClientApplication cca = + ConfidentialClientApplication + .builder(CLIENT_ID, credential) + .authority(AUTHORITY) + .setTokenCacheAccessAspect(tokenCacheAspect) + .build(); - BiConsumer processAuthResult = (res, ex) -> { - if (ex != null) { - System.out.println("Oops! We have an exception - " + ex.getMessage()); - } - System.out.println("Returned ok - " + res); - System.out.println("Access Token - " + res.accessToken()); - System.out.println("ID Token - " + res.idToken()); - }; + IAuthenticationResult result; + try { + SilentParameters silentParameters = + SilentParameters + .builder(SCOPE) + .build(); - future.whenCompleteAsync(processAuthResult); - future.join(); + // try to acquire token silently. This call will fail since the token cache does not + // have a token for the application you are requesting an access token for + result = cca.acquireTokenSilently(silentParameters).join(); + } catch (Exception ex) { + if (ex.getCause() instanceof MsalException) { - SilentParameters silentParameters = - SilentParameters.builder(Collections.singleton(TestData.GRAPH_DEFAULT_SCOPE)).build(); + ClientCredentialParameters parameters = + ClientCredentialParameters + .builder(SCOPE) + .build(); - future = app.acquireTokenSilently(silentParameters); - - future.whenCompleteAsync(processAuthResult); - future.join(); + // Try to acquire a token. If successful, you should see + // the token information printed out to console + result = cca.acquireToken(parameters).join(); + } else { + // Handle other exceptions accordingly + throw ex; + } + } + return result; } } diff --git a/src/samples/msal-b2c-web-sample/msal-b2c-web-sample.iml b/src/samples/msal-b2c-web-sample/msal-b2c-web-sample.iml deleted file mode 100644 index df646ed7..00000000 --- a/src/samples/msal-b2c-web-sample/msal-b2c-web-sample.iml +++ /dev/null @@ -1,102 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/samples/msal-b2c-web-sample/pom.xml b/src/samples/msal-b2c-web-sample/pom.xml index ae544f7b..8f1a8499 100644 --- a/src/samples/msal-b2c-web-sample/pom.xml +++ b/src/samples/msal-b2c-web-sample/pom.xml @@ -23,7 +23,7 @@ com.microsoft.azure msal4j - 1.3.0 + 1.4.0 com.nimbusds @@ -35,6 +35,12 @@ json 20090211 + + + org.apache.commons + commons-lang3 + 3.9 + org.springframework.boot diff --git a/src/samples/msal-b2c-web-sample/src/main/java/com/microsoft/azure/msalwebsample/AuthFilter.java b/src/samples/msal-b2c-web-sample/src/main/java/com/microsoft/azure/msalwebsample/AuthFilter.java index 1f69036f..c629ffa6 100644 --- a/src/samples/msal-b2c-web-sample/src/main/java/com/microsoft/azure/msalwebsample/AuthFilter.java +++ b/src/samples/msal-b2c-web-sample/src/main/java/com/microsoft/azure/msalwebsample/AuthFilter.java @@ -37,9 +37,7 @@ @Component public class AuthFilter implements Filter { - private static final String STATES = "states"; private static final String STATE = "state"; - private static final Integer STATE_TTL = 3600; private static final String FAILED_TO_VALIDATE_MESSAGE = "Failed to validate data received from Authorization service - "; private List excludedUrls = Arrays.asList("/", "/msal4jsample/"); @@ -69,6 +67,8 @@ public void doFilter(ServletRequest request, ServletResponse response, if(AuthHelper.containsAuthenticationCode(httpRequest)){ // response should have authentication code, which will be used to acquire access token processAuthenticationCodeRedirect(httpRequest, currentUri, fullUrl); + + CookieHelper.removeStateNonceCookies(httpResponse); } else { // not authenticated, redirecting to login.microsoft.com so user can authenticate sendAuthRedirect(authHelper.configuration.signUpSignInAuthority, httpRequest, httpResponse); @@ -107,7 +107,7 @@ private void processAuthenticationCodeRedirect(HttpServletRequest httpRequest, S params.put(key, Collections.singletonList(httpRequest.getParameterMap().get(key)[0])); } // validate that state in response equals to state in request - StateData stateData = validateState(httpRequest.getSession(), params.get(STATE).get(0)); + validateState(CookieHelper.getCookie(httpRequest, CookieHelper.MSAL_WEB_APP_STATE_COOKIE), params.get(STATE).get(0)); AuthenticationResponse authResponse = AuthenticationResponseParser.parse(new URI(fullUrl), params); if (AuthHelper.isAuthenticationSuccessful(authResponse)) { @@ -122,7 +122,9 @@ private void processAuthenticationCodeRedirect(HttpServletRequest httpRequest, S Collections.singleton(authHelper.configuration.apiScope)); // validate nonce to prevent reply attacks (code maybe substituted to one with broader access) - validateNonce(stateData, getNonceClaimValueFromIdToken(result.idToken())); + validateNonce(CookieHelper.getCookie(httpRequest, CookieHelper.MSAL_WEB_APP_NONCE_COOKIE), + getNonceClaimValueFromIdToken(result.idToken())); + authHelper.setSessionPrincipal(httpRequest, result); } else { AuthenticationErrorResponse oidcResponse = (AuthenticationErrorResponse) authResponse; @@ -136,31 +138,28 @@ void sendAuthRedirect(String authoriy, HttpServletRequest httpRequest, HttpServl // state parameter to validate response from Authorization server and nonce parameter to validate idToken String state = UUID.randomUUID().toString(); String nonce = UUID.randomUUID().toString(); - storeStateInSession(httpRequest.getSession(), state, nonce); + + CookieHelper.setStateNonceCookies(httpRequest, httpResponse, state, nonce); httpResponse.setStatus(302); String redirectUrl = getRedirectUrl(authoriy, httpRequest.getParameter("claims"), state, nonce); httpResponse.sendRedirect(redirectUrl); } - private void validateNonce(StateData stateData, String nonce) throws Exception { - if (StringUtils.isEmpty(nonce) || !nonce.equals(stateData.getNonce())) { - throw new Exception(FAILED_TO_VALIDATE_MESSAGE + "could not validate nonce"); - } - } - private String getNonceClaimValueFromIdToken(String idToken) throws ParseException { return (String) JWTParser.parse(idToken).getJWTClaimsSet().getClaim("nonce"); } - private StateData validateState(HttpSession session, String state) throws Exception { - if (StringUtils.isNotEmpty(state)) { - StateData stateDataInSession = removeStateFromSession(session, state); - if (stateDataInSession != null) { - return stateDataInSession; - } + private void validateState(String cookieValue, String state) throws Exception { + if (StringUtils.isEmpty(state) || !state.equals(cookieValue)) { + throw new Exception(FAILED_TO_VALIDATE_MESSAGE + "could not validate state"); + } + } + + private void validateNonce(String cookieValue, String nonce) throws Exception { + if (StringUtils.isEmpty(nonce) || !nonce.equals(cookieValue)) { + throw new Exception(FAILED_TO_VALIDATE_MESSAGE + "could not validate nonce"); } - throw new Exception(FAILED_TO_VALIDATE_MESSAGE + "could not validate state"); } private void validateAuthRespMatchesAuthCodeFlow(AuthenticationSuccessResponse oidcResponse) throws Exception { @@ -170,41 +169,6 @@ private void validateAuthRespMatchesAuthCodeFlow(AuthenticationSuccessResponse o } } - private void storeStateInSession(HttpSession session, String state, String nonce) { - if (session.getAttribute(STATES) == null) { - session.setAttribute(STATES, new HashMap()); - } - ((Map) session.getAttribute(STATES)).put(state, new StateData(nonce, new Date())); - } - - private StateData removeStateFromSession(HttpSession session, String state) { - Map states = (Map) session.getAttribute(STATES); - if (states != null) { - eliminateExpiredStates(states); - StateData stateData = states.get(state); - if (stateData != null) { - states.remove(state); - return stateData; - } - } - return null; - } - - private void eliminateExpiredStates(Map map) { - Iterator> it = map.entrySet().iterator(); - - Date currTime = new Date(); - while (it.hasNext()) { - Map.Entry entry = it.next(); - long diffInSeconds = TimeUnit.MILLISECONDS. - toSeconds(currTime.getTime() - entry.getValue().getExpirationDate().getTime()); - - if (diffInSeconds > STATE_TTL) { - it.remove(); - } - } - } - private String getRedirectUrl(String authority, String claims, String state, String nonce) throws UnsupportedEncodingException { diff --git a/src/samples/msal-b2c-web-sample/src/main/java/com/microsoft/azure/msalwebsample/CookieHelper.java b/src/samples/msal-b2c-web-sample/src/main/java/com/microsoft/azure/msalwebsample/CookieHelper.java new file mode 100644 index 00000000..76960a95 --- /dev/null +++ b/src/samples/msal-b2c-web-sample/src/main/java/com/microsoft/azure/msalwebsample/CookieHelper.java @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.msalwebsample; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class CookieHelper { + + static final String MSAL_WEB_APP_STATE_COOKIE = "msal_web_app_auth_state"; + static final String MSAL_WEB_APP_NONCE_COOKIE = "msal_web_app_auth_nonce"; + + static void setStateNonceCookies + (HttpServletRequest httpRequest, HttpServletResponse httpResponse, String state, String nonce){ + + boolean userAgentSameSiteNoneAware = + CookieHelper.isUserAgentAwareOfSameSiteNone(httpRequest.getHeader("User-Agent")); + + String sameSiteCookieAttribute = userAgentSameSiteNoneAware ? "; SameSite=none" : ""; + + httpResponse.addHeader("Set-Cookie", + MSAL_WEB_APP_STATE_COOKIE + "=" + state + "; secure; HttpOnly" + sameSiteCookieAttribute); + + httpResponse.addHeader("Set-Cookie", + MSAL_WEB_APP_NONCE_COOKIE + "=" + nonce + "; secure; HttpOnly" + sameSiteCookieAttribute); + } + + static void removeStateNonceCookies(HttpServletResponse httpResponse){ + + Cookie stateCookie = new Cookie(MSAL_WEB_APP_STATE_COOKIE, ""); + stateCookie.setMaxAge(0); + + httpResponse.addCookie(stateCookie); + + Cookie nonceCookie = new Cookie(MSAL_WEB_APP_NONCE_COOKIE, ""); + nonceCookie.setMaxAge(0); + + httpResponse.addCookie(nonceCookie); + } + + static String getCookie(HttpServletRequest httpRequest, String cookieName){ + for(Cookie cookie : httpRequest.getCookies()){ + if(cookie.getName().equals(cookieName)){ + return cookie.getValue(); + } + } + return null; + } + + /** + * Check whether user agent support "None" value of "SameSite" attribute of cookies + * + * The following code is for demonstration only: It should not be considered complete. + * It is not maintained or supported. + * + * @param userAgent + * @return true if user agent supports "None" value of "SameSite" attribute of cookies, + * false otherwise + */ + static boolean isUserAgentAwareOfSameSiteNone(String userAgent){ + + // Cover all iOS based browsers here. This includes: + // - Safari on iOS 12 for iPhone, iPod Touch, iPad + // - WkWebview on iOS 12 for iPhone, iPod Touch, iPad + // - Chrome on iOS 12 for iPhone, iPod Touch, iPad + // All of which are broken by SameSite=None, because they use the iOS networking + // stack. + if(userAgent.contains("CPU iPhone OS 12") || userAgent.contains("iPad; CPU OS 12")){ + return false; + } + + // Cover Mac OS X based browsers that use the Mac OS networking stack. + // This includes: + // - Safari on Mac OS X. + // This does not include: + // - Chrome on Mac OS X + // Because they do not use the Mac OS networking stack. + if (userAgent.contains("Macintosh; Intel Mac OS X 10_14") && + userAgent.contains("Version/") && userAgent.contains("Safari")) { + return false; + } + + // Cover Chrome 50-69, because some versions are broken by SameSite=None, + // and none in this range require it. + // Note: this covers some pre-Chromium Edge versions, + // but pre-Chromium Edge does not require SameSite=None. + if(userAgent.contains("Chrome/5") || userAgent.contains("Chrome/6")){ + return false; + } + + return true; + } +} diff --git a/src/samples/msal-b2c-web-sample/src/main/resources/application.properties b/src/samples/msal-b2c-web-sample/src/main/resources/application.properties index 6045ba67..c275bb81 100644 --- a/src/samples/msal-b2c-web-sample/src/main/resources/application.properties +++ b/src/samples/msal-b2c-web-sample/src/main/resources/application.properties @@ -1,17 +1,29 @@ -b2c.tenant= -b2c.host= - -b2c.authority.base=https://${b2c.host}/tfp/${b2c.tenant}/ +b2c.tenant=fabrikamb2c.onmicrosoft.com​ +b2c.host=fabrikamb2c.b2clogin.com​ +​ +b2c.authority.base=https://${b2c.host}/tfp/${b2c.tenant}/​ b2c.clientId= b2c.secret= -b2c.redirectUri=http://localhost:8080/msal4jsample/secure/aad +b2c.redirectUri=https://localhost:8080/msal4jsample/secure/aad​ +​ + +b2c.api=https://fabrikamb2chello.azurewebsites.net/hello​ +b2c.api-scope=https://fabrikamb2c.onmicrosoft.com/helloapi/demo.read​ +​ +policy.sign-up-sign-in=b2c_1_susi​ +policy.edit-profile=b2c_1_edit_profile​ +​ +​ +b2c.sign-up-sign-in-authority=${b2c.authority.base}${policy.sign-up-sign-in}/​ +b2c.edit-profile-authority=${b2c.authority.base}${policy.edit-profile}/​ +b2c.reset-password-authority=${b2c.authority.base}${policy.reset-password}/​ -b2c.api= -b2c.api-scope= +server.port=8080 -policy.sign-up-sign-in=b2c_1_susi -policy.edit-profile=b2c_1_edit_profile +server.servlet.session.cookie.secure=true -b2c.sign-up-sign-in-authority=${b2c.authority.base}${policy.sign-up-sign-in}/ -b2c.edit-profile-authority=${b2c.authority.base}${policy.edit-profile}/ -b2c.reset-password-authority=${b2c.authority.base}${policy.reset-password}/ \ No newline at end of file +server.ssl.key-store=classpath:Enter_Key_Store_Here +server.ssl.key-store-password=Enter_Key_Store_Password_Here +server.ssl.key-store-type=Enter_Key_Store_Type_Here +server.ssl.key-alias=Enter_Key_Alias_Here +server.ssl.key-password=Enter_Key_Password_Here \ No newline at end of file diff --git a/src/samples/msal-obo-sample/pom.xml b/src/samples/msal-obo-sample/pom.xml index 151c4d6d..c27898fb 100644 --- a/src/samples/msal-obo-sample/pom.xml +++ b/src/samples/msal-obo-sample/pom.xml @@ -23,7 +23,7 @@ com.microsoft.azure msal4j - 1.3.0 + 1.4.0 com.nimbusds diff --git a/src/samples/msal-web-sample/pom.xml b/src/samples/msal-web-sample/pom.xml index 947e215c..c37e3026 100644 --- a/src/samples/msal-web-sample/pom.xml +++ b/src/samples/msal-web-sample/pom.xml @@ -5,7 +5,7 @@ org.springframework.boot spring-boot-starter-parent - 2.1.4.RELEASE + 2.2.2.RELEASE com.microsoft.azure @@ -23,7 +23,7 @@ com.microsoft.azure msal4j - 1.3.0 + 1.4.0 com.nimbusds @@ -35,6 +35,12 @@ json 20090211 + + org.apache.commons + commons-lang3 + 3.9 + + org.springframework.boot @@ -54,6 +60,12 @@ spring-boot-starter-test test + + + org.springframework.session + spring-session-core + + diff --git a/src/samples/msal-web-sample/src/main/java/com/microsoft/azure/msalwebsample/AuthFilter.java b/src/samples/msal-web-sample/src/main/java/com/microsoft/azure/msalwebsample/AuthFilter.java index d2dac781..2a983a4c 100644 --- a/src/samples/msal-web-sample/src/main/java/com/microsoft/azure/msalwebsample/AuthFilter.java +++ b/src/samples/msal-web-sample/src/main/java/com/microsoft/azure/msalwebsample/AuthFilter.java @@ -5,27 +5,24 @@ import java.io.IOException; import java.io.UnsupportedEncodingException; -import java.net.MalformedURLException; import java.net.URI; import java.net.URLEncoder; import java.text.ParseException; import java.util.*; import java.util.concurrent.*; -import javax.naming.ServiceUnavailableException; import javax.servlet.Filter; import javax.servlet.FilterChain; -import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; +import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import com.microsoft.aad.msal4j.*; import com.nimbusds.jwt.JWTParser; -import com.nimbusds.oauth2.sdk.AuthorizationCode; import com.nimbusds.openid.connect.sdk.AuthenticationErrorResponse; import com.nimbusds.openid.connect.sdk.AuthenticationResponse; import com.nimbusds.openid.connect.sdk.AuthenticationResponseParser; @@ -37,10 +34,10 @@ @Component public class AuthFilter implements Filter { - private static final String STATES = "states"; private static final String STATE = "state"; - private static final Integer STATE_TTL = 3600; private static final String FAILED_TO_VALIDATE_MESSAGE = "Failed to validate data received from Authorization service - "; + public static final String MSAL_WEB_APP_STATE_COOKIE = "msal_web_app_auth_state"; + public static final String MSAL_WEB_APP_NONCE_COOKIE = "msal_web_app_auth_nonce"; private List excludedUrls = Arrays.asList("/", "/msal4jsample/"); @@ -69,6 +66,8 @@ public void doFilter(ServletRequest request, ServletResponse response, if(AuthHelper.containsAuthenticationCode(httpRequest)){ // response should have authentication code, which will be used to acquire access token processAuthenticationCodeRedirect(httpRequest, currentUri, fullUrl); + + CookieHelper.removeStateNonceCookies(httpResponse); } else { // not authenticated, redirecting to login.microsoft.com so user can authenticate sendAuthRedirect(httpRequest, httpResponse); @@ -107,7 +106,7 @@ private void processAuthenticationCodeRedirect(HttpServletRequest httpRequest, S params.put(key, Collections.singletonList(httpRequest.getParameterMap().get(key)[0])); } // validate that state in response equals to state in request - StateData stateData = validateState(httpRequest.getSession(), params.get(STATE).get(0)); + validateState(CookieHelper.getCookie(httpRequest, MSAL_WEB_APP_STATE_COOKIE), params.get(STATE).get(0)); AuthenticationResponse authResponse = AuthenticationResponseParser.parse(new URI(fullUrl), params); if (AuthHelper.isAuthenticationSuccessful(authResponse)) { @@ -121,7 +120,8 @@ private void processAuthenticationCodeRedirect(HttpServletRequest httpRequest, S currentUri); // validate nonce to prevent reply attacks (code maybe substituted to one with broader access) - validateNonce(stateData, getNonceClaimValueFromIdToken(result.idToken())); + validateNonce(CookieHelper.getCookie(httpRequest, MSAL_WEB_APP_NONCE_COOKIE), getNonceClaimValueFromIdToken(result.idToken())); + authHelper.setSessionPrincipal(httpRequest, result); } else { AuthenticationErrorResponse oidcResponse = (AuthenticationErrorResponse) authResponse; @@ -135,31 +135,29 @@ private void sendAuthRedirect(HttpServletRequest httpRequest, HttpServletRespons // state parameter to validate response from Authorization server and nonce parameter to validate idToken String state = UUID.randomUUID().toString(); String nonce = UUID.randomUUID().toString(); - storeStateInSession(httpRequest.getSession(), state, nonce); + + CookieHelper.setStateNonceCookies(httpRequest, httpResponse, state, nonce); httpResponse.setStatus(302); String redirectUrl = getRedirectUrl(httpRequest.getParameter("claims"), state, nonce); - httpResponse.sendRedirect(redirectUrl); - } - private void validateNonce(StateData stateData, String nonce) throws Exception { - if (StringUtils.isEmpty(nonce) || !nonce.equals(stateData.getNonce())) { - throw new Exception(FAILED_TO_VALIDATE_MESSAGE + "could not validate nonce"); - } + httpResponse.sendRedirect(redirectUrl); } private String getNonceClaimValueFromIdToken(String idToken) throws ParseException { return (String) JWTParser.parse(idToken).getJWTClaimsSet().getClaim("nonce"); } - private StateData validateState(HttpSession session, String state) throws Exception { - if (StringUtils.isNotEmpty(state)) { - StateData stateDataInSession = removeStateFromSession(session, state); - if (stateDataInSession != null) { - return stateDataInSession; - } + private void validateState(String cookieValue, String state) throws Exception { + if (StringUtils.isEmpty(state) || !state.equals(cookieValue)) { + throw new Exception(FAILED_TO_VALIDATE_MESSAGE + "could not validate state"); + } + } + + private void validateNonce(String cookieValue, String nonce) throws Exception { + if (StringUtils.isEmpty(nonce) || !nonce.equals(cookieValue)) { + throw new Exception(FAILED_TO_VALIDATE_MESSAGE + "could not validate nonce"); } - throw new Exception(FAILED_TO_VALIDATE_MESSAGE + "could not validate state"); } private void validateAuthRespMatchesAuthCodeFlow(AuthenticationSuccessResponse oidcResponse) throws Exception { @@ -169,41 +167,6 @@ private void validateAuthRespMatchesAuthCodeFlow(AuthenticationSuccessResponse o } } - private void storeStateInSession(HttpSession session, String state, String nonce) { - if (session.getAttribute(STATES) == null) { - session.setAttribute(STATES, new HashMap()); - } - ((Map) session.getAttribute(STATES)).put(state, new StateData(nonce, new Date())); - } - - private StateData removeStateFromSession(HttpSession session, String state) { - Map states = (Map) session.getAttribute(STATES); - if (states != null) { - eliminateExpiredStates(states); - StateData stateData = states.get(state); - if (stateData != null) { - states.remove(state); - return stateData; - } - } - return null; - } - - private void eliminateExpiredStates(Map map) { - Iterator> it = map.entrySet().iterator(); - - Date currTime = new Date(); - while (it.hasNext()) { - Map.Entry entry = it.next(); - long diffInSeconds = TimeUnit.MILLISECONDS. - toSeconds(currTime.getTime() - entry.getValue().getExpirationDate().getTime()); - - if (diffInSeconds > STATE_TTL) { - it.remove(); - } - } - } - private String getRedirectUrl(String claims, String state, String nonce) throws UnsupportedEncodingException { diff --git a/src/samples/msal-web-sample/src/main/java/com/microsoft/azure/msalwebsample/AuthPageController.java b/src/samples/msal-web-sample/src/main/java/com/microsoft/azure/msalwebsample/AuthPageController.java index 00ef6d98..3d3f4962 100644 --- a/src/samples/msal-web-sample/src/main/java/com/microsoft/azure/msalwebsample/AuthPageController.java +++ b/src/samples/msal-web-sample/src/main/java/com/microsoft/azure/msalwebsample/AuthPageController.java @@ -56,7 +56,7 @@ public void signOut(HttpServletRequest httpRequest, HttpServletResponse response httpRequest.getSession().invalidate(); - String redirectUrl = "http://localhost:8080/msal4jsample/"; + String redirectUrl = "https://localhost:8080/msal4jsample/"; response.sendRedirect(AuthHelper.END_SESSION_ENDPOINT + "?post_logout_redirect_uri=" + URLEncoder.encode(redirectUrl, "UTF-8")); } diff --git a/src/samples/msal-web-sample/src/main/java/com/microsoft/azure/msalwebsample/CookieHelper.java b/src/samples/msal-web-sample/src/main/java/com/microsoft/azure/msalwebsample/CookieHelper.java new file mode 100644 index 00000000..76960a95 --- /dev/null +++ b/src/samples/msal-web-sample/src/main/java/com/microsoft/azure/msalwebsample/CookieHelper.java @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.msalwebsample; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class CookieHelper { + + static final String MSAL_WEB_APP_STATE_COOKIE = "msal_web_app_auth_state"; + static final String MSAL_WEB_APP_NONCE_COOKIE = "msal_web_app_auth_nonce"; + + static void setStateNonceCookies + (HttpServletRequest httpRequest, HttpServletResponse httpResponse, String state, String nonce){ + + boolean userAgentSameSiteNoneAware = + CookieHelper.isUserAgentAwareOfSameSiteNone(httpRequest.getHeader("User-Agent")); + + String sameSiteCookieAttribute = userAgentSameSiteNoneAware ? "; SameSite=none" : ""; + + httpResponse.addHeader("Set-Cookie", + MSAL_WEB_APP_STATE_COOKIE + "=" + state + "; secure; HttpOnly" + sameSiteCookieAttribute); + + httpResponse.addHeader("Set-Cookie", + MSAL_WEB_APP_NONCE_COOKIE + "=" + nonce + "; secure; HttpOnly" + sameSiteCookieAttribute); + } + + static void removeStateNonceCookies(HttpServletResponse httpResponse){ + + Cookie stateCookie = new Cookie(MSAL_WEB_APP_STATE_COOKIE, ""); + stateCookie.setMaxAge(0); + + httpResponse.addCookie(stateCookie); + + Cookie nonceCookie = new Cookie(MSAL_WEB_APP_NONCE_COOKIE, ""); + nonceCookie.setMaxAge(0); + + httpResponse.addCookie(nonceCookie); + } + + static String getCookie(HttpServletRequest httpRequest, String cookieName){ + for(Cookie cookie : httpRequest.getCookies()){ + if(cookie.getName().equals(cookieName)){ + return cookie.getValue(); + } + } + return null; + } + + /** + * Check whether user agent support "None" value of "SameSite" attribute of cookies + * + * The following code is for demonstration only: It should not be considered complete. + * It is not maintained or supported. + * + * @param userAgent + * @return true if user agent supports "None" value of "SameSite" attribute of cookies, + * false otherwise + */ + static boolean isUserAgentAwareOfSameSiteNone(String userAgent){ + + // Cover all iOS based browsers here. This includes: + // - Safari on iOS 12 for iPhone, iPod Touch, iPad + // - WkWebview on iOS 12 for iPhone, iPod Touch, iPad + // - Chrome on iOS 12 for iPhone, iPod Touch, iPad + // All of which are broken by SameSite=None, because they use the iOS networking + // stack. + if(userAgent.contains("CPU iPhone OS 12") || userAgent.contains("iPad; CPU OS 12")){ + return false; + } + + // Cover Mac OS X based browsers that use the Mac OS networking stack. + // This includes: + // - Safari on Mac OS X. + // This does not include: + // - Chrome on Mac OS X + // Because they do not use the Mac OS networking stack. + if (userAgent.contains("Macintosh; Intel Mac OS X 10_14") && + userAgent.contains("Version/") && userAgent.contains("Safari")) { + return false; + } + + // Cover Chrome 50-69, because some versions are broken by SameSite=None, + // and none in this range require it. + // Note: this covers some pre-Chromium Edge versions, + // but pre-Chromium Edge does not require SameSite=None. + if(userAgent.contains("Chrome/5") || userAgent.contains("Chrome/6")){ + return false; + } + + return true; + } +} diff --git a/src/samples/msal-web-sample/src/main/resources/application.properties b/src/samples/msal-web-sample/src/main/resources/application.properties index 97fd8ed5..fa02118a 100644 --- a/src/samples/msal-web-sample/src/main/resources/application.properties +++ b/src/samples/msal-web-sample/src/main/resources/application.properties @@ -1,5 +1,15 @@ aad.authority=https://login.microsoftonline.com/common/ aad.clientId= aad.secretKey= -aad.redirectUri=http://localhost:8080/msal4jsample/secure/aad -aad.oboApi=/access_as_user \ No newline at end of file +aad.redirectUri=https://localhost:8080/msal4jsample/secure/aad +aad.oboApi=https://pesomka.onmicrosoft.com/TodoListService/access_as_user + +server.port=8080 + +server.servlet.session.cookie.secure=true + +server.ssl.key-store=classpath:keystore.p12 +server.ssl.key-store-password= +server.ssl.key-store-type=pkcs12 +server.ssl.key-alias=tomcat +server.ssl.key-password= \ No newline at end of file diff --git a/src/samples/public-client/B2CFlow.java b/src/samples/public-client/B2CFlow.java deleted file mode 100644 index d8dae7fd..00000000 --- a/src/samples/public-client/B2CFlow.java +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import com.microsoft.aad.msal4j.IAuthenticationResult; -import com.microsoft.aad.msal4j.PublicClientApplication; -import com.microsoft.aad.msal4j.UserNamePasswordParameters; - - -import java.util.Collections; -import java.util.concurrent.CompletableFuture; - -public class B2CFlow { - - public static void main(String args[]) throws Exception { - getAccessTokenFromUserCredentials(); - } - - private static void getAccessTokenFromUserCredentials() throws Exception { - - PublicClientApplication app = PublicClientApplication.builder(TestData.PUBLIC_CLIENT_ID) - .b2cAuthority(TestData.B2C_AUTHORITY) - .build(); - - CompletableFuture future = app.acquireToken( - UserNamePasswordParameters.builder( - Collections.singleton(TestData.LAB_DEFAULT_B2C_SCOPE), - TestData.USER_NAME, - TestData.USER_PASSWORD.toCharArray()).build()); - - future.handle((res, ex) -> { - if(ex != null) { - System.out.println("Oops! We have an exception - " + ex.getMessage()); - return "Unknown!"; - } - System.out.println("Returned ok - " + res); - - System.out.println("Access Token - " + res.accessToken()); - System.out.println("ID Token - " + res.idToken()); - return res; - }).join(); - } -} \ No newline at end of file diff --git a/src/samples/public-client/DeviceCodeFlow.java b/src/samples/public-client/DeviceCodeFlow.java index e928db56..cfc4b26e 100644 --- a/src/samples/public-client/DeviceCodeFlow.java +++ b/src/samples/public-client/DeviceCodeFlow.java @@ -4,42 +4,68 @@ import com.microsoft.aad.msal4j.*; import java.util.Collections; -import java.util.concurrent.CompletableFuture; +import java.util.Set; import java.util.function.Consumer; public class DeviceCodeFlow { + + private final static String CLIENT_ID = ""; + private final static String AUTHORITY = "https://login.microsoftonline.com/common/"; + private final static Set SCOPE = Collections.singleton(""); + public static void main(String args[]) throws Exception { - getAccessTokenByDeviceCodeGrant(); + IAuthenticationResult result = acquireTokenDeviceCode(); + System.out.println("Access token: " + result.accessToken()); + System.out.println("Id token: " + result.idToken()); + System.out.println("Account username: " + result.account().username()); } - private static void getAccessTokenByDeviceCodeGrant() throws Exception { - PublicClientApplication app = PublicClientApplication.builder(TestData.PUBLIC_CLIENT_ID) - .authority(TestData.AUTHORITY_COMMON) + private static IAuthenticationResult acquireTokenDeviceCode() throws Exception { + + // Load token cache from file and initialize token cache aspect. The token cache will have + // dummy data, so the acquireTokenSilently call will fail. + TokenCacheAspect tokenCacheAspect = new TokenCacheAspect("sample_cache.json"); + + PublicClientApplication pca = PublicClientApplication.builder(CLIENT_ID) + .authority(AUTHORITY) + .setTokenCacheAccessAspect(tokenCacheAspect) .build(); - Consumer deviceCodeConsumer = (DeviceCode deviceCode) -> { - System.out.println(deviceCode.message()); - }; - - CompletableFuture future = app.acquireToken( - DeviceCodeFlowParameters.builder( - Collections.singleton(TestData.GRAPH_DEFAULT_SCOPE), - deviceCodeConsumer) - .build()); - - future.handle((res, ex) -> { - if(ex != null) { - System.out.println("Oops! We have an exception of type - " + ex.getClass()); - System.out.println("message - " + ex.getMessage()); - return "Unknown!"; - } - System.out.println("Returned ok - " + res); + Set accountsInCache = pca.getAccounts().join(); + // Take first account in the cache. In a production application, you would filter + // accountsInCache to get the right account for the user authenticating. + IAccount account = accountsInCache.iterator().next(); - System.out.println("Access Token - " + res.accessToken()); - System.out.println("ID Token - " + res.idToken()); - return res; - }); + IAuthenticationResult result; + try { + SilentParameters silentParameters = + SilentParameters + .builder(SCOPE, account) + .build(); - future.join(); + // try to acquire token silently. This call will fail since the token cache + // does not have any data for the user you are trying to acquire a token for + result = pca.acquireTokenSilently(silentParameters).join(); + } catch (Exception ex) { + if (ex.getCause() instanceof MsalException) { + + Consumer deviceCodeConsumer = (DeviceCode deviceCode) -> + System.out.println(deviceCode.message()); + + DeviceCodeFlowParameters parameters = + DeviceCodeFlowParameters + .builder(SCOPE, deviceCodeConsumer) + .build(); + + // Try to acquire a token via device code flow. If successful, you should see + // the token and account information printed out to console, and the sample_cache.json + // file should have been updated with the latest tokens. + result = pca.acquireToken(parameters).join(); + } else { + // Handle other exceptions accordingly + throw ex; + } + } + return result; } } diff --git a/src/samples/public-client/IntegratedWindowsAuthFlow.java b/src/samples/public-client/IntegratedWindowsAuthFlow.java deleted file mode 100644 index 342196c1..00000000 --- a/src/samples/public-client/IntegratedWindowsAuthFlow.java +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import com.microsoft.aad.msal4j.IAuthenticationResult; -import com.microsoft.aad.msal4j.IntegratedWindowsAuthenticationParameters; -import com.microsoft.aad.msal4j.PublicClientApplication; - -import java.util.Collections; -import java.util.concurrent.Future; - -public class IntegratedWindowsAuthFlow { - public static void main(String args[]) throws Exception { - - IAuthenticationResult result = getAccessTokenByIntegratedAuth(); - - System.out.println("Access Token - " + result.accessToken()); - System.out.println("ID Token - " + result.idToken()); - } - - private static IAuthenticationResult getAccessTokenByIntegratedAuth() throws Exception { - PublicClientApplication app = PublicClientApplication.builder(TestData.PUBLIC_CLIENT_ID) - .authority(TestData.AUTHORITY_ORGANIZATION) - .build(); - - IntegratedWindowsAuthenticationParameters parameters = - IntegratedWindowsAuthenticationParameters.builder( - Collections.singleton(TestData.GRAPH_DEFAULT_SCOPE), TestData.USER_NAME) - .build(); - - Future future = app.acquireToken(parameters); - - IAuthenticationResult result = future.get(); - - return result; - } -} diff --git a/src/samples/public-client/IntegratedWindowsAuthenticationFlow.java b/src/samples/public-client/IntegratedWindowsAuthenticationFlow.java new file mode 100644 index 00000000..61faddc7 --- /dev/null +++ b/src/samples/public-client/IntegratedWindowsAuthenticationFlow.java @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import com.microsoft.aad.msal4j.IAccount; +import com.microsoft.aad.msal4j.IAuthenticationResult; +import com.microsoft.aad.msal4j.IntegratedWindowsAuthenticationParameters; +import com.microsoft.aad.msal4j.MsalException; +import com.microsoft.aad.msal4j.PublicClientApplication; +import com.microsoft.aad.msal4j.SilentParameters; + +import java.util.Collections; +import java.util.Set; + +public class IntegratedWindowsAuthenticationFlow { + + private final static String CLIENT_ID = ""; + private final static String AUTHORITY = "https://login.microsoftonline.com/organizations/"; + private final static Set SCOPE = Collections.singleton(""); + private final static String USER_NAME = ""; + + public static void main(String args[]) throws Exception { + + IAuthenticationResult result = acquireTokenIwa(); + System.out.println("Access token: " + result.accessToken()); + System.out.println("Id token: " + result.idToken()); + System.out.println("Account username: " + result.account().username()); + } + + private static IAuthenticationResult acquireTokenIwa() throws Exception { + + // Load token cache from file and initialize token cache aspect. The token cache will have + // dummy data, so the acquireTokenSilently call will fail. + TokenCacheAspect tokenCacheAspect = new TokenCacheAspect("sample_cache.json"); + + PublicClientApplication pca = PublicClientApplication.builder(CLIENT_ID) + .authority(AUTHORITY) + .setTokenCacheAccessAspect(tokenCacheAspect) + .build(); + + Set accountsInCache = pca.getAccounts().join(); + // Take first account in the cache. In a production application, you would filter + // accountsInCache to get the right account for the user authenticating. + IAccount account = accountsInCache.iterator().next(); + + IAuthenticationResult result; + try { + SilentParameters silentParameters = + SilentParameters + .builder(SCOPE, account) + .build(); + + // try to acquire token silently. This call will fail since the token cache + // does not have any data for the user you are trying to acquire a token for + result = pca.acquireTokenSilently(silentParameters).join(); + } catch (Exception ex) { + if (ex.getCause() instanceof MsalException) { + + IntegratedWindowsAuthenticationParameters parameters = + IntegratedWindowsAuthenticationParameters + .builder(SCOPE, USER_NAME) + .build(); + + // Try to acquire a IWA. You will need to generate a Kerberos ticket. + // If successful, you should see the token and account information printed out to + // console + result = pca.acquireToken(parameters).join(); + } else { + // Handle other exceptions accordingly + throw ex; + } + } + return result; + } +} diff --git a/src/samples/public-client/InteractiveFlow.java b/src/samples/public-client/InteractiveFlow.java new file mode 100644 index 00000000..806e2056 --- /dev/null +++ b/src/samples/public-client/InteractiveFlow.java @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +//// Licensed under the MIT License. + +import com.microsoft.aad.msal4j.IAccount; +import com.microsoft.aad.msal4j.IAuthenticationResult; +import com.microsoft.aad.msal4j.InteractiveRequestParameters; +import com.microsoft.aad.msal4j.MsalException; +import com.microsoft.aad.msal4j.PublicClientApplication; +import com.microsoft.aad.msal4j.SilentParameters; + +import java.net.URI; +import java.util.Collections; +import java.util.Set; + +public class InteractiveFlow { + + private final static String CLIENT_ID = ""; + private final static String AUTHORITY = ""; + private final static Set SCOPE = Collections.singleton(""); + + public static void main(String[] args) throws Exception{ + IAuthenticationResult result = acquireTokenInteractive(); + System.out.println("Access token: " + result.accessToken()); + System.out.println("Id token: " + result.idToken()); + System.out.println("Account username: " + result.account().username()); + } + + private static IAuthenticationResult acquireTokenInteractive() throws Exception { + + // Load token cache from file and initialize token cache aspect. The token cache will have + // dummy data, so the acquireTokenSilently call will fail. + TokenCacheAspect tokenCacheAspect = new TokenCacheAspect("sample_cache.json"); + + PublicClientApplication pca = PublicClientApplication.builder(CLIENT_ID) + .authority(AUTHORITY) + .setTokenCacheAccessAspect(tokenCacheAspect) + .build(); + + Set accountsInCache = pca.getAccounts().join(); + // Take first account in the cache. In a production application, you would filter + // accountsInCache to get the right account for the user authenticating. + IAccount account = accountsInCache.iterator().next(); + + IAuthenticationResult result; + try { + SilentParameters silentParameters = + SilentParameters + .builder(SCOPE, account) + .build(); + + // try to acquire token silently. This call will fail since the token cache + // does not have any data for the user you are trying to acquire a token for + result = pca.acquireTokenSilently(silentParameters).join(); + } catch (Exception ex) { + if (ex.getCause() instanceof MsalException) { + + InteractiveRequestParameters parameters = InteractiveRequestParameters + .builder(new URI("http://localhost")) + .scopes(SCOPE) + .build(); + + // Try to acquire a token interactively with system browser. If successful, you should see + // the token and account information printed out to console + result = pca.acquireToken(parameters).join(); + } else { + // Handle other exceptions accordingly + throw ex; + } + } + return result; + } +} \ No newline at end of file diff --git a/src/samples/public-client/InteractiveFlowB2C.java b/src/samples/public-client/InteractiveFlowB2C.java new file mode 100644 index 00000000..8c405498 --- /dev/null +++ b/src/samples/public-client/InteractiveFlowB2C.java @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import com.microsoft.aad.msal4j.IAccount; +import com.microsoft.aad.msal4j.IAuthenticationResult; +import com.microsoft.aad.msal4j.InteractiveRequestParameters; +import com.microsoft.aad.msal4j.MsalException; +import com.microsoft.aad.msal4j.PublicClientApplication; +import com.microsoft.aad.msal4j.SilentParameters; + +import java.net.URI; +import java.util.Collections; +import java.util.Set; + +public class InteractiveFlowB2C { + + private final static String CLIENT_ID = ""; + private final static String AUTHORITY = "https://.b2clogin.com/tfp//.com/"; + private final static Set SCOPE = Collections.singleton(""); + + public static void main(String args[]) throws Exception { + IAuthenticationResult result = acquireTokenInteractiveB2C(); + System.out.println("Access token: " + result.accessToken()); + System.out.println("Id token: " + result.idToken()); + System.out.println("Account username: " + result.account().username()); + } + + private static IAuthenticationResult acquireTokenInteractiveB2C() throws Exception { + + // Load token cache from file and initialize token cache aspect. The token cache will have + // dummy data, so the acquireTokenSilently call will fail. + TokenCacheAspect tokenCacheAspect = new TokenCacheAspect("sample_cache.json"); + + PublicClientApplication pca = + PublicClientApplication + .builder(CLIENT_ID) + .b2cAuthority(AUTHORITY) + .setTokenCacheAccessAspect(tokenCacheAspect) + .build(); + + Set accountsInCache = pca.getAccounts().join(); + // Use first account in the cache. In a production application, you would filter + // accountsInCache to get the right account for the user authenticating. + IAccount account = accountsInCache.iterator().next(); + + IAuthenticationResult result; + try { + SilentParameters silentParameters = + SilentParameters + .builder(SCOPE, account) + .build(); + + // try to acquire token silently. This call will fail since the token cache + // does not have any data for the user you are trying to acquire a token for + result = pca.acquireTokenSilently(silentParameters).join(); + } catch (Exception ex) { + if (ex.getCause() instanceof MsalException) { + + // For B2C, you have to specify a port for the redirect URL + InteractiveRequestParameters parameters = InteractiveRequestParameters + .builder(new URI("http://localhost:8080")) + .scopes(SCOPE) + .build(); + + // Try to acquire a token interactively with system browser. If successful, you should see + // the token and account information printed out to console + result = pca.acquireToken(parameters).join(); + } else { + // Handle other exceptions accordingly + throw ex; + } + } + return result; + } +} \ No newline at end of file diff --git a/src/samples/public-client/TestData.java b/src/samples/public-client/TestData.java deleted file mode 100644 index af70e5b9..00000000 --- a/src/samples/public-client/TestData.java +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -public class TestData { - - final static String TENANT_SPECIFIC_AUTHORITY = "https://login.microsoftonline.com//"; - - final static String AUTHORITY_COMMON = "https://login.microsoftonline.com/common/"; - final static String AUTHORITY_ORGANIZATION = "https://login.microsoftonline.com/organizations/"; - final static String B2C_AUTHORITY = "https://.b2clogin.com/tfp//.com/"; - - final static String PUBLIC_CLIENT_ID = ""; - - final static String GRAPH_DEFAULT_SCOPE = "https://graph.windows.net/.default"; - final static String LAB_DEFAULT_B2C_SCOPE = ""; - - final static String USER_NAME = ""; - final static String USER_PASSWORD = ""; - - final static String CONFIDENTIAL_CLIENT_ID = ""; - final static String CONFIDENTIAL_CLIENT_SECRET = ""; -} diff --git a/src/samples/public-client/UsernamePasswordFlow.java b/src/samples/public-client/UsernamePasswordFlow.java index ce9f47da..aaa55a53 100644 --- a/src/samples/public-client/UsernamePasswordFlow.java +++ b/src/samples/public-client/UsernamePasswordFlow.java @@ -1,65 +1,72 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import com.microsoft.aad.msal4j.*; +import com.microsoft.aad.msal4j.IAccount; +import com.microsoft.aad.msal4j.IAuthenticationResult; +import com.microsoft.aad.msal4j.MsalException; +import com.microsoft.aad.msal4j.PublicClientApplication; +import com.microsoft.aad.msal4j.SilentParameters; +import com.microsoft.aad.msal4j.UserNamePasswordParameters; -import java.net.MalformedURLException; -import java.util.Collection; import java.util.Collections; -import java.util.concurrent.CompletableFuture; +import java.util.Set; public class UsernamePasswordFlow { + private final static String CLIENT_ID = ""; + private final static String AUTHORITY = "https://login.microsoftonline.com/organizations/"; + private final static Set SCOPE = Collections.singleton(""); + private final static String USER_NAME = ""; + private final static String USER_PASSWORD = ""; + public static void main(String args[]) throws Exception { - getAccessTokenFromUserCredentials(); - } - private static void getAccessTokenFromUserCredentials() throws Exception { - PublicClientApplication app = PublicClientApplication.builder(TestData.PUBLIC_CLIENT_ID) - .authority(TestData.AUTHORITY_ORGANIZATION) - .build(); + IAuthenticationResult result = acquireTokenUsernamePassword(); + System.out.println("Access token: " + result.accessToken()); + System.out.println("Id token: " + result.idToken()); + System.out.println("Account username: " + result.account().username()); + } - UserNamePasswordParameters parameters = UserNamePasswordParameters.builder( - Collections.singleton(TestData.GRAPH_DEFAULT_SCOPE), - TestData.USER_NAME, - TestData.USER_PASSWORD.toCharArray()) - .build(); + private static IAuthenticationResult acquireTokenUsernamePassword() throws Exception { - CompletableFuture future = app.acquireToken(parameters); + // Load token cache from file and initialize token cache aspect. The token cache will have + // dummy data, so the acquireTokenSilently call will fail. + TokenCacheAspect tokenCacheAspect = new TokenCacheAspect("sample_cache.json"); - future.handle((res, ex) -> { - if(ex != null) { - System.out.println("Oops! We have an exception - " + ex.getMessage()); - return "Unknown!"; - } + PublicClientApplication pca = PublicClientApplication.builder(CLIENT_ID) + .authority(AUTHORITY) + .setTokenCacheAccessAspect(tokenCacheAspect) + .build(); - Collection accounts = app.getAccounts().join(); + Set accountsInCache = pca.getAccounts().join(); + // Take first account in the cache. In a production application, you would filter + // accountsInCache to get the right account for the user authenticating. + IAccount account = accountsInCache.iterator().next(); - CompletableFuture future1; - try { - future1 = app.acquireTokenSilently - (SilentParameters.builder(Collections.singleton(TestData.GRAPH_DEFAULT_SCOPE), - accounts.iterator().next()) - .forceRefresh(true) - .build()); + IAuthenticationResult result; + try { + SilentParameters silentParameters = + SilentParameters + .builder(SCOPE, account) + .build(); + // try to acquire token silently. This call will fail since the token cache + // does not have any data for the user you are trying to acquire a token for + result = pca.acquireTokenSilently(silentParameters).join(); + } catch (Exception ex) { + if (ex.getCause() instanceof MsalException) { - } catch (MalformedURLException e) { - e.printStackTrace(); - throw new RuntimeException(); + UserNamePasswordParameters parameters = + UserNamePasswordParameters + .builder(SCOPE, USER_NAME, USER_PASSWORD.toCharArray()) + .build(); + // Try to acquire a token via username/password. If successful, you should see + // the token and account information printed out to console + result = pca.acquireToken(parameters).join(); + } else { + // Handle other exceptions accordingly + throw ex; } - - future1.join(); - - IAccount account = app.getAccounts().join().iterator().next(); - app.removeAccount(account).join(); - accounts = app.getAccounts().join(); - - System.out.println("Num of account - " + accounts.size()); - System.out.println("Returned ok - " + res); - System.out.println("Access Token - " + res.accessToken()); - System.out.println("ID Token - " + res.idToken()); - return res; - }).join(); - + } + return result; } } diff --git a/src/samples/spring-security-web-app/pom.xml b/src/samples/spring-security-web-app/pom.xml index 85a4c46e..b84ec7ce 100644 --- a/src/samples/spring-security-web-app/pom.xml +++ b/src/samples/spring-security-web-app/pom.xml @@ -5,7 +5,7 @@ org.springframework.boot spring-boot-starter-parent - 2.1.8.RELEASE + 2.2.2.RELEASE com.microsoft.azure @@ -51,6 +51,11 @@ 2.1.8.RELEASE + + org.springframework.session + spring-session-core + + diff --git a/src/samples/spring-security-web-app/src/main/java/com/microsoft/azure/springsecuritywebapp/AppConfiguration.java b/src/samples/spring-security-web-app/src/main/java/com/microsoft/azure/springsecuritywebapp/AppConfiguration.java index 1a85d1c1..abfa9592 100644 --- a/src/samples/spring-security-web-app/src/main/java/com/microsoft/azure/springsecuritywebapp/AppConfiguration.java +++ b/src/samples/spring-security-web-app/src/main/java/com/microsoft/azure/springsecuritywebapp/AppConfiguration.java @@ -3,21 +3,31 @@ package com.microsoft.azure.springsecuritywebapp; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; +import org.springframework.core.env.Environment; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import java.net.URLEncoder; + @Configuration @EnableOAuth2Sso @Order(value = 0) public class AppConfiguration extends WebSecurityConfigurerAdapter { + @Autowired + private Environment env; + @Override public void configure(HttpSecurity http) throws Exception { + String logoutUrl = env.getProperty("endSessionEndpoint") + "?post_logout_redirect_uri=" + + URLEncoder.encode(env.getProperty("homePage"), "UTF-8"); + http.antMatcher("/**") .authorizeRequests() .antMatchers("/", "/login**", "/error**") @@ -28,6 +38,6 @@ public void configure(HttpSecurity http) throws Exception { .logout() .deleteCookies() .invalidateHttpSession(true) - .logoutSuccessUrl("/"); + .logoutSuccessUrl(logoutUrl); } } diff --git a/src/samples/spring-security-web-app/src/main/resources/application.properties b/src/samples/spring-security-web-app/src/main/resources/application.properties index ddc2ac01..a5d837be 100644 --- a/src/samples/spring-security-web-app/src/main/resources/application.properties +++ b/src/samples/spring-security-web-app/src/main/resources/application.properties @@ -1,4 +1,6 @@ logging.level.org.springframework.*=DEBUG +server.address=localhost +server.port=8080 ssoServiceUrl=https://login.microsoftonline.com/common @@ -8,9 +10,20 @@ security.oauth2.client.scope=openid profile security.oauth2.client.authentication-scheme=header security.oauth2.client.client-authentication-scheme=form -security.oauth2.issuer=https://login.microsoftonline.com//v2.0 +security.oauth2.issuer=https://login.microsoftonline.com/53f49862-d94b-42dc-849b-8f132ea3674d/v2.0 security.oauth2.client.access-token-uri=${ssoServiceUrl}/oauth2/v2.0/token security.oauth2.client.user-authorization-uri=${ssoServiceUrl}/oauth2/v2.0/authorize -security.oauth2.resource.user-info-uri=https://graph.microsoft.com/oidc/userinfo \ No newline at end of file +security.oauth2.resource.user-info-uri=https://graph.microsoft.com/oidc/userinfo + +server.servlet.session.cookie.secure=true + +server.ssl.key-store=classpath:keystore.p12 +server.ssl.key-store-password= +server.ssl.key-store-type=pkcs12 +server.ssl.key-alias=tomcat +server.ssl.key-password= + +homePage=https://${server.address}:${server.port} +endSessionEndpoint=https://login.microsoftonline.com/common/oauth2/v2.0/logout \ No newline at end of file diff --git a/src/test/java/com/microsoft/aad/msal4j/AuthorizationRequestUrlParametersTest.java b/src/test/java/com/microsoft/aad/msal4j/AuthorizationRequestUrlParametersTest.java new file mode 100644 index 00000000..f5b464b8 --- /dev/null +++ b/src/test/java/com/microsoft/aad/msal4j/AuthorizationRequestUrlParametersTest.java @@ -0,0 +1,125 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.aad.msal4j; + +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.io.UnsupportedEncodingException; +import java.net.URL; +import java.net.URLDecoder; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +public class AuthorizationRequestUrlParametersTest { + + @Test + public void testBuilder_onlyRequiredParameters() throws UnsupportedEncodingException { + PublicClientApplication app = PublicClientApplication.builder("client_id").build(); + + String redirectUri = "http://localhost:8080"; + Set scope = Collections.singleton("scope"); + + AuthorizationRequestUrlParameters parameters = + AuthorizationRequestUrlParameters + .builder(redirectUri, scope) + .build(); + + Assert.assertEquals(parameters.responseMode(), ResponseMode.FORM_POST); + Assert.assertEquals(parameters.redirectUri(), redirectUri); + Assert.assertEquals(parameters.scopes().size(), 4); + + Assert.assertNull(parameters.loginHint()); + Assert.assertNull(parameters.codeChallenge()); + Assert.assertNull(parameters.codeChallengeMethod()); + Assert.assertNull(parameters.correlationId()); + Assert.assertNull(parameters.nonce()); + Assert.assertNull(parameters.prompt()); + Assert.assertNull(parameters.state()); + + URL authorizationUrl = app.getAuthorizationRequestUrl(parameters); + + Assert.assertEquals(authorizationUrl.getHost(), "login.microsoftonline.com"); + Assert.assertEquals(authorizationUrl.getPath(), "/common/oauth2/v2.0/authorize"); + + Map queryParameters = new HashMap<>(); + String query = authorizationUrl.getQuery(); + + String[] queryPairs = query.split("&"); + for(String pair: queryPairs){ + int idx = pair.indexOf("="); + queryParameters.put( + URLDecoder.decode(pair.substring(0, idx), "UTF-8"), + URLDecoder.decode(pair.substring(idx+1), "UTF-8")); + } + + Assert.assertEquals(queryParameters.get("scope"), "offline_access openid profile scope"); + Assert.assertEquals(queryParameters.get("response_type"), "code"); + Assert.assertEquals(queryParameters.get("redirect_uri"), "http://localhost:8080"); + Assert.assertEquals(queryParameters.get("client_id"), "client_id"); + Assert.assertEquals(queryParameters.get("response_mode"), "form_post"); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testBuilder_invalidRequiredParameters(){ + String redirectUri = ""; + Set scope = Collections.singleton("scope"); + + AuthorizationRequestUrlParameters parameters = + AuthorizationRequestUrlParameters + .builder(redirectUri, scope) + .build(); + } + + @Test + public void testBuilder_optionalParameters() throws UnsupportedEncodingException{ + PublicClientApplication app = PublicClientApplication.builder("client_id").build(); + + String redirectUri = "http://localhost:8080"; + Set scope = Collections.singleton("scope"); + + AuthorizationRequestUrlParameters parameters = + AuthorizationRequestUrlParameters + .builder(redirectUri, scope) + .responseMode(ResponseMode.QUERY) + .codeChallenge("challenge") + .codeChallengeMethod("method") + .state("app_state") + .nonce("app_nonce") + .correlationId("corr_id") + .loginHint("hint") + .domainHint("domain_hint") + .prompt(Prompt.SELECT_ACCOUNT) + .build(); + + URL authorizationUrl = app.getAuthorizationRequestUrl(parameters); + + Map queryParameters = new HashMap<>(); + String query = authorizationUrl.getQuery(); + + String[] queryPairs = query.split("&"); + for(String pair: queryPairs){ + int idx = pair.indexOf("="); + queryParameters.put( + URLDecoder.decode(pair.substring(0, idx), "UTF-8"), + URLDecoder.decode(pair.substring(idx+1), "UTF-8")); + } + + Assert.assertEquals(queryParameters.get("scope"), "offline_access openid profile scope"); + Assert.assertEquals(queryParameters.get("response_type"), "code"); + Assert.assertEquals(queryParameters.get("redirect_uri"), "http://localhost:8080"); + Assert.assertEquals(queryParameters.get("client_id"), "client_id"); + Assert.assertEquals(queryParameters.get("prompt"), "select_account"); + Assert.assertEquals(queryParameters.get("response_mode"), "query"); + Assert.assertEquals(queryParameters.get("code_challenge"), "challenge"); + Assert.assertEquals(queryParameters.get("code_challenge_method"), "method"); + Assert.assertEquals(queryParameters.get("state"), "app_state"); + Assert.assertEquals(queryParameters.get("nonce"), "app_nonce"); + Assert.assertEquals(queryParameters.get("correlation_id"), "corr_id"); + Assert.assertEquals(queryParameters.get("login_hint"), "hint"); + Assert.assertEquals(queryParameters.get("domain_hint"), "domain_hint"); + } +} diff --git a/src/test/java/com/microsoft/aad/msal4j/DefaultHttpClientTest.java b/src/test/java/com/microsoft/aad/msal4j/DefaultHttpClientTest.java new file mode 100644 index 00000000..33b4cd6c --- /dev/null +++ b/src/test/java/com/microsoft/aad/msal4j/DefaultHttpClientTest.java @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.aad.msal4j; + +import org.apache.commons.io.IOUtils; +import org.easymock.EasyMock; +import org.powermock.api.easymock.PowerMock; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.testng.PowerMockTestCase; +import org.testng.Assert; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import javax.net.ssl.HttpsURLConnection; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.powermock.api.easymock.PowerMock.expectPrivate; + +@PrepareForTest({DefaultHttpClient.class}) +public class DefaultHttpClientTest extends PowerMockTestCase { + + @Test + public void ValidNotOkHttpResponse() throws Exception { + String TEST_URL = "https://somehost.com"; + + HttpsURLConnection mockCon = PowerMock.createMock(HttpsURLConnection.class); + + EasyMock.expect(mockCon.getResponseCode()) + .andReturn(HttpURLConnection.HTTP_INTERNAL_ERROR).times(1); + + String errorResponse = "Error Message"; + InputStream inputStream = IOUtils.toInputStream(errorResponse, "UTF-8"); + EasyMock.expect(mockCon.getErrorStream()).andReturn(inputStream).times(1); + + Map> expectedHeaders = new HashMap<>(); + expectedHeaders.put("header1", Arrays.asList("val1", "val2")); + + EasyMock.expect(mockCon.getHeaderFields()).andReturn(expectedHeaders).times(1); + + DefaultHttpClient httpClient = + PowerMock.createPartialMock(DefaultHttpClient.class, "openConnection"); + + expectPrivate(httpClient, "openConnection", EasyMock.isA(URL.class)).andReturn(mockCon); + + PowerMock.replayAll(mockCon, httpClient); + + + HttpRequest httpRequest = new HttpRequest(HttpMethod.GET, TEST_URL); + IHttpResponse response = httpClient.send(httpRequest); + + + Assert.assertEquals(response.body(), errorResponse); + Assert.assertEquals(response.headers(), expectedHeaders); + } +} diff --git a/src/test/java/com/microsoft/aad/msal4j/DeviceCodeFlowTest.java b/src/test/java/com/microsoft/aad/msal4j/DeviceCodeFlowTest.java index 77f5855d..df35f318 100644 --- a/src/test/java/com/microsoft/aad/msal4j/DeviceCodeFlowTest.java +++ b/src/test/java/com/microsoft/aad/msal4j/DeviceCodeFlowTest.java @@ -132,7 +132,7 @@ public void deviceCodeFlowTest() throws Exception { URL url = capturedHttpRequest.getValue().url(); Assert.assertEquals(url.getAuthority(), AAD_PREFERRED_NETWORK_ENV_ALIAS); Assert.assertEquals(url.getPath(), - "/" + AAD_TENANT_NAME + AADAuthority.DEVICE_CODE_ENDPOINT); + "/" + AAD_TENANT_NAME + "/" + AADAuthority.DEVICE_CODE_ENDPOINT); Map expectedQueryParams = new HashMap<>(); expectedQueryParams.put("client_id", AAD_CLIENT_ID); diff --git a/src/test/java/com/microsoft/aad/msal4j/ServerTelemetryTests.java b/src/test/java/com/microsoft/aad/msal4j/ServerTelemetryTests.java new file mode 100644 index 00000000..a4e943d6 --- /dev/null +++ b/src/test/java/com/microsoft/aad/msal4j/ServerTelemetryTests.java @@ -0,0 +1,170 @@ +package com.microsoft.aad.msal4j; + +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class ServerTelemetryTests { + + private static final String SCHEMA_VERSION = "2"; + private final static String CURRENT_REQUEST_HEADER_NAME = "x-client-current-telemetry"; + private final static String LAST_REQUEST_HEADER_NAME = "x-client-last-telemetry"; + + private final static String PUBLIC_API_ID = String.valueOf(PublicApi.ACQUIRE_TOKEN_BY_AUTHORIZATION_CODE.getApiId()); + private final static String ERROR = "invalid_grant"; + + @Test + public void serverTelemetryHeaders_correctSchema(){ + + CurrentRequest currentRequest = new CurrentRequest(PublicApi.ACQUIRE_TOKEN_BY_AUTHORIZATION_CODE); + currentRequest.forceRefresh(false); + + ServerSideTelemetry serverSideTelemetry = new ServerSideTelemetry(); + serverSideTelemetry.setCurrentRequest(currentRequest); + + String correlationId = "936732c6-74b9-4783-aad9-fa205eae8763"; + serverSideTelemetry.addFailedRequestTelemetry(PUBLIC_API_ID, correlationId, ERROR); + + Map headers = serverSideTelemetry.getServerTelemetryHeaderMap(); + + //Current request tests + List currentRequestHeader = Arrays.asList(headers.get(CURRENT_REQUEST_HEADER_NAME).split("\\|")); + + // ["2", "831, false"] + Assert.assertEquals(currentRequestHeader.size(), 2); + Assert.assertEquals(currentRequestHeader.get(0), SCHEMA_VERSION); + + // ["831", "false"] + List secondSegment = Arrays.asList(currentRequestHeader.get(1).split(",")); + Assert.assertEquals(secondSegment.get(0), String.valueOf(PublicApi.ACQUIRE_TOKEN_BY_AUTHORIZATION_CODE.getApiId())); + Assert.assertEquals(secondSegment.get(1), "false"); + + + // Previous request test + List previousRequestHeader = Arrays.asList(headers.get(LAST_REQUEST_HEADER_NAME).split("\\|")); + + // ["2","0","831,936732c6-74b9-4783-aad9-fa205eae8763","invalid_grant"] + Assert.assertEquals(previousRequestHeader.size(), 4); + Assert.assertEquals(previousRequestHeader.get(0), SCHEMA_VERSION); + Assert.assertEquals(previousRequestHeader.get(1), "0"); + Assert.assertEquals(previousRequestHeader.get(3), ERROR); + + List thirdSegment = Arrays.asList(previousRequestHeader.get(2).split(",")); + + Assert.assertEquals(thirdSegment.get(0), PUBLIC_API_ID); + Assert.assertEquals(thirdSegment.get(1), correlationId); + } + + @Test + public void serverTelemetryHeaders_previewsRequestNull(){ + + ServerSideTelemetry serverSideTelemetry = new ServerSideTelemetry(); + for(int i = 0; i < 3; i++){ + serverSideTelemetry.incrementSilentSuccessfulCount(); + } + + Map headers = serverSideTelemetry.getServerTelemetryHeaderMap(); + + Assert.assertEquals(headers.get(LAST_REQUEST_HEADER_NAME), "2|3|||"); + } + + @Test + public void serverTelemetryHeader_testMaximumHeaderSize(){ + + ServerSideTelemetry serverSideTelemetry = new ServerSideTelemetry(); + + for(int i = 0; i <100; i++){ + String correlationId = UUID.randomUUID().toString(); + serverSideTelemetry.addFailedRequestTelemetry(PUBLIC_API_ID, correlationId, ERROR); + } + + Map headers = serverSideTelemetry.getServerTelemetryHeaderMap(); + + String lastRequest = headers.get(LAST_REQUEST_HEADER_NAME); + + // http headers are encoded in ISO_8859_1 + byte[] lastRequestBytes = lastRequest.getBytes(StandardCharsets.ISO_8859_1); + + Assert.assertTrue(lastRequestBytes.length < 4000); + } + + @Test + public void serverTelemetryHeaders_multipleThreadsWrite(){ + + ServerSideTelemetry serverSideTelemetry = new ServerSideTelemetry(); + ExecutorService executor = Executors.newFixedThreadPool(10); + + try{ + for (int i=0; i < 10; i++){ + executor.execute(new FailedRequestRunnable(serverSideTelemetry)); + executor.execute(new SilentSuccessfulRequestRunnable(serverSideTelemetry)); + } + } catch (Exception ex){ + ex.printStackTrace(); + } + executor.shutdown(); + + try{ + Thread.sleep( 1000); + } catch(InterruptedException ex){ + ex.printStackTrace(); + } + + Map headers = serverSideTelemetry.getServerTelemetryHeaderMap(); + + List previousRequestHeader = Arrays.asList(headers.get(LAST_REQUEST_HEADER_NAME).split("\\|")); + + Assert.assertEquals(previousRequestHeader.get(1), "10"); + + List thirdSegment = Arrays.asList(previousRequestHeader.get(2).split(",")); + Assert.assertEquals(thirdSegment.size(), 20); + + List fourthSegment = Arrays.asList(previousRequestHeader.get(3).split(",")); + Assert.assertEquals(fourthSegment.size(), 10); + } + + class FailedRequestRunnable implements Runnable { + + ServerSideTelemetry telemetry; + + FailedRequestRunnable(ServerSideTelemetry telemetry){ + this.telemetry = telemetry; + } + + @Override + public void run(){ + + Random rand = new Random(); + int n = rand.nextInt(250); + try { + Thread.sleep(n); + } catch (InterruptedException ex){ + ex.printStackTrace(); + } + String correlationId = UUID.randomUUID().toString(); + telemetry.addFailedRequestTelemetry(PUBLIC_API_ID, correlationId, ERROR); + } + } + + class SilentSuccessfulRequestRunnable implements Runnable { + + ServerSideTelemetry telemetry; + + SilentSuccessfulRequestRunnable(ServerSideTelemetry telemetry){ + this.telemetry = telemetry; + } + + @Override + public void run(){ + telemetry.incrementSilentSuccessfulCount(); + } + } +} diff --git a/src/test/java/com/microsoft/aad/msal4j/TokenRequestExecutorTest.java b/src/test/java/com/microsoft/aad/msal4j/TokenRequestExecutorTest.java index 56cf1f4a..d6ece15c 100644 --- a/src/test/java/com/microsoft/aad/msal4j/TokenRequestExecutorTest.java +++ b/src/test/java/com/microsoft/aad/msal4j/TokenRequestExecutorTest.java @@ -298,7 +298,7 @@ public void testExecuteOAuth_Failure() throws SerializeException, .andReturn(msalOAuthHttpRequest).times(1); EasyMock.expect(msalOAuthHttpRequest.send()).andReturn(httpResponse) .times(1); - EasyMock.expect(httpResponse.getStatusCode()).andReturn(402).times(2); + EasyMock.expect(httpResponse.getStatusCode()).andReturn(402).times(3); EasyMock.expect(httpResponse.getStatusMessage()).andReturn("403 Forbidden"); EasyMock.expect(httpResponse.getHeaderMap()).andReturn(new HashMap<>()); EasyMock.expect(httpResponse.getContent()).andReturn(TestConfiguration.HTTP_ERROR_RESPONSE);