X Tutup
Skip to content

Commit 08d6ecb

Browse files
committed
add support for OAuth 2.0 Device Authorization Grant (RFC 8628) (thanks to https://github.com/rebarbora-mckvak)
1 parent 50e7c96 commit 08d6ecb

File tree

15 files changed

+496
-166
lines changed

15 files changed

+496
-166
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ ScribeJava support out-of-box several HTTP clients:
5050
* [RFC 6750](https://tools.ietf.org/html/rfc6750) The OAuth 2.0 Authorization Framework: Bearer Token Usage
5151
* [RFC 7636](https://tools.ietf.org/html/rfc7636) Proof Key for Code Exchange by OAuth Public Clients (PKCE), [example](https://github.com/scribejava/scribejava/blob/master/scribejava-apis/src/test/java/com/github/scribejava/apis/examples/Google20WithPKCEExample.java)
5252
* [RFC 7009](https://tools.ietf.org/html/rfc7009) OAuth 2.0 Token Revocation, [example](https://github.com/scribejava/scribejava/blob/master/scribejava-apis/src/test/java/com/github/scribejava/apis/examples/Google20RevokeExample.java)
53+
* [RFC 8628](https://tools.ietf.org/html/rfc8628) OAuth 2.0 Device Authorization Grant [example](https://github.com/scribejava/scribejava/blob/master/scribejava-apis/src/test/java/com/github/scribejava/apis/examples/Google20DeviceAuthorizationGrantExample.java)
5354
* [RFC 5849](https://tools.ietf.org/html/rfc5849) The OAuth 1.0 Protocol, [example](https://github.com/scribejava/scribejava/blob/master/scribejava-apis/src/test/java/com/github/scribejava/apis/examples/TwitterExample.java)
5455

5556
### Supports all (50+) major 1.0a and 2.0 OAuth APIs out-of-the-box

changelog

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
[SNAPSHOT]
22
* add Kakao API (https://kakao.com/) (thanks to https://github.com/v0o0v)
33
* support chunks in JDKHttpClient's Multipart (thanks to https://github.com/eos1d3)
4+
* add support for OAuth 2.0 Device Authorization Grant (RFC 8628) (thanks to https://github.com/rebarbora-mckvak)
45

56
[7.1.1]
67
* add Proxy support (via config's option) to internal JDKHttpClient (thanks to https://github.com/bjournaud)

scribejava-apis/src/main/java/com/github/scribejava/apis/GoogleApi20.java

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package com.github.scribejava.apis;
22

3+
import com.github.scribejava.apis.google.GoogleDeviceAuthorizationJsonExtractor;
34
import com.github.scribejava.apis.openid.OpenIdJsonTokenExtractor;
45
import com.github.scribejava.core.builder.api.DefaultApi20;
6+
import com.github.scribejava.core.extractors.DeviceAuthorizationJsonExtractor;
57
import com.github.scribejava.core.extractors.TokenExtractor;
68
import com.github.scribejava.core.model.OAuth2AccessToken;
79

@@ -11,6 +13,7 @@ protected GoogleApi20() {
1113
}
1214

1315
private static class InstanceHolder {
16+
1417
private static final GoogleApi20 INSTANCE = new GoogleApi20();
1518
}
1619

@@ -20,12 +23,12 @@ public static GoogleApi20 instance() {
2023

2124
@Override
2225
public String getAccessTokenEndpoint() {
23-
return "https://www.googleapis.com/oauth2/v4/token";
26+
return "https://oauth2.googleapis.com/token";
2427
}
2528

2629
@Override
2730
protected String getAuthorizationBaseUrl() {
28-
return "https://accounts.google.com/o/oauth2/auth";
31+
return "https://accounts.google.com/o/oauth2/v2/auth";
2932
}
3033

3134
@Override
@@ -35,6 +38,16 @@ public TokenExtractor<OAuth2AccessToken> getAccessTokenExtractor() {
3538

3639
@Override
3740
public String getRevokeTokenEndpoint() {
38-
return "https://accounts.google.com/o/oauth2/revoke";
41+
return "https://oauth2.googleapis.com/revoke";
42+
}
43+
44+
@Override
45+
public String getDeviceAuthorizationEndpoint() {
46+
return "https://oauth2.googleapis.com/device/code";
47+
}
48+
49+
@Override
50+
public DeviceAuthorizationJsonExtractor getDeviceAuthorizationExtractor() {
51+
return GoogleDeviceAuthorizationJsonExtractor.instance();
3952
}
4053
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.github.scribejava.apis.google;
2+
3+
import com.github.scribejava.core.extractors.DeviceAuthorizationJsonExtractor;
4+
5+
public class GoogleDeviceAuthorizationJsonExtractor extends DeviceAuthorizationJsonExtractor {
6+
7+
protected GoogleDeviceAuthorizationJsonExtractor() {
8+
}
9+
10+
private static class InstanceHolder {
11+
12+
private static final GoogleDeviceAuthorizationJsonExtractor INSTANCE
13+
= new GoogleDeviceAuthorizationJsonExtractor();
14+
}
15+
16+
public static GoogleDeviceAuthorizationJsonExtractor instance() {
17+
return GoogleDeviceAuthorizationJsonExtractor.InstanceHolder.INSTANCE;
18+
}
19+
20+
@Override
21+
protected String getVerificationUriParamName() {
22+
return "verification_url";
23+
}
24+
25+
}

scribejava-apis/src/main/java/com/github/scribejava/apis/microsoftazureactivedirectory/BaseMicrosoftAzureActiveDirectoryApi.java

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,6 @@ protected String getAuthorizationBaseUrl() {
3030
return MSFT_LOGIN_URL + tenant + OAUTH_2 + getEndpointVersionPath() + "/authorize";
3131
}
3232

33-
@Override
34-
public String getDeviceAuthorizationUrl() {
35-
return MSFT_LOGIN_URL + tenant + OAUTH_2 + getEndpointVersionPath() + "/devicecode";
36-
}
37-
3833
@Override
3934
public ClientAuthentication getClientAuthentication() {
4035
return RequestBodyAuthenticationScheme.instance();
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package com.github.scribejava.apis.examples;
2+
3+
import com.github.scribejava.core.builder.ServiceBuilder;
4+
import com.github.scribejava.apis.GoogleApi20;
5+
import com.github.scribejava.core.model.DeviceAuthorization;
6+
import com.github.scribejava.core.model.OAuth2AccessToken;
7+
import com.github.scribejava.core.model.OAuthRequest;
8+
import com.github.scribejava.core.model.Response;
9+
import com.github.scribejava.core.model.Verb;
10+
import com.github.scribejava.core.oauth.OAuth20Service;
11+
import java.io.IOException;
12+
import java.util.Scanner;
13+
import java.util.concurrent.ExecutionException;
14+
15+
public class Google20DeviceAuthorizationGrantExample {
16+
17+
private static final String NETWORK_NAME = "Google";
18+
private static final String PROTECTED_RESOURCE_URL = "https://www.googleapis.com/oauth2/v3/userinfo";
19+
20+
private Google20DeviceAuthorizationGrantExample() {
21+
}
22+
23+
@SuppressWarnings("PMD.SystemPrintln")
24+
public static void main(String... args) throws IOException, InterruptedException, ExecutionException {
25+
// Replace these with your client id and secret
26+
final String clientId = "your client id";
27+
final String clientSecret = "your_client_secret";
28+
29+
final OAuth20Service service = new ServiceBuilder(clientId)
30+
.debug()
31+
.apiSecret(clientSecret)
32+
.defaultScope("profile") // replace with desired scope
33+
.build(GoogleApi20.instance());
34+
final Scanner in = new Scanner(System.in, "UTF-8");
35+
36+
System.out.println("=== " + NETWORK_NAME + "'s OAuth Workflow ===");
37+
System.out.println();
38+
39+
System.out.println("Requesting a set of verification codes...");
40+
41+
final DeviceAuthorization deviceAuthorization = service.getDeviceAuthorizationCodes();
42+
System.out.println("Got the Device Authorization Codes!");
43+
System.out.println(deviceAuthorization);
44+
45+
System.out.println("Now go and authorize ScribeJava. Visit: " + deviceAuthorization.getVerificationUri()
46+
+ " and enter the code: " + deviceAuthorization.getUserCode());
47+
if (deviceAuthorization.getVerificationUriComplete() != null) {
48+
System.out.println("Or visit " + deviceAuthorization.getVerificationUriComplete());
49+
}
50+
51+
System.out.println("Polling for an Access Token...");
52+
final OAuth2AccessToken accessToken = service.pollAccessTokenDeviceAuthorizationGrant(deviceAuthorization);
53+
54+
System.out.println("Got the Access Token!");
55+
System.out.println("(The raw response looks like this: " + accessToken.getRawResponse() + "')");
56+
57+
// Now let's go and ask for a protected resource!
58+
System.out.println("Now we're going to access a protected resource...");
59+
while (true) {
60+
System.out.println("Paste fieldnames to fetch (leave empty to get profile, 'exit' to stop the example)");
61+
System.out.print(">>");
62+
final String query = in.nextLine();
63+
System.out.println();
64+
final String requestUrl;
65+
if ("exit".equals(query)) {
66+
break;
67+
} else if (query == null || query.isEmpty()) {
68+
requestUrl = PROTECTED_RESOURCE_URL;
69+
} else {
70+
requestUrl = PROTECTED_RESOURCE_URL + "?fields=" + query;
71+
}
72+
final OAuthRequest request = new OAuthRequest(Verb.GET, requestUrl);
73+
service.signRequest(accessToken, request);
74+
System.out.println();
75+
try (Response response = service.execute(request)) {
76+
System.out.println(response.getCode());
77+
System.out.println(response.getBody());
78+
}
79+
System.out.println();
80+
}
81+
}
82+
}

scribejava-apis/src/test/java/com/github/scribejava/apis/examples/Google20Example.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ private Google20Example() {
2525
@SuppressWarnings("PMD.SystemPrintln")
2626
public static void main(String... args) throws IOException, InterruptedException, ExecutionException {
2727
// Replace these with your client id and secret
28-
final String clientId = "your client id";
29-
final String clientSecret = "your client secret";
28+
final String clientId = "your_client_id";
29+
final String clientSecret = "your_client_secret";
3030
final String secretState = "secret" + new Random().nextInt(999_999);
3131
final OAuth20Service service = new ServiceBuilder(clientId)
3232
.apiSecret(clientSecret)

scribejava-core/src/main/java/com/github/scribejava/core/builder/api/DefaultApi20.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.github.scribejava.core.builder.api;
22

3+
import com.github.scribejava.core.extractors.DeviceAuthorizationJsonExtractor;
34
import com.github.scribejava.core.extractors.OAuth2AccessTokenJsonExtractor;
45
import com.github.scribejava.core.extractors.TokenExtractor;
56
import com.github.scribejava.core.httpclient.HttpClient;
@@ -122,7 +123,18 @@ public ClientAuthentication getClientAuthentication() {
122123
return HttpBasicAuthenticationScheme.instance();
123124
}
124125

125-
public String getDeviceAuthorizationUrl() {
126-
return null;
126+
/**
127+
* RFC 8628 OAuth 2.0 Device Authorization Grant
128+
*
129+
* @see <a href="https://tools.ietf.org/html/rfc8628">RFC 8628</a>
130+
* @return the device authorization endpoint
131+
*/
132+
public String getDeviceAuthorizationEndpoint() {
133+
throw new UnsupportedOperationException(
134+
"This API doesn't support Device Authorization Grant or we have no info about this");
135+
}
136+
137+
public DeviceAuthorizationJsonExtractor getDeviceAuthorizationExtractor() {
138+
return DeviceAuthorizationJsonExtractor.instance();
127139
}
128140
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.github.scribejava.core.extractors;
2+
3+
import com.fasterxml.jackson.databind.JsonNode;
4+
import com.fasterxml.jackson.databind.ObjectMapper;
5+
import com.github.scribejava.core.exceptions.OAuthException;
6+
7+
public abstract class AbstractJsonExtractor {
8+
9+
protected static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
10+
11+
protected static JsonNode extractRequiredParameter(JsonNode errorNode, String parameterName, String rawResponse)
12+
throws OAuthException {
13+
final JsonNode value = errorNode.get(parameterName);
14+
15+
if (value == null) {
16+
throw new OAuthException("Response body is incorrect. Can't extract a '" + parameterName
17+
+ "' from this: '" + rawResponse + "'", null);
18+
}
19+
20+
return value;
21+
}
22+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package com.github.scribejava.core.extractors;
2+
3+
import static com.github.scribejava.core.extractors.AbstractJsonExtractor.OBJECT_MAPPER;
4+
import static com.github.scribejava.core.extractors.AbstractJsonExtractor.extractRequiredParameter;
5+
import com.fasterxml.jackson.databind.JsonNode;
6+
import com.github.scribejava.core.model.DeviceAuthorization;
7+
import java.io.IOException;
8+
import com.github.scribejava.core.model.Response;
9+
10+
public class DeviceAuthorizationJsonExtractor extends AbstractJsonExtractor {
11+
12+
protected DeviceAuthorizationJsonExtractor() {
13+
}
14+
15+
private static class InstanceHolder {
16+
17+
private static final DeviceAuthorizationJsonExtractor INSTANCE = new DeviceAuthorizationJsonExtractor();
18+
}
19+
20+
public static DeviceAuthorizationJsonExtractor instance() {
21+
return InstanceHolder.INSTANCE;
22+
}
23+
24+
public DeviceAuthorization extract(Response response) throws IOException {
25+
26+
final String body = response.getBody();
27+
28+
if (response.getCode() != 200) {
29+
generateError(body);
30+
}
31+
return createDeviceAuthorization(body);
32+
}
33+
34+
public void generateError(String rawResponse) throws IOException {
35+
OAuth2AccessTokenJsonExtractor.instance().generateError(rawResponse);
36+
}
37+
38+
private DeviceAuthorization createDeviceAuthorization(String rawResponse) throws IOException {
39+
40+
final JsonNode response = OBJECT_MAPPER.readTree(rawResponse);
41+
42+
final DeviceAuthorization deviceAuthorization = new DeviceAuthorization(
43+
extractRequiredParameter(response, "device_code", rawResponse).textValue(),
44+
extractRequiredParameter(response, "user_code", rawResponse).textValue(),
45+
extractRequiredParameter(response, getVerificationUriParamName(), rawResponse).textValue(),
46+
extractRequiredParameter(response, "expires_in", rawResponse).intValue());
47+
48+
final JsonNode intervalSeconds = response.get("interval");
49+
if (intervalSeconds != null) {
50+
deviceAuthorization.setIntervalSeconds(intervalSeconds.asInt(5));
51+
}
52+
53+
final JsonNode verificationUriComplete = response.get("verification_uri_complete");
54+
if (verificationUriComplete != null) {
55+
deviceAuthorization.setVerificationUriComplete(verificationUriComplete.asText());
56+
}
57+
58+
return deviceAuthorization;
59+
}
60+
61+
protected String getVerificationUriParamName() {
62+
return "verification_uri";
63+
}
64+
}

0 commit comments

Comments
 (0)
X Tutup