diff options
author | Jesse Morgan <jesse@jesterpm.net> | 2013-08-27 08:28:16 -0700 |
---|---|---|
committer | Jesse Morgan <jesse@jesterpm.net> | 2013-08-27 08:28:16 -0700 |
commit | 1cdb43bb3e432040aed18c05e129f0131ee7d20a (patch) | |
tree | a4c5ad41d183b3874c990de0c5416d1810a1dc85 /src/com/p4square/restlet/oauth | |
parent | 9b33aaf27cd8f73402ee9967c6b0fd76a90f8ebe (diff) |
Introducing F1 Authentication and Adding Site Content.
This change introduced the f1oauth and jesterpm oauth packages for
interacting with Fellowship One's developer API. I have also reworked
the login authentication to verify credentials through F1 and added
session management to track logged in users.
The Authenticator chain works as follows: on every page load we check
for a session cookie, if the cookie exists, the Request is marked as
authenticated and the OAuthUser object is restored in ClientInfo. If
this request is going to an account page, we require authentication. The
LoginFormAuthenticator checks if the user is already authenticated (via
cookie) and if not redirects the user to the login page. When the login
form is submitted, LoginFormAuthenticator catches the POST request and
authenticates the user through F1.
I'm also adding a new account page, but it is currently a work in
progress.
This commit also adds Allen's content to the site.
Diffstat (limited to 'src/com/p4square/restlet/oauth')
-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); + } +} |