diff options
Diffstat (limited to 'src/com/p4square/restlet')
-rw-r--r-- | src/com/p4square/restlet/oauth/OAuthAuthenticator.java | 95 | ||||
-rw-r--r-- | src/com/p4square/restlet/oauth/OAuthAuthenticatorHelper.java | 177 | ||||
-rw-r--r-- | src/com/p4square/restlet/oauth/OAuthException.java | 25 | ||||
-rw-r--r-- | src/com/p4square/restlet/oauth/OAuthHelper.java | 143 | ||||
-rw-r--r-- | src/com/p4square/restlet/oauth/OAuthUser.java | 50 | ||||
-rw-r--r-- | src/com/p4square/restlet/oauth/Token.java | 52 |
6 files changed, 542 insertions, 0 deletions
diff --git a/src/com/p4square/restlet/oauth/OAuthAuthenticator.java b/src/com/p4square/restlet/oauth/OAuthAuthenticator.java new file mode 100644 index 0000000..c33bb5a --- /dev/null +++ b/src/com/p4square/restlet/oauth/OAuthAuthenticator.java @@ -0,0 +1,95 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.restlet.oauth; + +import org.apache.log4j.Logger; + +import org.restlet.Context; +import org.restlet.Request; +import org.restlet.Response; +import org.restlet.security.Authenticator; +import org.restlet.security.User; + +/** + * Authenticator which makes an OAuth request to authenticate the user. + * + * If this Authenticator is made optional than no requests are made to the + * service provider. + * + * @author Jesse Morgan <jesse@jesterpm.net> + */ +public class OAuthAuthenticator extends Authenticator { + private static Logger LOG = Logger.getLogger(OAuthAuthenticator.class); + + private static final String OAUTH_TOKEN = "oauth_token"; + private static final String COOKIE_NAME = "oauth_secret"; + + private final OAuthHelper mHelper; + + /** + * Create a new Authenticator. + * + * @param Context the current context. + * @param optional If true, unauthenticated users are allowed to continue. + * @param helper The OAuthHelper which will help with the requests. + */ + public OAuthAuthenticator(Context context, boolean optional, OAuthHelper helper) { + super(context, false, optional, null); + + mHelper = helper; + } + + protected boolean authenticate(Request request, Response response) { + /* + * The authentication workflow has three steps: + * 1. Get RequestToken + * 2. Authenticate the user + * 3. Get AccessToken + * + * The authentication workflow is broken into two stages. In the first, + * we generate the RequestToken (step 1) and redirect the user to the + * authentication page. When the user comes back, we will request the + * AccessToken (step 2). + * + * We determine which half we are in by the presence of the oauth_token + * parameter in the query string. + */ + + final String token = request.getResourceRef().getQueryAsForm().getFirstValue(OAUTH_TOKEN); + final String secret = request.getCookies().getFirstValue(COOKIE_NAME); + + try { + if (token == null) { + if (isOptional()) { + return false; + } + + // 1. Get RequestToken + Token requestToken = mHelper.getRequestToken(); + + if (requestToken == null) { + return false; + } + + // 2. Redirect user + // TODO Encrypt cookie + response.getCookieSettings().add(COOKIE_NAME, requestToken.getSecret()); + response.redirectSeeOther(mHelper.getLoginUrl(requestToken, request.getResourceRef().toString())); + return false; + + } else { + // 3. Get AccessToken + Token requestToken = new Token(token, secret); + User user = mHelper.getAccessToken(requestToken); + request.getClientInfo().setUser(user); + return true; + } + + } catch (OAuthException e) { + LOG.debug("Authentication failed: " + e); + return false; + } + } +} diff --git a/src/com/p4square/restlet/oauth/OAuthAuthenticatorHelper.java b/src/com/p4square/restlet/oauth/OAuthAuthenticatorHelper.java new file mode 100644 index 0000000..76ff044 --- /dev/null +++ b/src/com/p4square/restlet/oauth/OAuthAuthenticatorHelper.java @@ -0,0 +1,177 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.restlet.oauth; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; + +import java.net.URLEncoder; + +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + +import java.util.Collections; +import java.util.Random; + +import javax.crypto.Mac; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +import org.restlet.Request; +import org.restlet.Response; +import org.restlet.data.ChallengeRequest; +import org.restlet.data.ChallengeResponse; +import org.restlet.data.ChallengeScheme; +import org.restlet.data.CharacterSet; +import org.restlet.data.Form; +import org.restlet.data.Method; +import org.restlet.data.Parameter; +import org.restlet.data.Reference; +import org.restlet.engine.header.ChallengeWriter; +import org.restlet.engine.header.Header; +import org.restlet.engine.security.AuthenticatorHelper; +import org.restlet.engine.util.Base64; +import org.restlet.util.Series; + +/** + * Authentication helper for signing OAuth Requests. + * + * This implementation is limited to one consumer token/secret per restlet + * engine. In practice this means you will only be able to interact with one + * service provider unless you loaded/unloaded the AuthenticationHelper for + * each request. + * + * @author Jesse Morgan <jesse@jesterpm.net> + */ +public class OAuthAuthenticatorHelper extends AuthenticatorHelper { + private static final String SIGNATURE_METHOD = "HMAC-SHA1"; + private static final String JAVA_SIGNATURE_METHOD = "HmacSHA1"; + private static final String ENCODING = "UTF-8"; + + private final Random mRandom; + private final Token mConsumerToken; + + /** + * Package-private constructor. + * + * This class should only be instantiated by OAuthHelper. + */ + OAuthAuthenticatorHelper(Token consumerToken) { + super(ChallengeScheme.HTTP_OAUTH, true, false); + + mRandom = new Random(); + mConsumerToken = consumerToken; + } + + @Override + public void formatRequest(ChallengeWriter cw, ChallengeRequest cr, + Response response, Series<Header> httpHeaders) throws IOException { + + throw new UnsupportedOperationException("OAuth Requests are not implemented"); + } + + @Override + public void formatResponse(ChallengeWriter cw, ChallengeResponse response, + Request request, Series<Header> httpHeaders) { + + try { + Series<Parameter> authParams = new Series<Parameter>(Parameter.class); + + String nonce = String.valueOf(mRandom.nextInt()); + String timestamp = String.valueOf(System.currentTimeMillis() / 1000); + + authParams.add(new Parameter("oauth_consumer_key", mConsumerToken.getToken())); + authParams.add(new Parameter("oauth_nonce", nonce)); + authParams.add(new Parameter("oauth_signature_method", SIGNATURE_METHOD)); + authParams.add(new Parameter("oauth_timestamp", timestamp)); + authParams.add(new Parameter("oauth_version", "1.0")); + + String accessToken = response.getIdentifier(); + if (accessToken != null) { + authParams.add(new Parameter("oauth_token", accessToken)); + } + + // Generate Signature + String signature = generateSignature(response, request, authParams); + authParams.add(new Parameter("oauth_signature", signature)); + + // Write Header + for (Parameter p : authParams) { + cw.appendQuotedChallengeParameter(encode(p.getName()), encode(p.getValue())); + } + + } catch (IOException e) { + throw new RuntimeException(e); + + } catch (InvalidKeyException e) { + throw new RuntimeException(e); + + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + /** + * Helper method to generate an OAuth Signature. + */ + private String generateSignature(ChallengeResponse response, Request request, + Series<Parameter> authParams) + throws NoSuchAlgorithmException, InvalidKeyException, IOException, + UnsupportedEncodingException { + + // HTTP Request Method + String httpMethod = request.getMethod().getName(); + + // Request Url + Reference url = request.getResourceRef(); + String requestUrl = encode(url.getScheme() + ":" + url.getHierarchicalPart()); + + // Normalized parameters + Series<Parameter> params = new Series<Parameter>(Parameter.class); + + // OAUTH Params + params.addAll(authParams); + + // Query Params + Form query = url.getQueryAsForm(); + params.addAll(query); + + // Sort it + Collections.sort(params); + + StringBuilder normalizedParamsBuilder = new StringBuilder(); + for (Parameter p : params) { + normalizedParamsBuilder.append('&'); + normalizedParamsBuilder.append(p.encode(CharacterSet.UTF_8)); + } + String normalizedParams = encode(normalizedParamsBuilder.substring(1)); // remove the first & + + // Generate signature base + String sigBase = httpMethod + "&" + requestUrl + "&" + normalizedParams.toString(); + + // Sign the signature base + Mac mac = Mac.getInstance(JAVA_SIGNATURE_METHOD); + + String accessTokenSecret = ""; + if (response.getIdentifier() != null) { + accessTokenSecret = new String(response.getSecret()); + } + + byte[] keyBytes = (encode(mConsumerToken.getSecret()) + "&" + encode(accessTokenSecret)).getBytes(ENCODING); + SecretKey key = new SecretKeySpec(keyBytes, JAVA_SIGNATURE_METHOD); + mac.init(key); + + byte[] signature = mac.doFinal(sigBase.getBytes(ENCODING)); + + return Base64.encode(signature, false).trim(); + } + + /** + * Helper method to URL Encode Strings. + */ + private String encode(String input) throws UnsupportedEncodingException { + return URLEncoder.encode(input, ENCODING); + } +} diff --git a/src/com/p4square/restlet/oauth/OAuthException.java b/src/com/p4square/restlet/oauth/OAuthException.java new file mode 100644 index 0000000..dd326d3 --- /dev/null +++ b/src/com/p4square/restlet/oauth/OAuthException.java @@ -0,0 +1,25 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.restlet.oauth; + +import org.restlet.data.Status; + +/** + * Exception throw when the service provider returns an error. + * + * @author Jesse Morgan <jesse@jesterpm.net> + */ +public class OAuthException extends Exception { + private final Status mStatus; + + public OAuthException(Status status) { + super("Service provider failed request: " + status.getDescription()); + mStatus = status; + } + + public Status getStatus() { + return mStatus; + } +} diff --git a/src/com/p4square/restlet/oauth/OAuthHelper.java b/src/com/p4square/restlet/oauth/OAuthHelper.java new file mode 100644 index 0000000..544e4e3 --- /dev/null +++ b/src/com/p4square/restlet/oauth/OAuthHelper.java @@ -0,0 +1,143 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.restlet.oauth; + +import java.net.URLEncoder; + +import org.restlet.Context; +import org.restlet.Request; +import org.restlet.Response; +import org.restlet.Restlet; +import org.restlet.data.ChallengeResponse; +import org.restlet.data.ChallengeScheme; +import org.restlet.data.Form; +import org.restlet.data.Method; +import org.restlet.data.Reference; +import org.restlet.data.Status; +import org.restlet.engine.Engine; + +/** + * Helper Class for OAuth 1.0 Authentication. + * + * @author Jesse Morgan <jesse@jesterpm.net> + */ +public abstract class OAuthHelper { + private final Restlet mDispatcher; + private final Token mConsumerToken; + + /** + * Create a new OAuth Helper. + * As currently implemented, there can only be one OAuthHelper per Restlet + * Engine since this class registers its own provider for the OAuth + * authentication protocol. + * + * FIXME: This could be improved by making OAuthAuthenticationHelper and + * maybe Token aware of multiple service providers. + * + * @param context The restlet context which provides a ClientDispatcher. + * @param consumerKey The OAuth consumer key for this application. + * @param consumerSecret the OAuth consumer secret for this application. + */ + public OAuthHelper(Context context, String consumerKey, String consumerSecret) { + mDispatcher = context.getClientDispatcher(); + mConsumerToken = new Token(consumerKey, consumerSecret); + + Engine.getInstance().getRegisteredAuthenticators().add(new OAuthAuthenticatorHelper(mConsumerToken)); + } + + /** + * @return the URL for the initial RequestToken request. + */ + protected abstract String getRequestTokenUrl(); + + /** + * Request a RequestToken. + * + * @return a Token containing the RequestToken. + * @throws OAuthException if the request fails. + */ + public Token getRequestToken() throws OAuthException { + Request request = new Request(Method.GET, getRequestTokenUrl()); + request.setChallengeResponse(new ChallengeResponse(ChallengeScheme.HTTP_OAUTH)); + + Response response = mDispatcher.handle(request); + + return processTokenRequest(response); + } + + /** + * @return the URL to redirect the user to for Authentication. + */ + public abstract String getLoginUrl(Token requestToken, String callback); + + /** + * @return the URL for the AccessToken request. + */ + protected abstract String getAccessTokenUrl(); + + /** + * Request an AccessToken for a previously authenticated RequestToken. + * + * @return an OAuthUser object containing the AccessToken. + * @throws OAuthException if the request fails. + */ + public OAuthUser getAccessToken(Token requestToken) throws OAuthException { + Request request = new Request(Method.GET, getAccessTokenUrl()); + request.setChallengeResponse(requestToken.getChallengeResponse()); + + return processAccessTokenRequest(request); + } + + /** + * Helper method to decode the token returned from an OAuth Request. + * + * @param response The Response object from the Request. + * @return the Token from the oauth_token and oauth_token_secret parameters. + * @throws OAuthException is the server reported an error. + */ + protected Token processTokenRequest(Response response) throws OAuthException { + Status status = response.getStatus(); + + if (status.isSuccess()) { + Form form = new Form(response.getEntity()); + String token = form.getFirstValue("oauth_token"); + String secret = form.getFirstValue("oauth_token_secret"); + + return new Token(token, secret); + + } else { + throw new OAuthException(status); + } + } + + /** + * Helper method to create an OAuthUser from the AccessToken request. + * + * The User's identifier is set to the Content-Location header, if present. + * + * @param response The Response to the AccessToken Request. + * @return An OAuthUser object wrapping the AccessToken. + * @throws OAuthException if the request failed. + */ + protected OAuthUser processAccessTokenRequest(Request request) throws OAuthException { + Response response = getResponse(request); + Token accessToken = processTokenRequest(response); + + Reference ref = response.getEntity().getLocationRef(); + if (ref != null) { + return new OAuthUser(ref.toString(), accessToken); + + } else { + return new OAuthUser(accessToken); + } + } + + /** + * Helper method to get a Response for a Request. + */ + protected Response getResponse(Request request) { + return mDispatcher.handle(request); + } +} diff --git a/src/com/p4square/restlet/oauth/OAuthUser.java b/src/com/p4square/restlet/oauth/OAuthUser.java new file mode 100644 index 0000000..11dbac1 --- /dev/null +++ b/src/com/p4square/restlet/oauth/OAuthUser.java @@ -0,0 +1,50 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.restlet.oauth; + +import org.restlet.data.ChallengeResponse; +import org.restlet.security.User; + +/** + * Simple User object which also contains an OAuth AccessToken. + * + * @author Jesse Morgan <jesse@jesterpm.net> + */ +public class OAuthUser extends User { + private final Token mToken; + private final String mContentLocation; + + public OAuthUser(Token token) { + this(null, token); + } + + public OAuthUser(String location, Token token) { + super(); + mToken = token; + mContentLocation = location; + } + + /** + * @return the Location associated with the user. + */ + public String getLocation() { + return mContentLocation; + } + + /** + * @return The AccessToken. + */ + public Token getToken() { + return mToken; + } + + /** + * Convenience method for getToken().getChallengeResponse(). + * @return A ChallengeResponse based upon the access token. + */ + public ChallengeResponse getChallengeResponse() { + return mToken.getChallengeResponse(); + } +} diff --git a/src/com/p4square/restlet/oauth/Token.java b/src/com/p4square/restlet/oauth/Token.java new file mode 100644 index 0000000..51a9087 --- /dev/null +++ b/src/com/p4square/restlet/oauth/Token.java @@ -0,0 +1,52 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.restlet.oauth; + +import org.restlet.data.ChallengeResponse; +import org.restlet.data.ChallengeScheme; + +/** + * Token wraps the two Strings which make up an OAuth Token: the public + * component and the private component. + * + * @author Jesse Morgan <jesse@jesterpm.net> + */ +public class Token { + private final String mToken; + private final String mSecret; + + public Token(String token, String secret) { + mToken = token; + mSecret = secret; + } + + /** + * @return the public component. + */ + public String getToken() { + return mToken; + } + + /** + * @return the secret component. + */ + public String getSecret() { + return mSecret; + } + + @Override + public String toString() { + return mToken + "&" + mSecret; + } + + /** + * Generate a ChallengeResponse based on this Token. + * + * @return a ChallengeResponse object using the OAUTH ChallengeScheme. + */ + public ChallengeResponse getChallengeResponse() { + return new ChallengeResponse(ChallengeScheme.HTTP_OAUTH, mToken, mSecret); + } +} |