Skip to content

Commit df4e339

Browse files
authored
Merge pull request #241 from AzureAD/avdunn/adfs-devicecode
Enable device code flow for ADFS 2019
2 parents 2fd7db4 + 57238cb commit df4e339

File tree

8 files changed

+70
-40
lines changed

8 files changed

+70
-40
lines changed

src/integrationtest/java/com.microsoft.aad.msal4j/DeviceCodeIT.java

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public void setUp(){
3535
}
3636

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

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

62+
@Test(dataProvider = "environments", dataProviderClass = EnvironmentsProvider.class)
63+
public void DeviceCodeFlowADFSv2019Test(String environment) throws Exception {
64+
65+
User user = labUserProvider.getOnPremAdfsUser(FederationProvider.ADFS_2019);
66+
67+
PublicClientApplication pca = PublicClientApplication.builder(
68+
TestConstants.ADFS_APP_ID).
69+
authority(TestConstants.ADFS_AUTHORITY).validateAuthority(false).
70+
build();
71+
72+
Consumer<DeviceCode> deviceCodeConsumer = (DeviceCode deviceCode) -> {
73+
runAutomatedDeviceCodeFlow(deviceCode, user);
74+
};
75+
76+
IAuthenticationResult result = pca.acquireToken(DeviceCodeFlowParameters
77+
.builder(Collections.singleton(TestConstants.ADFS_SCOPE),
78+
deviceCodeConsumer)
79+
.build())
80+
.get();
81+
82+
Assert.assertNotNull(result);
83+
Assert.assertFalse(Strings.isNullOrEmpty(result.accessToken()));
84+
}
85+
6286
private void runAutomatedDeviceCodeFlow(DeviceCode deviceCode, User user){
6387
boolean isRunningLocally = true;//!Strings.isNullOrEmpty(
6488
//System.getenv(TestConstants.LOCAL_FLAG_ENV_VAR));
6589

90+
boolean isADFS2019 = user.getFederationProvider().equals("adfsv2019");
91+
6692
LOG.info("Device code running locally: " + isRunningLocally);
6793
try{
6894
String deviceCodeFormId;
6995
String continueButtonId;
7096
if(isRunningLocally){
71-
deviceCodeFormId = "otc";
72-
continueButtonId = "idSIButton9";
97+
if (isADFS2019) {
98+
deviceCodeFormId = "userCodeInput";
99+
continueButtonId = "confirmationButton";
100+
} else {
101+
deviceCodeFormId = "otc";
102+
continueButtonId = "idSIButton9";
103+
}
73104
} else {
74105
deviceCodeFormId = "code";
75106
continueButtonId = "continueBtn";
76107
}
77108
LOG.info("Loggin in ... Entering device code");
109+
if (isADFS2019) {
110+
seleniumDriver.manage().deleteAllCookies();
111+
}
78112
seleniumDriver.navigate().to(deviceCode.verificationUri());
79113
seleniumDriver.findElement(new By.ById(deviceCodeFormId)).sendKeys(deviceCode.userCode());
80114

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

87-
SeleniumExtensions.performADLogin(seleniumDriver, user);
121+
if (isADFS2019) {
122+
SeleniumExtensions.performADFS2019Login(seleniumDriver, user);
123+
} else {
124+
SeleniumExtensions.performADLogin(seleniumDriver, user);
125+
}
88126
} catch(Exception e){
89127
if(!isRunningLocally){
90128
SeleniumExtensions.takeScreenShot(seleniumDriver);

src/main/java/com/microsoft/aad/msal4j/AADAuthority.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@ class AADAuthority extends Authority {
2323
private final static String AAD_TOKEN_ENDPOINT_FORMAT = AAD_AUTHORITY_FORMAT + TOKEN_ENDPOINT;
2424
private final static String DEVICE_CODE_ENDPOINT_FORMAT = AAD_AUTHORITY_FORMAT + DEVICE_CODE_ENDPOINT;
2525

26-
String deviceCodeEndpoint;
27-
2826
AADAuthority(final URL authorityUrl) {
2927
super(authorityUrl, AuthorityType.AAD);
3028
setAuthorityProperties();

src/main/java/com/microsoft/aad/msal4j/ADFSAuthority.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,19 @@ class ADFSAuthority extends Authority{
99

1010
final static String AUTHORIZATION_ENDPOINT = "oauth2/authorize";
1111
final static String TOKEN_ENDPOINT = "oauth2/token";
12+
final static String DEVICE_CODE_ENDPOINT = "oauth2/devicecode";
1213

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

1517
ADFSAuthority(final URL authorityUrl) {
1618
super(authorityUrl, AuthorityType.ADFS);
1719
this.authority = String.format(ADFS_AUTHORITY_FORMAT, host, tenant);
1820
this.authorizationEndpoint = authority + AUTHORIZATION_ENDPOINT;
1921
this.tokenEndpoint = authority + TOKEN_ENDPOINT;
2022
this.selfSignedJwtAudience = this.tokenEndpoint;
23+
this.deviceCodeEndpoint = String.format(DEVICE_CODE_ENDPOINT_FORMAT, host, tenant);
2124
}
25+
26+
2227
}

src/main/java/com/microsoft/aad/msal4j/AcquireTokenByDeviceCodeFlowSupplier.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,12 @@ AuthenticationResult execute() throws Exception {
2222
Authority requestAuthority = clientApplication.authenticationAuthority;
2323
requestAuthority = getAuthorityWithPrefNetworkHost(requestAuthority.authority());
2424

25-
DeviceCode deviceCode = getDeviceCode((AADAuthority) requestAuthority);
25+
DeviceCode deviceCode = getDeviceCode(requestAuthority);
2626

2727
return acquireTokenWithDeviceCode(deviceCode, requestAuthority);
2828
}
2929

30-
private DeviceCode getDeviceCode(AADAuthority requestAuthority) throws Exception{
30+
private DeviceCode getDeviceCode(Authority requestAuthority) throws Exception{
3131

3232
DeviceCode deviceCode = deviceCodeFlowRequest.acquireDeviceCode(
3333
requestAuthority.deviceCodeEndpoint(),

src/main/java/com/microsoft/aad/msal4j/Authority.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ abstract class Authority {
3535

3636
String authorizationEndpoint;
3737
String tokenEndpoint;
38+
39+
String deviceCodeEndpoint;
3840

3941
URL tokenEndpointUrl() throws MalformedURLException {
4042
return new URL(tokenEndpoint);
@@ -145,4 +147,8 @@ private static boolean isAdfsAuthority(final String firstPath) {
145147
private static boolean isB2CAuthority(final String firstPath) {
146148
return firstPath.compareToIgnoreCase(B2C_PATH_SEGMENT) == 0;
147149
}
150+
151+
String deviceCodeEndpoint() {
152+
return deviceCodeEndpoint;
153+
}
148154
}

src/main/java/com/microsoft/aad/msal4j/DeviceCodeFlowRequest.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,13 @@ class DeviceCodeFlowRequest extends MsalRequest {
3939
DeviceCode acquireDeviceCode(String url,
4040
String clientId,
4141
Map<String, String> clientDataHeaders,
42-
ServiceBundle serviceBundle) throws Exception {
42+
ServiceBundle serviceBundle) {
4343

44-
String urlWithQueryParams = createQueryParamsAndAppendToURL(url, clientId);
4544
Map<String, String> headers = appendToHeaders(clientDataHeaders);
45+
String bodyParams = createQueryParams(clientId);
46+
47+
HttpRequest httpRequest = new HttpRequest(HttpMethod.POST, url, headers, bodyParams);
4648

47-
HttpRequest httpRequest = new HttpRequest(HttpMethod.GET, urlWithQueryParams, headers);
4849
final IHttpResponse response = HttpHelper.executeHttpRequest(
4950
httpRequest,
5051
this.requestContext(),
@@ -57,7 +58,7 @@ void createAuthenticationGrant(DeviceCode deviceCode) {
5758
msalAuthorizationGrant = new DeviceCodeAuthorizationGrant(deviceCode, deviceCode.scopes());
5859
}
5960

60-
private String createQueryParamsAndAppendToURL(String url, String clientId) {
61+
private String createQueryParams(String clientId) {
6162
Map<String, List<String>> queryParameters = new HashMap<>();
6263
queryParameters.put("client_id", Collections.singletonList(clientId));
6364

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

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

69-
url = url + "?" + URLUtils.serializeParameters(queryParameters);
70-
return url;
70+
return URLUtils.serializeParameters(queryParameters);
7171
}
7272

7373
private Map<String, String> appendToHeaders(Map<String, String> clientDataHeaders) {

src/main/java/com/microsoft/aad/msal4j/PublicClientApplication.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,10 @@ public CompletableFuture<IAuthenticationResult> acquireToken(IntegratedWindowsAu
5555
@Override
5656
public CompletableFuture<IAuthenticationResult> acquireToken(DeviceCodeFlowParameters parameters) {
5757

58-
if (!AuthorityType.AAD.equals(authenticationAuthority.authorityType())) {
58+
if (!(AuthorityType.AAD.equals(authenticationAuthority.authorityType()) ||
59+
AuthorityType.ADFS.equals(authenticationAuthority.authorityType()))) {
5960
throw new IllegalArgumentException(
60-
"Invalid authority type. Device Flow is only supported by AAD authority");
61+
"Invalid authority type. Device Flow is only supported by AAD and ADFS authorities");
6162
}
6263

6364
validateNotNull("parameters", parameters);

src/test/java/com/microsoft/aad/msal4j/DeviceCodeFlowTest.java

Lines changed: 6 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -134,16 +134,14 @@ public void deviceCodeFlowTest() throws Exception {
134134
Assert.assertEquals(url.getPath(),
135135
"/" + AAD_TENANT_NAME + "/" + AADAuthority.DEVICE_CODE_ENDPOINT);
136136

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

142-
Assert.assertEquals(getQueryMap(url.getQuery()), expectedQueryParams);
141+
String body = capturedHttpRequest.getValue().body();
142+
Assert.assertEquals(body, expectedBody);
143143

144144
// make sure same correlation id is used for acquireDeviceCode and acquireTokenByDeviceCode calls
145-
146-
Map<String, String > headers = capturedMsalRequest.getValue().headers().getReadonlyHeaderMap();
147145
Assert.assertEquals(capturedMsalRequest.getValue().headers().getReadonlyHeaderMap().
148146
get(HttpHeaders.CORRELATION_ID_HEADER_NAME), deviceCodeCorrelationId.get());
149147
Assert.assertNotNull(authResult);
@@ -152,22 +150,7 @@ public void deviceCodeFlowTest() throws Exception {
152150
}
153151

154152
@Test(expectedExceptions = IllegalArgumentException.class,
155-
expectedExceptionsMessageRegExp = "Invalid authority type. Device Flow is only supported by AAD authority")
156-
public void executeAcquireDeviceCode_AdfsAuthorityUsed_IllegalArgumentExceptionThrown()
157-
throws Exception {
158-
159-
app = PublicClientApplication.builder("client_id")
160-
.authority(ADFS_TENANT_ENDPOINT)
161-
.validateAuthority(false).build();
162-
163-
app.acquireToken
164-
(DeviceCodeFlowParameters
165-
.builder(Collections.singleton(AAD_RESOURCE_ID), (DeviceCode deviceCode) -> {})
166-
.build());
167-
}
168-
169-
@Test(expectedExceptions = IllegalArgumentException.class,
170-
expectedExceptionsMessageRegExp = "Invalid authority type. Device Flow is only supported by AAD authority")
153+
expectedExceptionsMessageRegExp = "Invalid authority type. Device Flow is only supported by AAD and ADFS authorities")
171154
public void executeAcquireDeviceCode_B2CAuthorityUsed_IllegalArgumentExceptionThrown()
172155
throws Exception {
173156

@@ -181,7 +164,6 @@ public void executeAcquireDeviceCode_B2CAuthorityUsed_IllegalArgumentExceptionTh
181164
.build());
182165
}
183166

184-
185167
@Test
186168
public void executeAcquireDeviceCode_AuthenticaionPendingErrorReturned_AuthenticationExceptionThrown()
187169
throws Exception {

0 commit comments

Comments
 (0)