Skip to content

Enable device code flow for ADFS 2019 #241

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jun 2, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public void setUp(){
}

@Test(dataProvider = "environments", dataProviderClass = EnvironmentsProvider.class)
public void DeviceCodeFlowTest(String environment) throws Exception {
public void DeviceCodeFlowADTest(String environment) throws Exception {
cfg = new Config(environment);

User user = labUserProvider.getDefaultUser(cfg.azureEnvironment);
Expand All @@ -59,22 +59,56 @@ public void DeviceCodeFlowTest(String environment) throws Exception {
Assert.assertTrue(!Strings.isNullOrEmpty(result.accessToken()));
}

@Test(dataProvider = "environments", dataProviderClass = EnvironmentsProvider.class)
public void DeviceCodeFlowADFSv2019Test(String environment) throws Exception {

User user = labUserProvider.getOnPremAdfsUser(FederationProvider.ADFS_2019);

PublicClientApplication pca = PublicClientApplication.builder(
TestConstants.ADFS_APP_ID).
authority(TestConstants.ADFS_AUTHORITY).validateAuthority(false).
build();

Consumer<DeviceCode> deviceCodeConsumer = (DeviceCode deviceCode) -> {
runAutomatedDeviceCodeFlow(deviceCode, user);
};

IAuthenticationResult result = pca.acquireToken(DeviceCodeFlowParameters
.builder(Collections.singleton(TestConstants.ADFS_SCOPE),
deviceCodeConsumer)
.build())
.get();

Assert.assertNotNull(result);
Assert.assertFalse(Strings.isNullOrEmpty(result.accessToken()));
}

private void runAutomatedDeviceCodeFlow(DeviceCode deviceCode, User user){
boolean isRunningLocally = true;//!Strings.isNullOrEmpty(
//System.getenv(TestConstants.LOCAL_FLAG_ENV_VAR));

boolean isADFS2019 = user.getFederationProvider().equals("adfsv2019");

LOG.info("Device code running locally: " + isRunningLocally);
try{
String deviceCodeFormId;
String continueButtonId;
if(isRunningLocally){
deviceCodeFormId = "otc";
continueButtonId = "idSIButton9";
if (isADFS2019) {
deviceCodeFormId = "userCodeInput";
continueButtonId = "confirmationButton";
} else {
deviceCodeFormId = "otc";
continueButtonId = "idSIButton9";
}
} else {
deviceCodeFormId = "code";
continueButtonId = "continueBtn";
}
LOG.info("Loggin in ... Entering device code");
if (isADFS2019) {
seleniumDriver.manage().deleteAllCookies();
}
seleniumDriver.navigate().to(deviceCode.verificationUri());
seleniumDriver.findElement(new By.ById(deviceCodeFormId)).sendKeys(deviceCode.userCode());

Expand All @@ -84,7 +118,11 @@ private void runAutomatedDeviceCodeFlow(DeviceCode deviceCode, User user){
new By.ById(continueButtonId));
continueBtn.click();

SeleniumExtensions.performADLogin(seleniumDriver, user);
if (isADFS2019) {
SeleniumExtensions.performADFS2019Login(seleniumDriver, user);
} else {
SeleniumExtensions.performADLogin(seleniumDriver, user);
}
} catch(Exception e){
if(!isRunningLocally){
SeleniumExtensions.takeScreenShot(seleniumDriver);
Expand Down
2 changes: 0 additions & 2 deletions src/main/java/com/microsoft/aad/msal4j/AADAuthority.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@ class AADAuthority extends Authority {
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;

AADAuthority(final URL authorityUrl) {
super(authorityUrl, AuthorityType.AAD);
setAuthorityProperties();
Expand Down
5 changes: 5 additions & 0 deletions src/main/java/com/microsoft/aad/msal4j/ADFSAuthority.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,19 @@ class ADFSAuthority extends Authority{

final static String AUTHORIZATION_ENDPOINT = "oauth2/authorize";
final static String TOKEN_ENDPOINT = "oauth2/token";
final static String DEVICE_CODE_ENDPOINT = "oauth2/devicecode";

private final static String ADFS_AUTHORITY_FORMAT = "https://%s/%s/";
private final static String DEVICE_CODE_ENDPOINT_FORMAT = ADFS_AUTHORITY_FORMAT + DEVICE_CODE_ENDPOINT;

ADFSAuthority(final URL authorityUrl) {
super(authorityUrl, AuthorityType.ADFS);
this.authority = String.format(ADFS_AUTHORITY_FORMAT, host, tenant);
this.authorizationEndpoint = authority + AUTHORIZATION_ENDPOINT;
this.tokenEndpoint = authority + TOKEN_ENDPOINT;
this.selfSignedJwtAudience = this.tokenEndpoint;
this.deviceCodeEndpoint = String.format(DEVICE_CODE_ENDPOINT_FORMAT, host, tenant);
}


Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: extra line

}
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@ AuthenticationResult execute() throws Exception {
Authority requestAuthority = clientApplication.authenticationAuthority;
requestAuthority = getAuthorityWithPrefNetworkHost(requestAuthority.authority());

DeviceCode deviceCode = getDeviceCode((AADAuthority) requestAuthority);
DeviceCode deviceCode = getDeviceCode(requestAuthority);

return acquireTokenWithDeviceCode(deviceCode, requestAuthority);
}

private DeviceCode getDeviceCode(AADAuthority requestAuthority) throws Exception{
private DeviceCode getDeviceCode(Authority requestAuthority) throws Exception{

DeviceCode deviceCode = deviceCodeFlowRequest.acquireDeviceCode(
requestAuthority.deviceCodeEndpoint(),
Expand Down
6 changes: 6 additions & 0 deletions src/main/java/com/microsoft/aad/msal4j/Authority.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ abstract class Authority {

String authorizationEndpoint;
String tokenEndpoint;

String deviceCodeEndpoint;

URL tokenEndpointUrl() throws MalformedURLException {
return new URL(tokenEndpoint);
Expand Down Expand Up @@ -145,4 +147,8 @@ private static boolean isAdfsAuthority(final String firstPath) {
private static boolean isB2CAuthority(final String firstPath) {
return firstPath.compareToIgnoreCase(B2C_PATH_SEGMENT) == 0;
}

String deviceCodeEndpoint() {
return deviceCodeEndpoint;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,13 @@ class DeviceCodeFlowRequest extends MsalRequest {
DeviceCode acquireDeviceCode(String url,
String clientId,
Map<String, String> clientDataHeaders,
ServiceBundle serviceBundle) throws Exception {
ServiceBundle serviceBundle) {

String urlWithQueryParams = createQueryParamsAndAppendToURL(url, clientId);
Map<String, String> headers = appendToHeaders(clientDataHeaders);
String bodyParams = createQueryParams(clientId);

HttpRequest httpRequest = new HttpRequest(HttpMethod.POST, url, headers, bodyParams);

HttpRequest httpRequest = new HttpRequest(HttpMethod.GET, urlWithQueryParams, headers);
final IHttpResponse response = HttpHelper.executeHttpRequest(
httpRequest,
this.requestContext(),
Expand All @@ -57,7 +58,7 @@ void createAuthenticationGrant(DeviceCode deviceCode) {
msalAuthorizationGrant = new DeviceCodeAuthorizationGrant(deviceCode, deviceCode.scopes());
}

private String createQueryParamsAndAppendToURL(String url, String clientId) {
private String createQueryParams(String clientId) {
Map<String, List<String>> queryParameters = new HashMap<>();
queryParameters.put("client_id", Collections.singletonList(clientId));

Expand All @@ -66,8 +67,7 @@ private String createQueryParamsAndAppendToURL(String url, String clientId) {

queryParameters.put("scope", Collections.singletonList(scopesParam));

url = url + "?" + URLUtils.serializeParameters(queryParameters);
return url;
return URLUtils.serializeParameters(queryParameters);
}

private Map<String, String> appendToHeaders(Map<String, String> clientDataHeaders) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,10 @@ public CompletableFuture<IAuthenticationResult> acquireToken(IntegratedWindowsAu
@Override
public CompletableFuture<IAuthenticationResult> acquireToken(DeviceCodeFlowParameters parameters) {

if (!AuthorityType.AAD.equals(authenticationAuthority.authorityType())) {
if (!(AuthorityType.AAD.equals(authenticationAuthority.authorityType()) ||
AuthorityType.ADFS.equals(authenticationAuthority.authorityType()))) {
throw new IllegalArgumentException(
"Invalid authority type. Device Flow is only supported by AAD authority");
"Invalid authority type. Device Flow is only supported by AAD and ADFS authorities");
}

validateNotNull("parameters", parameters);
Expand Down
30 changes: 6 additions & 24 deletions src/test/java/com/microsoft/aad/msal4j/DeviceCodeFlowTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -134,16 +134,14 @@ public void deviceCodeFlowTest() throws Exception {
Assert.assertEquals(url.getPath(),
"/" + AAD_TENANT_NAME + "/" + AADAuthority.DEVICE_CODE_ENDPOINT);

Map<String, String> expectedQueryParams = new HashMap<>();
expectedQueryParams.put("client_id", AAD_CLIENT_ID);
expectedQueryParams.put("scope", URLEncoder.encode(AbstractMsalAuthorizationGrant.COMMON_SCOPES_PARAM +
AbstractMsalAuthorizationGrant.SCOPES_DELIMITER + AAD_RESOURCE_ID, "UTF-8" ));
String expectedScope = URLEncoder.encode(AbstractMsalAuthorizationGrant.COMMON_SCOPES_PARAM +
AbstractMsalAuthorizationGrant.SCOPES_DELIMITER + AAD_RESOURCE_ID, "UTF-8");
String expectedBody = String.format("scope=%s&client_id=%s", expectedScope, AAD_CLIENT_ID);

Assert.assertEquals(getQueryMap(url.getQuery()), expectedQueryParams);
String body = capturedHttpRequest.getValue().body();
Assert.assertEquals(body, expectedBody);

// make sure same correlation id is used for acquireDeviceCode and acquireTokenByDeviceCode calls

Map<String, String > headers = capturedMsalRequest.getValue().headers().getReadonlyHeaderMap();
Assert.assertEquals(capturedMsalRequest.getValue().headers().getReadonlyHeaderMap().
get(HttpHeaders.CORRELATION_ID_HEADER_NAME), deviceCodeCorrelationId.get());
Assert.assertNotNull(authResult);
Expand All @@ -152,22 +150,7 @@ public void deviceCodeFlowTest() throws Exception {
}

@Test(expectedExceptions = IllegalArgumentException.class,
expectedExceptionsMessageRegExp = "Invalid authority type. Device Flow is only supported by AAD authority")
public void executeAcquireDeviceCode_AdfsAuthorityUsed_IllegalArgumentExceptionThrown()
throws Exception {

app = PublicClientApplication.builder("client_id")
.authority(ADFS_TENANT_ENDPOINT)
.validateAuthority(false).build();

app.acquireToken
(DeviceCodeFlowParameters
.builder(Collections.singleton(AAD_RESOURCE_ID), (DeviceCode deviceCode) -> {})
.build());
}

@Test(expectedExceptions = IllegalArgumentException.class,
expectedExceptionsMessageRegExp = "Invalid authority type. Device Flow is only supported by AAD authority")
expectedExceptionsMessageRegExp = "Invalid authority type. Device Flow is only supported by AAD and ADFS authorities")
public void executeAcquireDeviceCode_B2CAuthorityUsed_IllegalArgumentExceptionThrown()
throws Exception {

Expand All @@ -181,7 +164,6 @@ public void executeAcquireDeviceCode_B2CAuthorityUsed_IllegalArgumentExceptionTh
.build());
}


@Test
public void executeAcquireDeviceCode_AuthenticaionPendingErrorReturned_AuthenticationExceptionThrown()
throws Exception {
Expand Down