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);