diff options
author | Jesse Morgan <jesse@jesterpm.net> | 2016-04-09 15:53:24 -0700 |
---|---|---|
committer | Jesse Morgan <jesse@jesterpm.net> | 2016-04-09 15:53:24 -0700 |
commit | 371ccae3d1f31ec38f4af77fb7fcd175d49b3cd5 (patch) | |
tree | 38c4f1e8828f9af9c4b77a173bee0d312b321698 /src/main/java/com/p4square/restlet | |
parent | bbf907e51dfcf157bdee24dead1d531122aa25db (diff) | |
parent | 3102d8bce3426d9cf41aeaf201c360d342677770 (diff) |
Merge pull request #10 from PuyallupFoursquare/maven
Switching from Ivy+Ant to Maven.
Diffstat (limited to 'src/main/java/com/p4square/restlet')
9 files changed, 684 insertions, 0 deletions
diff --git a/src/main/java/com/p4square/restlet/metrics/MetricRouter.java b/src/main/java/com/p4square/restlet/metrics/MetricRouter.java new file mode 100644 index 0000000..d4da270 --- /dev/null +++ b/src/main/java/com/p4square/restlet/metrics/MetricRouter.java @@ -0,0 +1,61 @@ +/* + * Copyright 2014 Jesse Morgan + */ + +package com.p4square.restlet.metrics; + +import com.codahale.metrics.Counter; +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.Timer; + +import org.restlet.Context; +import org.restlet.Request; +import org.restlet.Response; +import org.restlet.Restlet; +import org.restlet.routing.TemplateRoute; +import org.restlet.routing.Router; + +/** + * + * @author Jesse Morgan <jesse@jesterpm.net> + */ +public class MetricRouter extends Router { + + private final MetricRegistry mMetricRegistry; + + public MetricRouter(Context context, MetricRegistry metrics) { + super(context); + mMetricRegistry = metrics; + } + + @Override + protected void doHandle(Restlet next, Request request, Response response) { + String baseName; + if (next instanceof TemplateRoute) { + TemplateRoute temp = (TemplateRoute) next; + baseName = MetricRegistry.name("MetricRouter", temp.getTemplate().getPattern()); + } else { + baseName = MetricRegistry.name("MetricRouter", "unknown"); + } + + final Timer.Context aggTimer = mMetricRegistry.timer("MetricRouter.time").time(); + final Timer.Context timer = mMetricRegistry.timer(baseName + ".time").time(); + + try { + super.doHandle(next, request, response); + } finally { + timer.stop(); + aggTimer.stop(); + + // Record status code + boolean success = !response.getStatus().isError(); + if (success) { + mMetricRegistry.counter("MetricRouter.success").inc(); + mMetricRegistry.counter(baseName + ".response.success").inc(); + } else { + mMetricRegistry.counter("MetricRouter.failure").inc(); + mMetricRegistry.counter(baseName + ".response.failure").inc(); + } + } + } +} diff --git a/src/main/java/com/p4square/restlet/metrics/MetricsApplication.java b/src/main/java/com/p4square/restlet/metrics/MetricsApplication.java new file mode 100644 index 0000000..6caf742 --- /dev/null +++ b/src/main/java/com/p4square/restlet/metrics/MetricsApplication.java @@ -0,0 +1,43 @@ +/* + * Copyright 2014 Jesse Morgan + */ + +package com.p4square.restlet.metrics; + +import java.util.concurrent.TimeUnit; + +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.json.MetricsModule; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.restlet.Application; +import org.restlet.Restlet; +import org.restlet.resource.Finder; + +/** + * + * @author Jesse Morgan <jesse@jesterpm.net> + */ +public class MetricsApplication extends Application { + static final ObjectMapper MAPPER; + static { + MAPPER = new ObjectMapper(); + MAPPER.registerModule(new MetricsModule(TimeUnit.SECONDS, TimeUnit.MILLISECONDS, true)); + } + + private final MetricRegistry mMetricRegistry; + + public MetricsApplication(MetricRegistry metrics) { + mMetricRegistry = metrics; + } + + public MetricRegistry getMetricRegistry() { + return mMetricRegistry; + } + + @Override + public Restlet createInboundRoot() { + return new Finder(getContext(), MetricsResource.class); + } +} diff --git a/src/main/java/com/p4square/restlet/metrics/MetricsResource.java b/src/main/java/com/p4square/restlet/metrics/MetricsResource.java new file mode 100644 index 0000000..e2ab14d --- /dev/null +++ b/src/main/java/com/p4square/restlet/metrics/MetricsResource.java @@ -0,0 +1,32 @@ +/* + * Copyright 2014 Jesse Morgan + */ + +package com.p4square.restlet.metrics; + +import com.codahale.metrics.MetricRegistry; + +import org.restlet.ext.jackson.JacksonRepresentation; +import org.restlet.representation.Representation; +import org.restlet.resource.ServerResource; + +/** + * + * @author Jesse Morgan <jesse@jesterpm.net> + */ +public class MetricsResource extends ServerResource { + + private MetricRegistry mMetricRegistry; + + @Override + public void doInit() { + mMetricRegistry = ((MetricsApplication) getApplication()).getMetricRegistry(); + } + + @Override + protected Representation get() { + JacksonRepresentation<MetricRegistry> rep = new JacksonRepresentation<>(mMetricRegistry); + rep.setObjectMapper(MetricsApplication.MAPPER); + return rep; + } +} diff --git a/src/main/java/com/p4square/restlet/oauth/OAuthAuthenticator.java b/src/main/java/com/p4square/restlet/oauth/OAuthAuthenticator.java new file mode 100644 index 0000000..c33bb5a --- /dev/null +++ b/src/main/java/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/main/java/com/p4square/restlet/oauth/OAuthAuthenticatorHelper.java b/src/main/java/com/p4square/restlet/oauth/OAuthAuthenticatorHelper.java new file mode 100644 index 0000000..76ff044 --- /dev/null +++ b/src/main/java/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/main/java/com/p4square/restlet/oauth/OAuthException.java b/src/main/java/com/p4square/restlet/oauth/OAuthException.java new file mode 100644 index 0000000..dd326d3 --- /dev/null +++ b/src/main/java/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/main/java/com/p4square/restlet/oauth/OAuthHelper.java b/src/main/java/com/p4square/restlet/oauth/OAuthHelper.java new file mode 100644 index 0000000..67dd238 --- /dev/null +++ b/src/main/java/com/p4square/restlet/oauth/OAuthHelper.java @@ -0,0 +1,149 @@ +/* + * 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; +import org.restlet.representation.Representation; + +/** + * 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(); + Representation entity = response.getEntity(); + + try { + if (status.isSuccess()) { + Form form = new Form(entity); + String token = form.getFirstValue("oauth_token"); + String secret = form.getFirstValue("oauth_token_secret"); + + return new Token(token, secret); + + } else { + throw new OAuthException(status); + } + } finally { + entity.release(); + } + } + + /** + * 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. + */ + public 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. + */ + public Response getResponse(Request request) { + return mDispatcher.handle(request); + } +} diff --git a/src/main/java/com/p4square/restlet/oauth/OAuthUser.java b/src/main/java/com/p4square/restlet/oauth/OAuthUser.java new file mode 100644 index 0000000..11dbac1 --- /dev/null +++ b/src/main/java/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/main/java/com/p4square/restlet/oauth/Token.java b/src/main/java/com/p4square/restlet/oauth/Token.java new file mode 100644 index 0000000..51a9087 --- /dev/null +++ b/src/main/java/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); + } +} |