Skip to content
This repository was archived by the owner on Oct 12, 2022. It is now read-only.
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
@@ -0,0 +1,36 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root.

package com.microsoft.alm.authentication;

import com.microsoft.alm.helpers.QueryString;

public class AzureDeviceFlow extends DeviceFlowImpl
{
private String resource;

public String getResource()
{
return resource;
}

public void setResource(final String resource)
{
this.resource = resource;
}

@Override
protected void contributeAuthorizationRequestParameters(final QueryString bodyParameters)
{
if (resource != null)
{
bodyParameters.put("resource", resource);
}
}

@Override
protected DeviceFlowResponse buildDeviceFlowResponse(final String responseText)
{
return AzureDeviceFlowResponse.fromJson(responseText);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root.

package com.microsoft.alm.authentication;

import com.microsoft.alm.helpers.PropertyBag;

import java.net.URI;

public class AzureDeviceFlowResponse extends DeviceFlowResponse
{
static final String VERIFICATION_URL = "verification_url";
static final String MESSAGE = "message";

private final String message;

public AzureDeviceFlowResponse(final String deviceCode, final String userCode, final URI verificationUri, final int expiresIn, final int interval, final String message)
{
super(deviceCode, userCode, verificationUri, expiresIn, interval);
this.message = message;
}

public String getMessage()
{
return message;
}

public static AzureDeviceFlowResponse fromJson(final String jsonText) {
final PropertyBag bag = PropertyBag.fromJson(jsonText);
final String deviceCode = (String) bag.get(OAuthParameter.DEVICE_CODE);
final String userCode = (String) bag.get(OAuthParameter.USER_CODE);
final String verificationUriString = (String) bag.get(VERIFICATION_URL);
final URI verificationUri = URI.create(verificationUriString);
final int expiresInSeconds = bag.readOptionalInteger(OAuthParameter.EXPIRES_IN, 600);
final int intervalInSeconds = bag.readOptionalInteger(OAuthParameter.INTERVAL, 5);
final String message = (String) bag.get(MESSAGE);

final AzureDeviceFlowResponse result = new AzureDeviceFlowResponse(deviceCode, userCode, verificationUri, expiresInSeconds, intervalInSeconds, message);
return result;
}
}
21 changes: 20 additions & 1 deletion src/main/java/com/microsoft/alm/helpers/PropertyBag.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,26 @@ public static PropertyBag fromJson(final String input) {
}

public int readOptionalInteger(final String key, final int defaultValue) {
return SimpleJson.readOptionalInteger(this, key, defaultValue);
final int result;
if (containsKey(key)) {
final Object candidateResult = get(key);
if (candidateResult instanceof Double) {
final Double resultAsDouble = (Double) candidateResult;
result = (int) Math.round(resultAsDouble);
}
else if (candidateResult instanceof String) {
final String resultAsString = (String) candidateResult;
result = Integer.parseInt(resultAsString, 10);
}
else {
result = defaultValue;
}
}
else {
result = defaultValue;
}

return result;
}

public String readOptionalString(final String key, final String defaultValue) {
Expand Down
35 changes: 23 additions & 12 deletions src/main/java/com/microsoft/alm/helpers/SimpleJson.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ enum State {
STRING_VALUE,
STRING_VALUE_ESCAPE,
STRING_VALUE_UNICODE,
SQUARE_BRACKET_STRING,
LITERAL_VALUE,
POST_VALUE,
END,
Expand Down Expand Up @@ -57,6 +58,14 @@ static boolean isRightCurlyBracket(final char c) {
return c == '}';
}

static boolean isLeftSquareBracket(final char c) {
return c == '[';
}

static boolean isRightSquareBracket(final char c) {
return c == ']';
}

static boolean isColon(final char c) {
return c == ':';
}
Expand Down Expand Up @@ -176,6 +185,9 @@ else if (isLiteralStart(c)) {
token.append(c);
state = State.LITERAL_VALUE;
}
else if (isLeftSquareBracket(c)) {
state = State.SQUARE_BRACKET_STRING;
}
else if (isInsignificantWhitespace(c)) {
continue;
}
Expand Down Expand Up @@ -271,6 +283,17 @@ else if (isDoubleQuote(c)) {
error(c, state);
}
break;
case SQUARE_BRACKET_STRING:
if (isRightSquareBracket(c)) {
value = token.toString();
token.setLength(0);
destination.put(key, value);
state = State.POST_VALUE;
}
else {
token.append(c);
}
break;
case LITERAL_VALUE:
switch (c) {
case 'a':
Expand Down Expand Up @@ -324,18 +347,6 @@ else if (isInsignificantWhitespace(c)) {
}
}

public static int readOptionalInteger(final Map<String, Object> pairs, final String key, final int defaultValue) {
final int result;
if (pairs.containsKey(key)) {
final Double resultAsDouble = (Double) pairs.get(key);
result = (int) Math.round(resultAsDouble);
}
else {
result = defaultValue;
}
return result;
}

public static String format(final Map<String, Object> input) {
return null;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root.

package com.microsoft.alm.authentication

import groovy.transform.CompileStatic
import org.junit.Test

/**
* A class to test {@link AzureDeviceFlowResponse}.
*/
@CompileStatic
public class AzureDeviceFlowResponseTest {

@Test public void fromJson_azureActiveDirectory() {
final now = Calendar.instance
final def input = """\
{"user_code":"EZ2KYPAW4","device_code":"EAAABAAEAiL9Kn2Z27Uubv","verification_url":"https://aka.ms/devicelogin","expires_in":"900","interval":"5","message":"To sign in, use a web browser to open the page https://aka.ms/devicelogin. Enter the code EZ2KYPAW4 to authenticate."}\
"""

final actual = AzureDeviceFlowResponse.fromJson(input)

assert "EAAABAAEAiL9Kn2Z27Uubv" == actual.deviceCode
assert "EZ2KYPAW4" == actual.userCode
assert URI.create("https://aka.ms/devicelogin") == actual.verificationUri
assert 5 == actual.interval
assert 900 == actual.expiresIn
assert actual.expiresAt.timeInMillis - now.timeInMillis >= 900
assert "To sign in, use a web browser to open the page https://aka.ms/devicelogin. Enter the code EZ2KYPAW4 to authenticate." == actual.message;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root.

package com.microsoft.alm.authentication

import groovy.transform.CompileStatic
import org.junit.Assert
import org.junit.Ignore
import org.junit.Test

/**
* A class to test {@link AzureDeviceFlow}.
*/
@CompileStatic
public class AzureDeviceFlowTest {

@Test public void buildTokenPair_sampleResponse() {
final input = /{"token_type":"Bearer","scope":"user_impersonation","expires_in":"3600","expires_on":"1460690464","not_before":"1460686564","resource":"4e725760-015b-4168-8adf-e8329e863974","access_token":"Q29uZ3JhdHVsYXRpb25zLCB5b3UgaGF2ZSBzdWNjZXNzZnVsbHkgZGVjb2RlZCBhIGZha2UgYWNjZXNzIHRva2VuLg==","refresh_token":"Tm93IHlvdSBzdWNjZXNzZnVsbHkgZGVjb2RlZCBhIGZha2UgcmVmcmVzaCB0b2tlbi4=","id_token":"SSBub3RpY2UgeW91J3JlIHN0aWxsIGRlY29kaW5nIGZha2UgdG9rZW5zLiAgVGhpcyBvbmUncyBmb3IgaWRfdG9rZW4u"}/;
final cut = new AzureDeviceFlow();

final actualTokenPair = cut.buildTokenPair(input);

assert "Q29uZ3JhdHVsYXRpb25zLCB5b3UgaGF2ZSBzdWNjZXNzZnVsbHkgZGVjb2RlZCBhIGZha2UgYWNjZXNzIHRva2VuLg==" == actualTokenPair.AccessToken.Value;
}

@Ignore("Must be run manually after setting some system properties")
@Test public void endToEnd_manual() {
final tenantIdString = System.getProperty("tenantId");
final clientId = System.getProperty("clientId");
final resource = System.getProperty("resource");
final UUID tenantId = UUID.fromString(tenantIdString);
final String authorityUrl = AzureAuthority.getAuthorityUrl(tenantId);
final cut = new AzureDeviceFlow();
cut.resource = resource;
final deviceEndpoint = URI.create(authorityUrl + "/oauth2/devicecode");
final tokenEndpoint = URI.create(authorityUrl + "/oauth2/token");

final deviceFlowResponse = cut.requestAuthorization(deviceEndpoint, clientId, null);
System.err.println("------------------------------------");
System.err.println("OAuth 2.0 Device Flow authentication");
System.err.println("------------------------------------");
System.err.println("To complete the authentication process, please open a web browser and visit the following URI:");
System.err.println(deviceFlowResponse.getVerificationUri());
System.err.println("When prompted, enter the following code:");
System.err.println(deviceFlowResponse.getUserCode());
System.err.println("Once authenticated and authorized, execution will continue.");

final TokenPair actualTokens = cut.requestToken(tokenEndpoint, clientId, deviceFlowResponse);

Assert.assertEquals(TokenType.Access, actualTokens.AccessToken.Type);
}

}
50 changes: 50 additions & 0 deletions src/test/groovy/com/microsoft/alm/helpers/PropertyBagTest.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root.

package com.microsoft.alm.helpers

import groovy.transform.CompileStatic
import org.junit.Test

/**
* A class to test {@see PropertyBag}.
*/
@CompileStatic
public class PropertyBagTest {

@Test public void readOptionalInteger_number() {
final cut = new PropertyBag();
cut.put("answer", 42.0d);

final actual = cut.readOptionalInteger("answer", 0);

assert 42 == actual
}

@Test public void readOptionalInteger_string() {
final cut = new PropertyBag();
cut.put("answer", "42");

final actual = cut.readOptionalInteger("answer", 0);

assert 42 == actual
}

@Test public void readOptionalInteger_otherObject() {
final cut = new PropertyBag();
cut.put("answer", null);

final actual = cut.readOptionalInteger("answer", 0);

assert 0 == actual
}

@Test public void readOptionalInteger_missing() {
final cut = new PropertyBag();

final actual = cut.readOptionalInteger("answer", 0);

assert 0 == actual
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ public class SimpleJsonTest {
assertParse(["name":"value"], /{"name":"value",}/)
}

@Test public void parse_singleSquareBracketString() {
assertParse(["name":'"value"'], /{"name":["value"]}/)
assertParse(["name":'"value"'], /{"name":["value"],}/)
assertParse(["error_codes":'50001'], /{"error_codes":[50001]}/)
}

@Test public void parse_insignificantWhitespace() {
assertParse(["name":"value"], /{"name":"value" ,}/)
assertParse(["name":"value"], /{"name":"value", }/)
Expand Down