Skip to content

Commit 06fe229

Browse files
committed
Enable device code flow for ADFS 2019
1 parent 2fd7db4 commit 06fe229

File tree

7 files changed

+80
-51
lines changed

7 files changed

+80
-51
lines changed

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

Lines changed: 43 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,57 @@ 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+
cfg = new Config(environment);
65+
66+
User user = labUserProvider.getOnPremAdfsUser(FederationProvider.ADFS_2019);
67+
68+
PublicClientApplication pca = PublicClientApplication.builder(
69+
TestConstants.ADFS_APP_ID).
70+
authority(TestConstants.ADFS_AUTHORITY).validateAuthority(false).
71+
build();
72+
73+
Consumer<DeviceCode> deviceCodeConsumer = (DeviceCode deviceCode) -> {
74+
runAutomatedDeviceCodeFlow(deviceCode, user);
75+
};
76+
77+
IAuthenticationResult result = pca.acquireToken(DeviceCodeFlowParameters
78+
.builder(Collections.singleton(TestConstants.ADFS_SCOPE),
79+
deviceCodeConsumer)
80+
.build())
81+
.get();
82+
83+
Assert.assertNotNull(result);
84+
Assert.assertFalse(Strings.isNullOrEmpty(result.accessToken()));
85+
}
86+
6287
private void runAutomatedDeviceCodeFlow(DeviceCode deviceCode, User user){
6388
boolean isRunningLocally = true;//!Strings.isNullOrEmpty(
6489
//System.getenv(TestConstants.LOCAL_FLAG_ENV_VAR));
6590

91+
boolean isADFS2019 = user.getFederationProvider().equals("adfsv2019");
92+
6693
LOG.info("Device code running locally: " + isRunningLocally);
6794
try{
6895
String deviceCodeFormId;
6996
String continueButtonId;
7097
if(isRunningLocally){
71-
deviceCodeFormId = "otc";
72-
continueButtonId = "idSIButton9";
98+
if (isADFS2019) {
99+
deviceCodeFormId = "userCodeInput";
100+
continueButtonId = "confirmationButton";
101+
} else {
102+
deviceCodeFormId = "otc";
103+
continueButtonId = "idSIButton9";
104+
}
73105
} else {
74106
deviceCodeFormId = "code";
75107
continueButtonId = "continueBtn";
76108
}
77109
LOG.info("Loggin in ... Entering device code");
110+
if (isADFS2019) {
111+
seleniumDriver.manage().deleteAllCookies();
112+
}
78113
seleniumDriver.navigate().to(deviceCode.verificationUri());
79114
seleniumDriver.findElement(new By.ById(deviceCodeFormId)).sendKeys(deviceCode.userCode());
80115

@@ -84,7 +119,11 @@ private void runAutomatedDeviceCodeFlow(DeviceCode deviceCode, User user){
84119
new By.ById(continueButtonId));
85120
continueBtn.click();
86121

87-
SeleniumExtensions.performADLogin(seleniumDriver, user);
122+
if (isADFS2019) {
123+
SeleniumExtensions.performADFS2019Login(seleniumDriver, user);
124+
} else {
125+
SeleniumExtensions.performADLogin(seleniumDriver, user);
126+
}
88127
} catch(Exception e){
89128
if(!isRunningLocally){
90129
SeleniumExtensions.takeScreenShot(seleniumDriver);

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,23 @@ 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;
16+
17+
String deviceCodeEndpoint;
1418

1519
ADFSAuthority(final URL authorityUrl) {
1620
super(authorityUrl, AuthorityType.ADFS);
1721
this.authority = String.format(ADFS_AUTHORITY_FORMAT, host, tenant);
1822
this.authorizationEndpoint = authority + AUTHORIZATION_ENDPOINT;
1923
this.tokenEndpoint = authority + TOKEN_ENDPOINT;
2024
this.selfSignedJwtAudience = this.tokenEndpoint;
25+
this.deviceCodeEndpoint = String.format(DEVICE_CODE_ENDPOINT_FORMAT, host, tenant);
26+
}
27+
28+
String deviceCodeEndpoint() {
29+
return deviceCodeEndpoint;
2130
}
2231
}

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: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,17 @@ abstract class Authority {
3636
String authorizationEndpoint;
3737
String tokenEndpoint;
3838

39+
final static String DEVICE_CODE_ENDPOINT = "oauth2/devicecode";
40+
41+
private final static String ADFS_AUTHORITY_FORMAT = "https://%s/%s/";
42+
private final static String DEVICE_CODE_ENDPOINT_FORMAT = ADFS_AUTHORITY_FORMAT + DEVICE_CODE_ENDPOINT;
43+
44+
String deviceCodeEndpoint;
45+
46+
String deviceCodeEndpoint() {
47+
return deviceCodeEndpoint;
48+
}
49+
3950
URL tokenEndpointUrl() throws MalformedURLException {
4051
return new URL(tokenEndpoint);
4152
}
@@ -49,6 +60,7 @@ URL tokenEndpointUrl() throws MalformedURLException {
4960
private void setCommonAuthorityProperties() {
5061
this.tenant = getTenant(canonicalAuthorityUrl, authorityType);
5162
this.host = canonicalAuthorityUrl.getAuthority().toLowerCase();
63+
this.deviceCodeEndpoint = String.format(DEVICE_CODE_ENDPOINT_FORMAT, host, tenant);
5264
}
5365

5466
static Authority createAuthority(URL authorityUrl) {

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,17 @@ DeviceCode acquireDeviceCode(String url,
4141
Map<String, String> clientDataHeaders,
4242
ServiceBundle serviceBundle) throws Exception {
4343

44-
String urlWithQueryParams = createQueryParamsAndAppendToURL(url, clientId);
4544
Map<String, String> headers = appendToHeaders(clientDataHeaders);
4645

47-
HttpRequest httpRequest = new HttpRequest(HttpMethod.GET, urlWithQueryParams, headers);
46+
String scopesParam = AbstractMsalAuthorizationGrant.COMMON_SCOPES_PARAM +
47+
AbstractMsalAuthorizationGrant.SCOPES_DELIMITER + scopesStr;
48+
49+
Map<String, List<String>> queryParameters = new HashMap<>();
50+
queryParameters.put("client_id", Collections.singletonList(clientId));
51+
queryParameters.put("scope", Collections.singletonList(scopesParam));
52+
53+
HttpRequest httpRequest = new HttpRequest(HttpMethod.POST, url, headers, URLUtils.serializeParameters(queryParameters));
54+
4855
final IHttpResponse response = HttpHelper.executeHttpRequest(
4956
httpRequest,
5057
this.requestContext(),

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

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

58-
if (!AuthorityType.AAD.equals(authenticationAuthority.authorityType())) {
59-
throw new IllegalArgumentException(
60-
"Invalid authority type. Device Flow is only supported by AAD authority");
61-
}
62-
6358
validateNotNull("parameters", parameters);
6459

6560
AtomicReference<CompletableFuture<IAuthenticationResult>> futureReference =

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

Lines changed: 5 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -134,54 +134,21 @@ 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);
150148

151149
PowerMock.verify();
152150
}
153151

154-
@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")
171-
public void executeAcquireDeviceCode_B2CAuthorityUsed_IllegalArgumentExceptionThrown()
172-
throws Exception {
173-
174-
app = PublicClientApplication.builder("client_id")
175-
.b2cAuthority(TestConfiguration.B2C_AUTHORITY)
176-
.validateAuthority(false).build();
177-
178-
app.acquireToken
179-
(DeviceCodeFlowParameters
180-
.builder(Collections.singleton(AAD_RESOURCE_ID), (DeviceCode deviceCode) -> {})
181-
.build());
182-
}
183-
184-
185152
@Test
186153
public void executeAcquireDeviceCode_AuthenticaionPendingErrorReturned_AuthenticationExceptionThrown()
187154
throws Exception {

0 commit comments

Comments
 (0)