From 3c82c51e500ef4122e4d7d904e64bec38e8ee1c4 Mon Sep 17 00:00:00 2001 From: Xiang Yao Date: Fri, 18 Dec 2020 20:10:14 -0800 Subject: [PATCH] Added OAuth2 support and examples for Twitter API --- README.md | 1 + .../github/scribejava/apis/TwitterApi20.java | 48 +++++++ .../examples/Twitter20WithPKCEExample.java | 133 ++++++++++++++++++ .../core/oauth/AccessTokenRequestParams.java | 14 +- .../scribejava/core/oauth/OAuth20Service.java | 31 +++- 5 files changed, 220 insertions(+), 7 deletions(-) create mode 100644 scribejava-apis/src/main/java/com/github/scribejava/apis/TwitterApi20.java create mode 100644 scribejava-apis/src/test/java/com/github/scribejava/apis/examples/Twitter20WithPKCEExample.java diff --git a/README.md b/README.md index 83180de24..a79a7f61f 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,7 @@ ScribeJava support out-of-box several HTTP clients: * Tumblr (https://www.tumblr.com/) [example](https://github.com/scribejava/scribejava/blob/master/scribejava-apis/src/test/java/com/github/scribejava/apis/examples/TumblrExample.java) * TUT.BY (http://www.tut.by/) [example](https://github.com/scribejava/scribejava/blob/master/scribejava-apis/src/test/java/com/github/scribejava/apis/examples/TutByExample.java) * Twitter (https://twitter.com/) [example](https://github.com/scribejava/scribejava/blob/master/scribejava-apis/src/test/java/com/github/scribejava/apis/examples/TwitterExample.java) +* Twitter 2.0 (https://twitter.com/) [example](https://github.com/scribejava/scribejava/blob/master/scribejava-apis/src/test/java/com/github/scribejava/apis/examples/Twitter20WithPKCEExample.java) * uCoz (https://www.ucoz.com/) [example](https://github.com/scribejava/scribejava/blob/master/scribejava-apis/src/test/java/com/github/scribejava/apis/examples/UcozExample.java) * Viadeo (http://viadeo.com/) [example](https://github.com/scribejava/scribejava/blob/master/scribejava-apis/src/test/java/com/github/scribejava/apis/examples/ViadeoExample.java) * VK ВКонтакте (http://vk.com/) [example](https://github.com/scribejava/scribejava/blob/master/scribejava-apis/src/test/java/com/github/scribejava/apis/examples/VkontakteExample.java), [example Client Credentials Grant](https://github.com/scribejava/scribejava/blob/master/scribejava-apis/src/test/java/com/github/scribejava/apis/examples/VkontakteClientCredentialsGrantExample.java), [example with External HTTP Client](https://github.com/scribejava/scribejava/blob/master/scribejava-apis/src/test/java/com/github/scribejava/apis/examples/VkontakteExternalHttpExample.java) diff --git a/scribejava-apis/src/main/java/com/github/scribejava/apis/TwitterApi20.java b/scribejava-apis/src/main/java/com/github/scribejava/apis/TwitterApi20.java new file mode 100644 index 000000000..49fe3f029 --- /dev/null +++ b/scribejava-apis/src/main/java/com/github/scribejava/apis/TwitterApi20.java @@ -0,0 +1,48 @@ +package com.github.scribejava.apis; + +import com.github.scribejava.apis.openid.OpenIdJsonTokenExtractor; +import com.github.scribejava.core.builder.api.DefaultApi20; +import com.github.scribejava.core.extractors.TokenExtractor; +import com.github.scribejava.core.model.OAuth2AccessToken; +import com.github.scribejava.core.oauth2.clientauthentication.ClientAuthentication; +import com.github.scribejava.core.oauth2.clientauthentication.RequestBodyAuthenticationScheme; + +public class TwitterApi20 extends DefaultApi20 { + + protected TwitterApi20() { + } + + private static class InstanceHolder { + + private static final TwitterApi20 INSTANCE = new TwitterApi20(); + } + + public static TwitterApi20 instance() { + return InstanceHolder.INSTANCE; + } + + @Override + public String getAccessTokenEndpoint() { + return "https://api.twitter.com/2/oauth2/token"; + } + + @Override + protected String getAuthorizationBaseUrl() { + return "https://developer.twitter.com/2/oauth2/consent"; + } + + @Override + public TokenExtractor getAccessTokenExtractor() { + return OpenIdJsonTokenExtractor.instance(); + } + + @Override + public String getRevokeTokenEndpoint() { + return "https://api.twitter.com/2/oauth2/revoke"; + } + + @Override + public ClientAuthentication getClientAuthentication() { + return RequestBodyAuthenticationScheme.instance(); + } +} diff --git a/scribejava-apis/src/test/java/com/github/scribejava/apis/examples/Twitter20WithPKCEExample.java b/scribejava-apis/src/test/java/com/github/scribejava/apis/examples/Twitter20WithPKCEExample.java new file mode 100644 index 000000000..cfd3d2f0a --- /dev/null +++ b/scribejava-apis/src/test/java/com/github/scribejava/apis/examples/Twitter20WithPKCEExample.java @@ -0,0 +1,133 @@ +package com.github.scribejava.apis.examples; + +import java.util.Random; +import java.util.Scanner; + +import com.github.scribejava.apis.TwitterApi20; +import com.github.scribejava.core.builder.ServiceBuilder; +import com.github.scribejava.core.oauth.AuthorizationUrlBuilder; +import com.github.scribejava.core.model.OAuth2AccessToken; +import com.github.scribejava.core.model.OAuthRequest; +import com.github.scribejava.core.model.Response; +import com.github.scribejava.core.model.Verb; +import com.github.scribejava.core.oauth.AccessTokenRequestParams; +import com.github.scribejava.core.oauth.OAuth20Service; +import com.github.scribejava.core.revoke.TokenTypeHint; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutionException; + +public class Twitter20WithPKCEExample { + + private static final String NETWORK_NAME = "Twitter"; + private static final String PROTECTED_RESOURCE_URL = "https://api.twitter.com/2/tweets?ids=1261326399320715264,1278347468690915330"; + + private Twitter20WithPKCEExample() { + } + + @SuppressWarnings("PMD.SystemPrintln") + public static void main(String... args) throws IOException, InterruptedException, ExecutionException { + final String clientId = "CLIENT_ID"; // replace these with your client id + final String state = "secret" + new Random().nextInt(999_999); + final OAuth20Service service = new ServiceBuilder(clientId) + .defaultScope("tweet.read users.read account.follows.read account.follows.write") // replace with desired scope + .callback("https://twitter.com/") + .build(TwitterApi20.instance()); + + final Scanner in = new Scanner(System.in, "UTF-8"); + + System.out.println("=== " + NETWORK_NAME + "'s OAuth Workflow ==="); + System.out.println(); + + // Obtain the Authorization URL + System.out.println("Fetching the Authorization URL..."); + final Map additionalParams = new HashMap<>(); + + final AuthorizationUrlBuilder authorizationUrlBuilder = service.createAuthorizationUrlBuilder() + .state(state) + .additionalParams(additionalParams) + .initPKCE(); + + System.out.println("Got the Authorization URL!"); + System.out.println("Now go and authorize ScribeJava here:"); + System.out.println(authorizationUrlBuilder.build()); + System.out.println("And paste the authorization code here"); + System.out.print(">>"); + final String code = in.nextLine(); + System.out.println(); + + System.out.println("And paste the state from server here. We have set 'state'='" + state + "'."); + System.out.print(">>"); + final String value = in.nextLine(); + if (state.equals(value)) { + System.out.println("State value does match!"); + } else { + System.out.println("Ooops, state value does not match!"); + System.out.println("Expected = " + state); + System.out.println("Got = " + value); + System.out.println(); + } + + System.out.println("Trading the Authorization Code for an Access Token..."); + OAuth2AccessToken accessToken = service.getAccessToken(AccessTokenRequestParams.create(code).clientId(clientId) + .pkceCodeVerifier(authorizationUrlBuilder.getPkce().getCodeVerifier())); + System.out.println("Got the Access Token!"); + System.out.println("(The raw response looks like this: " + accessToken.getRawResponse() + "')"); + + fetchResource(service, accessToken, PROTECTED_RESOURCE_URL); + + System.out.println("Refreshing the Access Token..."); + accessToken = service.refreshAccessToken(accessToken.getRefreshToken(), null, clientId); + System.out.println("Refreshed the Access Token!"); + System.out.println("(The raw response looks like this: " + accessToken.getRawResponse() + "')"); + System.out.println(); + + fetchResource(service, accessToken, PROTECTED_RESOURCE_URL); + + System.out.println("Revoking the Refresh Token..."); + service.revokeToken(accessToken.getRefreshToken(), TokenTypeHint.REFRESH_TOKEN); + System.out.println("Revoked the Refresh Token!"); + // Access Token is still valid + fetchResource(service, accessToken, PROTECTED_RESOURCE_URL); + + System.out.println("Revoking the Access Token..."); + service.revokeToken(accessToken.getAccessToken(), TokenTypeHint.ACCESS_TOKEN); + System.out.println("Revoked the Access Token!"); + // Both Access Token and Refresh Token are revoked at this moment + fetchResource(service, accessToken, PROTECTED_RESOURCE_URL); + + // Now let's go and ask for a protected resource! + while (true) { + System.out.println("Paste fieldnames to fetch (leave empty to get profile, 'exit' to stop example)"); + System.out.print(">>"); + final String query = in.nextLine(); + System.out.println(); + + final String requestUrl; + if ("exit".equals(query)) { + break; + } else if (query == null || query.isEmpty()) { + requestUrl = PROTECTED_RESOURCE_URL; + } else { + requestUrl = PROTECTED_RESOURCE_URL + "?fields=" + query; + } + fetchResource(service, accessToken, requestUrl); + } + } + + private static void fetchResource(OAuth20Service service, OAuth2AccessToken accessToken, String requestUrl) + throws IOException, InterruptedException, ExecutionException { + // Now let's go and ask for a protected resource! + System.out.println(); + System.out.println("Now we're going to access a protected resource..."); + final OAuthRequest request = new OAuthRequest(Verb.GET, requestUrl); + service.signRequest(accessToken, request); + try (Response response = service.execute(request)) { + System.out.println(response.getCode()); + System.out.println(response.getBody()); + } + System.out.println(); + } +} diff --git a/scribejava-core/src/main/java/com/github/scribejava/core/oauth/AccessTokenRequestParams.java b/scribejava-core/src/main/java/com/github/scribejava/core/oauth/AccessTokenRequestParams.java index 5ee5d42ef..ae46f2d8d 100644 --- a/scribejava-core/src/main/java/com/github/scribejava/core/oauth/AccessTokenRequestParams.java +++ b/scribejava-core/src/main/java/com/github/scribejava/core/oauth/AccessTokenRequestParams.java @@ -7,6 +7,7 @@ public class AccessTokenRequestParams { private final String code; private String pkceCodeVerifier; private String scope; + private String clientId; public AccessTokenRequestParams(String code) { this.code = code; @@ -21,13 +22,18 @@ public AccessTokenRequestParams pkceCodeVerifier(String pkceCodeVerifier) { return this; } + public AccessTokenRequestParams scope(ScopeBuilder scope) { + this.scope = scope.build(); + return this; + } + public AccessTokenRequestParams scope(String scope) { this.scope = scope; return this; } - public AccessTokenRequestParams scope(ScopeBuilder scope) { - this.scope = scope.build(); + public AccessTokenRequestParams clientId(String clientId) { + this.clientId = clientId; return this; } @@ -42,4 +48,8 @@ public String getPkceCodeVerifier() { public String getScope() { return scope; } + + public String getClientId() { + return clientId; + } } diff --git a/scribejava-core/src/main/java/com/github/scribejava/core/oauth/OAuth20Service.java b/scribejava-core/src/main/java/com/github/scribejava/core/oauth/OAuth20Service.java index 0e028fb3c..fd13bea55 100644 --- a/scribejava-core/src/main/java/com/github/scribejava/core/oauth/OAuth20Service.java +++ b/scribejava-core/src/main/java/com/github/scribejava/core/oauth/OAuth20Service.java @@ -140,6 +140,14 @@ protected OAuthRequest createAccessTokenRequest(AccessTokenRequestParams params) if (pkceCodeVerifier != null) { request.addParameter(PKCE.PKCE_CODE_VERIFIER_PARAM, pkceCodeVerifier); } + + final String clientId = params.getClientId(); + if (clientId != null) { + // OAuth2 token endpoint may require client id + // https://www.oauth.com/oauth2-servers/access-tokens/authorization-code-request/ + request.addParameter(OAuthConstants.CLIENT_ID, clientId); + } + logRequestWithParams("access token", request); return request; } @@ -149,7 +157,7 @@ public Future refreshAccessTokenAsync(String refreshToken) { } public Future refreshAccessTokenAsync(String refreshToken, String scope) { - return refreshAccessToken(refreshToken, scope, null); + return refreshAccessToken(refreshToken, scope, (OAuthAsyncRequestCallback) null); } public OAuth2AccessToken refreshAccessToken(String refreshToken) @@ -159,26 +167,33 @@ public OAuth2AccessToken refreshAccessToken(String refreshToken) public OAuth2AccessToken refreshAccessToken(String refreshToken, String scope) throws IOException, InterruptedException, ExecutionException { - final OAuthRequest request = createRefreshTokenRequest(refreshToken, scope); + final OAuthRequest request = createRefreshTokenRequest(refreshToken, scope, null); + + return sendAccessTokenRequestSync(request); + } + + public OAuth2AccessToken refreshAccessToken(String refreshToken, String scope, String clientId) + throws IOException, InterruptedException, ExecutionException { + final OAuthRequest request = createRefreshTokenRequest(refreshToken, scope, clientId); return sendAccessTokenRequestSync(request); } public Future refreshAccessToken(String refreshToken, OAuthAsyncRequestCallback callback) { - final OAuthRequest request = createRefreshTokenRequest(refreshToken, null); + final OAuthRequest request = createRefreshTokenRequest(refreshToken, null, null); return sendAccessTokenRequestAsync(request, callback); } public Future refreshAccessToken(String refreshToken, String scope, OAuthAsyncRequestCallback callback) { - final OAuthRequest request = createRefreshTokenRequest(refreshToken, scope); + final OAuthRequest request = createRefreshTokenRequest(refreshToken, scope, null); return sendAccessTokenRequestAsync(request, callback); } - protected OAuthRequest createRefreshTokenRequest(String refreshToken, String scope) { + protected OAuthRequest createRefreshTokenRequest(String refreshToken, String scope, String clientId) { if (refreshToken == null || refreshToken.isEmpty()) { throw new IllegalArgumentException("The refreshToken cannot be null or empty"); } @@ -195,6 +210,12 @@ protected OAuthRequest createRefreshTokenRequest(String refreshToken, String sco request.addParameter(OAuthConstants.REFRESH_TOKEN, refreshToken); request.addParameter(OAuthConstants.GRANT_TYPE, OAuthConstants.REFRESH_TOKEN); + if (clientId != null) { + // OAuth2 token endpoint may require client id + // https://www.oauth.com/oauth2-servers/access-tokens/authorization-code-request/ + request.addParameter(OAuthConstants.CLIENT_ID, clientId); + } + logRequestWithParams("refresh token", request); return request;