diff --git a/changelog.txt b/changelog.txt index c5cab0d..4bd9c24 100644 --- a/changelog.txt +++ b/changelog.txt @@ -29,4 +29,5 @@ * FIX: Removed isEmpty() from OAuth2.0 code in order to support older JVMs * FEATURE: Overloaded provider() method to support instances as well as classes (thanks dustismo) - * FIX: White-space parameter bug (thanks Krogoth) \ No newline at end of file + * FIX: White-space parameter bug (thanks Krogoth) + * FEATURE: OAuth 2.0 now implements server-flow (thanks Diego Silveira) \ No newline at end of file diff --git a/src/main/java/org/scribe/builder/api/DefaultApi20.java b/src/main/java/org/scribe/builder/api/DefaultApi20.java index 6e03567..7e3eb9b 100644 --- a/src/main/java/org/scribe/builder/api/DefaultApi20.java +++ b/src/main/java/org/scribe/builder/api/DefaultApi20.java @@ -1,5 +1,6 @@ package org.scribe.builder.api; +import org.scribe.extractors.*; import org.scribe.model.*; import org.scribe.oauth.*; @@ -22,6 +23,33 @@ import org.scribe.oauth.*; public abstract class DefaultApi20 implements Api { + /** + * Returns the access token extractor. + * + * @return access token extractor + */ + public AccessTokenExtractor getAccessTokenExtractor() + { + return new TokenExtractor20Impl(); + } + + /** + * Returns the verb for the access token endpoint (defaults to GET) + * + * @return access token endpoint verb + */ + public Verb getAccessTokenVerb() + { + return Verb.GET; + } + + /** + * Returns the URL that receives the access token requests. + * + * @return access token URL + */ + public abstract String getAccessTokenEndpoint(); + /** * Returns the URL where you should redirect your users to authenticate * your application. diff --git a/src/main/java/org/scribe/builder/api/FacebookApi.java b/src/main/java/org/scribe/builder/api/FacebookApi.java index bcda56e..7d4ae5b 100644 --- a/src/main/java/org/scribe/builder/api/FacebookApi.java +++ b/src/main/java/org/scribe/builder/api/FacebookApi.java @@ -5,8 +5,14 @@ import org.scribe.utils.*; public class FacebookApi extends DefaultApi20 { - private static final String AUTHORIZE_URL = "https://graph.facebook.com/oauth/authorize?response_type=token&client_id=%s&redirect_uri=%s"; + private static final String AUTHORIZE_URL = "https://www.facebook.com/dialog/oauth?client_id=%s&redirect_uri=%s"; + @Override + public String getAccessTokenEndpoint() + { + return "https://graph.facebook.com/oauth/access_token"; + } + @Override public String getAuthorizationUrl(OAuthConfig config) { diff --git a/src/main/java/org/scribe/extractors/TokenExtractor20Impl.java b/src/main/java/org/scribe/extractors/TokenExtractor20Impl.java new file mode 100644 index 0000000..8c956c5 --- /dev/null +++ b/src/main/java/org/scribe/extractors/TokenExtractor20Impl.java @@ -0,0 +1,35 @@ +package org.scribe.extractors; + +import java.util.regex.*; + +import org.scribe.exceptions.*; +import org.scribe.model.*; +import org.scribe.utils.*; + +/** + * Default implementation of {@AccessTokenExtractor}. Conforms to OAuth 2.0 + * + */ +public class TokenExtractor20Impl implements AccessTokenExtractor +{ + private static final String TOKEN_REGEX = "access_token=(\\S*?)(&(\\S*))?"; + private static final String EMPTY_SECRET = ""; + + /** + * {@inheritDoc} + */ + public Token extract(String response) + { + Preconditions.checkEmptyString(response, "Response body is incorrect. Can't extract a token from an empty string"); + + Matcher matcher = Pattern.compile(TOKEN_REGEX).matcher(response); + if (matcher.matches()) + { + String token = URLUtils.percentDecode(matcher.group(1)); + return new Token(token, EMPTY_SECRET); + } else + { + throw new OAuthException("Response body is incorrect. Can't extract a token from this: '" + response + "'", null); + } + } +} diff --git a/src/main/java/org/scribe/model/OAuthConstants.java b/src/main/java/org/scribe/model/OAuthConstants.java index 01b98d3..c93c31c 100644 --- a/src/main/java/org/scribe/model/OAuthConstants.java +++ b/src/main/java/org/scribe/model/OAuthConstants.java @@ -41,4 +41,9 @@ public class OAuthConstants //OAuth 2.0 public static final String ACCESS_TOKEN = "access_token"; + public static final String CLIENT_ID = "client_id"; + public static final String CLIENT_SECRET = "client_secret"; + public static final String REDIRECT_URI = "redirect_uri"; + public static final String CODE = "code"; + } diff --git a/src/main/java/org/scribe/oauth/OAuth20ServiceImpl.java b/src/main/java/org/scribe/oauth/OAuth20ServiceImpl.java index a81dc88..94b0cbe 100644 --- a/src/main/java/org/scribe/oauth/OAuth20ServiceImpl.java +++ b/src/main/java/org/scribe/oauth/OAuth20ServiceImpl.java @@ -35,7 +35,13 @@ public class OAuth20ServiceImpl implements OAuthService */ public Token getAccessToken(Token requestToken, Verifier verifier) { - throw new UnsupportedOperationException("Unsupported operation, please use 'getAuthorizationUrl' and redirect your users there"); + OAuthRequest request = new OAuthRequest(api.getAccessTokenVerb(), api.getAccessTokenEndpoint()); + request.addQuerystringParameter(OAuthConstants.CLIENT_ID, config.getApiKey()); + request.addQuerystringParameter(OAuthConstants.CLIENT_SECRET, config.getApiSecret()); + request.addQuerystringParameter(OAuthConstants.CODE, verifier.getValue()); + request.addQuerystringParameter(OAuthConstants.REDIRECT_URI, config.getCallback()); + Response response = request.send(); + return api.getAccessTokenExtractor().extract(response.getBody()); } /** diff --git a/src/test/java/org/scribe/examples/FacebookExample.java b/src/test/java/org/scribe/examples/FacebookExample.java index 74e8ed0..d5dc3cb 100644 --- a/src/test/java/org/scribe/examples/FacebookExample.java +++ b/src/test/java/org/scribe/examples/FacebookExample.java @@ -11,13 +11,12 @@ public class FacebookExample { private static final String NETWORK_NAME = "Facebook"; private static final String PROTECTED_RESOURCE_URL = "https://graph.facebook.com/me"; - private static final String NO_SECRET_NEEDED = ""; private static final Token EMPTY_TOKEN = null; public static void main(String[] args) { // Replace these with your own api key and secret - String apiKey = "your_api_key"; + String apiKey = "your_app_id"; String apiSecret = "your_api_secret"; OAuthService service = new ServiceBuilder() .provider(FacebookApi.class) @@ -36,9 +35,16 @@ public class FacebookExample System.out.println("Got the Authorization URL!"); System.out.println("Now go and authorize Scribe here:"); System.out.println(authorizationUrl); - System.out.println("And paste the access token here"); + System.out.println("And paste the authorization code here"); System.out.print(">>"); - Token accessToken = new Token(in.nextLine(), NO_SECRET_NEEDED); + Verifier verifier = new Verifier(in.nextLine()); + System.out.println(); + + // Trade the Request Token and Verfier for the Access Token + System.out.println("Trading the Request Token for an Access Token..."); + Token accessToken = service.getAccessToken(EMPTY_TOKEN, verifier); + System.out.println("Got the Access Token!"); + System.out.println("(if your curious it looks like this: " + accessToken + " )"); System.out.println(); // Now let's go and ask for a protected resource! diff --git a/src/test/java/org/scribe/extractors/TokenExtractor20Test.java b/src/test/java/org/scribe/extractors/TokenExtractor20Test.java new file mode 100644 index 0000000..17f1a01 --- /dev/null +++ b/src/test/java/org/scribe/extractors/TokenExtractor20Test.java @@ -0,0 +1,58 @@ +package org.scribe.extractors; + +import static org.junit.Assert.*; + +import org.junit.*; +import org.scribe.exceptions.*; +import org.scribe.model.*; + +public class TokenExtractor20Test +{ + + private TokenExtractor20Impl extractor; + + @Before + public void setup() + { + extractor = new TokenExtractor20Impl(); + } + + @Test + public void shouldExtractTokenFromOAuthStandardResponse() + { + String response = "access_token=166942940015970|2.2ltzWXYNDjCtg5ZDVVJJeg__.3600.1295816400-548517159|RsXNdKrpxg8L6QNLWcs2TVTmcaE"; + Token extracted = extractor.extract(response); + assertEquals("166942940015970|2.2ltzWXYNDjCtg5ZDVVJJeg__.3600.1295816400-548517159|RsXNdKrpxg8L6QNLWcs2TVTmcaE", extracted.getToken()); + assertEquals("", extracted.getSecret()); + } + + @Test + public void shouldExtractTokenFromResponseWithExpiresParam() + { + String response = "access_token=166942940015970|2.2ltzWXYNDjCtg5ZDVVJJeg__.3600.1295816400-548517159|RsXNdKrpxg8L6QNLWcs2TVTmcaE&expires=5108"; + Token extracted = extractor.extract(response); + assertEquals("166942940015970|2.2ltzWXYNDjCtg5ZDVVJJeg__.3600.1295816400-548517159|RsXNdKrpxg8L6QNLWcs2TVTmcaE", extracted.getToken()); + assertEquals("", extracted.getSecret()); + } + + @Test(expected = OAuthException.class) + public void shouldThrowExceptionIfTokenIsAbsent() + { + String response = "&expires=5108"; + extractor.extract(response); + } + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowExceptionIfResponseIsNull() + { + String response = null; + extractor.extract(response); + } + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowExceptionIfResponseIsEmptyString() + { + String response = ""; + extractor.extract(response); + } +}