|
1 | 1 | package com.github.scribejava.core.oauth; |
2 | 2 |
|
3 | | -import java.io.IOException; |
4 | | -import java.io.OutputStream; |
5 | | -import java.util.concurrent.Future; |
| 3 | +import com.fasterxml.jackson.databind.JsonNode; |
| 4 | +import com.fasterxml.jackson.databind.ObjectMapper; |
6 | 5 | import com.github.scribejava.core.builder.api.DefaultApi20; |
7 | 6 | import com.github.scribejava.core.extractors.OAuth2AccessTokenJsonExtractor; |
8 | 7 | import com.github.scribejava.core.httpclient.HttpClient; |
9 | 8 | import com.github.scribejava.core.httpclient.HttpClientConfig; |
| 9 | +import com.github.scribejava.core.model.DeviceCode; |
10 | 10 | import com.github.scribejava.core.model.OAuth2AccessToken; |
| 11 | +import com.github.scribejava.core.model.OAuth2AccessTokenErrorResponse; |
11 | 12 | import com.github.scribejava.core.model.OAuth2Authorization; |
12 | 13 | import com.github.scribejava.core.model.OAuthAsyncRequestCallback; |
13 | 14 | import com.github.scribejava.core.model.OAuthConstants; |
14 | 15 | import com.github.scribejava.core.model.OAuthRequest; |
15 | 16 | import com.github.scribejava.core.model.Response; |
16 | 17 | import com.github.scribejava.core.model.Verb; |
| 18 | +import com.github.scribejava.core.oauth2.OAuth2Error; |
17 | 19 | import com.github.scribejava.core.pkce.PKCE; |
18 | | -import java.util.Map; |
19 | | -import java.util.concurrent.ExecutionException; |
20 | 20 | import com.github.scribejava.core.revoke.TokenTypeHint; |
| 21 | +import java.io.IOException; |
| 22 | +import java.io.OutputStream; |
21 | 23 | import java.io.UnsupportedEncodingException; |
22 | 24 | import java.net.URLDecoder; |
| 25 | +import java.util.Map; |
| 26 | +import java.util.concurrent.ExecutionException; |
| 27 | +import java.util.concurrent.Future; |
| 28 | + |
| 29 | +import static com.github.scribejava.core.extractors.OAuth2AccessTokenJsonExtractor.extractRequiredParameter; |
| 30 | +import static com.github.scribejava.core.oauth2.OAuth2Error.AUTHORIZATION_PENDING; |
| 31 | +import static com.github.scribejava.core.oauth2.OAuth2Error.SLOW_DOWN; |
| 32 | +import static java.lang.Thread.sleep; |
23 | 33 |
|
24 | 34 | public class OAuth20Service extends OAuthService { |
| 35 | + protected static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); |
25 | 36 |
|
26 | 37 | private static final String VERSION = "2.0"; |
27 | 38 | private final DefaultApi20 api; |
@@ -490,4 +501,83 @@ public String getResponseType() { |
490 | 501 | public String getDefaultScope() { |
491 | 502 | return defaultScope; |
492 | 503 | } |
| 504 | + |
| 505 | + /** |
| 506 | + * Requests a device code from a server. |
| 507 | + * |
| 508 | + * @see <a href="https://tools.ietf.org/html/rfc8628#section-3">rfc8628</a> |
| 509 | + * @see <a href="https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code"> |
| 510 | + * azure v2-oauth2-device-code</a> |
| 511 | + */ |
| 512 | + public DeviceCode getDeviceCode() throws InterruptedException, ExecutionException, IOException { |
| 513 | + final OAuthRequest request = new OAuthRequest(Verb.POST, api.getDeviceAuthorizationUrl()); |
| 514 | + request.addBodyParameter(OAuthConstants.CLIENT_ID, getApiKey()); |
| 515 | + request.addBodyParameter(OAuthConstants.SCOPE, getDefaultScope()); |
| 516 | + try (Response response = execute(request)) { |
| 517 | + final String body = response.getBody(); |
| 518 | + if (response.getCode() == 200) { |
| 519 | + final JsonNode n = OBJECT_MAPPER.readTree(body); |
| 520 | + return new DeviceCode( |
| 521 | + extractRequiredParameter(n, "device_code", body).textValue(), |
| 522 | + extractRequiredParameter(n, "user_code", body).textValue(), |
| 523 | + extractRequiredParameter(n, "verification_uri", body).textValue(), |
| 524 | + n.path("interval").asInt(5), |
| 525 | + extractRequiredParameter(n, "expires_in", body).intValue()); |
| 526 | + } else { |
| 527 | + OAuth2AccessTokenJsonExtractor.instance().generateError(body); |
| 528 | + throw new IllegalStateException(); // generateError() always throws an exception |
| 529 | + } |
| 530 | + } |
| 531 | + } |
| 532 | + |
| 533 | + /** |
| 534 | + * Attempts to get a token from a server. |
| 535 | + * Function {@link #pollDeviceAccessToken(DeviceCode)} is usually used instead of this. |
| 536 | + * |
| 537 | + * @return token |
| 538 | + * @throws OAuth2AccessTokenErrorResponse |
| 539 | + * If {@link OAuth2AccessTokenErrorResponse#getError()} is |
| 540 | + * {@link OAuth2Error#AUTHORIZATION_PENDING} or {@link OAuth2Error#SLOW_DOWN}, |
| 541 | + * another attempt should be made after a while. |
| 542 | + * |
| 543 | + * @see #getDeviceCode() |
| 544 | + */ |
| 545 | + public OAuth2AccessToken getAccessTokenDeviceCodeGrant(DeviceCode deviceCode) |
| 546 | + throws IOException, InterruptedException, ExecutionException { |
| 547 | + final OAuthRequest request = new OAuthRequest(Verb.POST, api.getAccessTokenEndpoint()); |
| 548 | + request.addParameter(OAuthConstants.GRANT_TYPE, "urn:ietf:params:oauth:grant-type:device_code"); |
| 549 | + request.addBodyParameter(OAuthConstants.CLIENT_ID, getApiKey()); |
| 550 | + request.addParameter("device_code", deviceCode.getDeviceCode()); |
| 551 | + try (Response response = execute(request)) { |
| 552 | + return api.getAccessTokenExtractor().extract(response); |
| 553 | + } |
| 554 | + } |
| 555 | + |
| 556 | + /** |
| 557 | + * Periodically tries to get a token from a server (waiting for the user to give consent). |
| 558 | + * |
| 559 | + * @return token |
| 560 | + * @throws OAuth2AccessTokenErrorResponse |
| 561 | + * Indicates OAuth error. |
| 562 | + * |
| 563 | + * @see #getDeviceCode() |
| 564 | + */ |
| 565 | + public OAuth2AccessToken pollDeviceAccessToken(DeviceCode deviceCode) |
| 566 | + throws InterruptedException, ExecutionException, IOException { |
| 567 | + long intervalMillis = deviceCode.getIntervalSeconds() * 1000; |
| 568 | + while (true) { |
| 569 | + try { |
| 570 | + return getAccessTokenDeviceCodeGrant(deviceCode); |
| 571 | + } catch (OAuth2AccessTokenErrorResponse e) { |
| 572 | + if (e.getError() != AUTHORIZATION_PENDING) { |
| 573 | + if (e.getError() == SLOW_DOWN) { |
| 574 | + intervalMillis += 5000; |
| 575 | + } else { |
| 576 | + throw e; |
| 577 | + } |
| 578 | + } |
| 579 | + } |
| 580 | + sleep(intervalMillis); |
| 581 | + } |
| 582 | + } |
493 | 583 | } |
0 commit comments