Skip to content

Commit

Permalink
add support for RFC 7009 OAuth 2.0 Token Revocation (thanks to https:…
Browse files Browse the repository at this point in the history
  • Loading branch information
kullfar committed Nov 14, 2017
1 parent 68919e6 commit f3cd519
Show file tree
Hide file tree
Showing 12 changed files with 215 additions and 8 deletions.
1 change: 1 addition & 0 deletions changelog
Expand Up @@ -6,6 +6,7 @@
* switch to use HTTP Basic Authorization by default in requests with need of
(2.3. Client Authentication) https://tools.ietf.org/html/rfc6749#section-2.3 Can be overrided in API class
* add support for client_credentials grant type (thanks to https://github.com/vivin)
* add support for RFC 7009 OAuth 2.0 Token Revocation (thanks to https://github.com/vivin)

[4.2.0]
* DELETE in JdkClient permits, but not requires payload (thanks to https://github.com/miguelD73)
Expand Down
Expand Up @@ -36,7 +36,7 @@ public String getAccessTokenEndpoint() {

@Override
public String getRefreshTokenEndpoint() {
throw new UnsupportedOperationException("Facebook doesn't support refershing tokens");
throw new UnsupportedOperationException("Facebook doesn't support refreshing tokens");
}

@Override
Expand Down
Expand Up @@ -32,4 +32,9 @@ protected String getAuthorizationBaseUrl() {
public TokenExtractor<OAuth2AccessToken> getAccessTokenExtractor() {
return OpenIdJsonTokenExtractor.instance();
}

@Override
public String getRevokeTokenEndpoint() {
return "https://accounts.google.com/o/oauth2/revoke";
}
}
Expand Up @@ -35,7 +35,7 @@ public static FacebookAccessTokenJsonExtractor instance() {
* ID.","type":"OAuthException","code":101,"fbtrace_id":"CvDR+X4WWIx"}}'
*/
@Override
protected void generateError(String response) {
public void generateError(String response) {
extractParameter(response, MESSAGE_REGEX_PATTERN, false);

throw new FacebookAccessTokenErrorResponse(extractParameter(response, MESSAGE_REGEX_PATTERN, false),
Expand Down
Expand Up @@ -14,7 +14,7 @@
import java.util.Map;
import java.util.concurrent.ExecutionException;

public final class Google20Example {
public class Google20Example {

private static final String NETWORK_NAME = "G+";
private static final String PROTECTED_RESOURCE_URL = "https://www.googleapis.com/plus/v1/people/me";
Expand Down
@@ -0,0 +1,104 @@
package com.github.scribejava.apis.examples;

import java.util.Random;
import java.util.Scanner;
import com.github.scribejava.core.builder.ServiceBuilder;
import com.github.scribejava.apis.GoogleApi20;
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.OAuth20Service;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutionException;

public class Google20RevokeExample {

private static final String NETWORK_NAME = "G+";
private static final String PROTECTED_RESOURCE_URL = "https://www.googleapis.com/plus/v1/people/me";

private Google20RevokeExample() {
}

public static void main(String... args) throws IOException, InterruptedException, ExecutionException {
// Replace these with your client id and secret
final String clientId = "your client id";
final String clientSecret = "your client secret";
final String secretState = "secret" + new Random().nextInt(999_999);
final OAuth20Service service = new ServiceBuilder(clientId)
.apiSecret(clientSecret)
.scope("profile") // replace with desired scope
.state(secretState)
.callback("http://example.com/callback")
.build(GoogleApi20.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...");
//pass access_type=offline to get refresh token
//https://developers.google.com/identity/protocols/OAuth2WebServer#preparing-to-start-the-oauth-20-flow
final Map<String, String> additionalParams = new HashMap<>();
additionalParams.put("access_type", "offline");
//force to reget refresh token (if usera are asked not the first time)
additionalParams.put("prompt", "consent");
final String authorizationUrl = service.getAuthorizationUrl(additionalParams);
System.out.println("Got the Authorization URL!");
System.out.println("Now go and authorize ScribeJava here:");
System.out.println(authorizationUrl);
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 'secretState'='" + secretState + "'.");
System.out.print(">>");
final String value = in.nextLine();
if (secretState.equals(value)) {
System.out.println("State value does match!");
} else {
System.out.println("Ooops, state value does not match!");
System.out.println("Expected = " + secretState);
System.out.println("Got = " + value);
System.out.println();
}

// Trade the Request Token and Verfier for the Access Token
System.out.println("Trading the Request Token for an Access Token...");
final OAuth2AccessToken accessToken = service.getAccessToken(code);
System.out.println("Got the Access Token!");
System.out.println("(if your curious it looks like this: " + accessToken
+ ", 'rawResponse'='" + accessToken.getRawResponse() + "')");

// Now let's go and ask for a protected resource!
System.out.println("Now we're going to access a protected resource...");
OAuthRequest request = new OAuthRequest(Verb.GET, PROTECTED_RESOURCE_URL);
service.signRequest(accessToken, request);
Response response = service.execute(request);
System.out.println();
System.out.println(response.getCode());
System.out.println(response.getBody());
System.out.println();

System.out.println("Revoking token...");
service.revokeToken(accessToken.getAccessToken());
System.out.println("done.");
System.out.println("After revoke we should fail requesting any data...");
//Google Note: Following a successful revocation response,
//it might take some time before the revocation has full effect.
while (response.getCode() == 200) {
Thread.sleep(1000);
request = new OAuthRequest(Verb.GET, PROTECTED_RESOURCE_URL);
service.signRequest(accessToken, request);
response = service.execute(request);
System.out.println();
System.out.println(response.getCode());
System.out.println(response.getBody());
System.out.println();
}
}
}
Expand Up @@ -54,6 +54,18 @@ public String getRefreshTokenEndpoint() {
return getAccessTokenEndpoint();
}

/**
* As stated in RFC 7009 OAuth 2.0 Token Revocation
*
* @return endpoint, which allows clients to notify the authorization server that a previously obtained refresh or
* access token is no longer needed.
* @see <a href="https://tools.ietf.org/html/rfc7009">RFC 7009</a>
*/
public String getRevokeTokenEndpoint() {
throw new UnsupportedOperationException(
"This API doesn't support revoking tokens or we have no info about this");
}

protected abstract String getAuthorizationBaseUrl();

/**
Expand Down
Expand Up @@ -40,11 +40,10 @@ public static OAuth2AccessTokenJsonExtractor instance() {
@Override
public OAuth2AccessToken extract(Response response) throws IOException {
final String body = response.getBody();
Preconditions.checkEmptyString(body,
"Response body is incorrect. Can't extract a token from an empty string");
Preconditions.checkEmptyString(body, "Response body is incorrect. Can't extract a token from an empty string");

if (response.getCode() != 200) {
generateError(response.getBody());
generateError(body);
}
return createToken(body);
}
Expand All @@ -54,7 +53,7 @@ public OAuth2AccessToken extract(Response response) throws IOException {
*
* @param response response
*/
protected void generateError(String response) {
public void generateError(String response) {
final String errorInString = extractParameter(response, ERROR_REGEX_PATTERN, true);
final String errorDescription = extractParameter(response, ERROR_DESCRIPTION_REGEX_PATTERN, false);
final String errorUriInString = extractParameter(response, ERROR_URI_REGEX_PATTERN, false);
Expand Down
Expand Up @@ -12,7 +12,11 @@ public class OAuth2AccessTokenErrorResponse extends OAuthException {
private static final long serialVersionUID = 2309424849700276816L;

public enum ErrorCode {
invalid_request, invalid_client, invalid_grant, unauthorized_client, unsupported_grant_type, invalid_scope
invalid_request, invalid_client, invalid_grant, unauthorized_client, unsupported_grant_type, invalid_scope,
/**
* @see <a href="https://tools.ietf.org/html/rfc7009#section-2.2.1">RFC 7009, 2.2.1. Error Response</a>
*/
unsupported_token_type
}

private final ErrorCode errorCode;
Expand Down
Expand Up @@ -3,18 +3,22 @@
import java.io.IOException;
import java.util.concurrent.Future;
import com.github.scribejava.core.builder.api.DefaultApi20;
import com.github.scribejava.core.extractors.OAuth2AccessTokenJsonExtractor;
import com.github.scribejava.core.model.OAuth2AccessToken;
import com.github.scribejava.core.model.OAuth2Authorization;
import com.github.scribejava.core.model.OAuthAsyncRequestCallback;
import com.github.scribejava.core.model.OAuthConfig;
import com.github.scribejava.core.model.OAuthConstants;
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.pkce.AuthorizationUrlWithPKCE;
import com.github.scribejava.core.pkce.PKCE;
import com.github.scribejava.core.pkce.PKCEService;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import revoke.TokenTypeHint;

public class OAuth20Service extends OAuthService<OAuth2AccessToken> {

Expand Down Expand Up @@ -291,6 +295,57 @@ public DefaultApi20 getApi() {
return api;
}

protected OAuthRequest createRevokeTokenRequest(String tokenToRevoke, TokenTypeHint tokenTypeHint) {
final OAuthRequest request = new OAuthRequest(Verb.POST, api.getRevokeTokenEndpoint());

api.getClientAuthenticationType().addClientAuthentication(request, getConfig());

request.addParameter("token", tokenToRevoke);
if (tokenTypeHint != null) {
request.addParameter("token_type_hint", tokenTypeHint.toString());
}
return request;
}

public final Future<Void> revokeTokenAsync(String tokenToRevoke) {
return revokeTokenAsync(tokenToRevoke, null);
}

public final Future<Void> revokeTokenAsync(String tokenToRevoke, TokenTypeHint tokenTypeHint) {
return revokeToken(tokenToRevoke, null, tokenTypeHint);
}

public final void revokeToken(String tokenToRevoke) throws IOException, InterruptedException, ExecutionException {
revokeToken(tokenToRevoke, (TokenTypeHint) null);
}

public final void revokeToken(String tokenToRevoke, TokenTypeHint tokenTypeHint)
throws IOException, InterruptedException, ExecutionException {
final OAuthRequest request = createRevokeTokenRequest(tokenToRevoke, tokenTypeHint);

checkForErrorRevokeToken(execute(request));
}

public final Future<Void> revokeToken(String tokenToRevoke, OAuthAsyncRequestCallback<Void> callback) {
return revokeToken(tokenToRevoke, callback, null);
}

public final Future<Void> revokeToken(String tokenToRevoke, OAuthAsyncRequestCallback<Void> callback,
TokenTypeHint tokenTypeHint) {
final OAuthRequest request = createRevokeTokenRequest(tokenToRevoke, tokenTypeHint);

return execute(request, callback, response -> {
checkForErrorRevokeToken(response);
return null;
});
}

private void checkForErrorRevokeToken(Response response) throws IOException {
if (response.getCode() != 200) {
OAuth2AccessTokenJsonExtractor.instance().generateError(response.getBody());
}
}

public OAuth2Authorization extractAuthorization(String redirectLocation) {
final OAuth2Authorization authorization = new OAuth2Authorization();
int end = redirectLocation.indexOf('#');
Expand Down
@@ -0,0 +1,15 @@
package revoke;

import com.github.scribejava.core.extractors.OAuth2AccessTokenJsonExtractor;
import com.github.scribejava.core.model.Response;
import java.io.IOException;

public class OAuth2RevokeTokenResponseConverter {

public Void convert(Response response) throws IOException {
if (response.getCode() != 200) {
OAuth2AccessTokenJsonExtractor.instance().generateError(response.getBody());
}
return null;
}
}
12 changes: 12 additions & 0 deletions scribejava-core/src/main/java/revoke/TokenTypeHint.java
@@ -0,0 +1,12 @@
package revoke;

/**
*
* as stated in RFC 7009 <br>
* 2.1. Revocation Request
*
* @see <a href="https://tools.ietf.org/html/rfc7009#section-2.1">RFC 7009, 2.1. Revocation Request</a>
*/
public enum TokenTypeHint {
access_token, refresh_token
}

0 comments on commit f3cd519

Please sign in to comment.