summaryrefslogtreecommitdiff
path: root/src/com/p4square
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/p4square')
-rw-r--r--src/com/p4square/f1oauth/F1OAuthHelper.java128
-rw-r--r--src/com/p4square/f1oauth/SecondPartyAuthenticator.java52
-rw-r--r--src/com/p4square/f1oauth/SecondPartyVerifier.java60
-rw-r--r--src/com/p4square/grow/frontend/ErrorPage.java25
-rw-r--r--src/com/p4square/grow/frontend/GrowFrontend.java71
-rw-r--r--src/com/p4square/grow/frontend/LoginFormAuthenticator.java122
-rw-r--r--src/com/p4square/grow/frontend/LoginPageResource.java40
-rw-r--r--src/com/p4square/grow/frontend/NewAccountResource.java115
-rw-r--r--src/com/p4square/grow/frontend/SurveyPageResource.java6
-rw-r--r--src/com/p4square/grow/frontend/TrainingPageResource.java6
-rw-r--r--src/com/p4square/grow/frontend/VideosResource.java4
-rw-r--r--src/com/p4square/grow/frontend/session/Session.java55
-rw-r--r--src/com/p4square/grow/frontend/session/SessionAuthenticator.java (renamed from src/com/p4square/grow/frontend/LoginAuthenticator.java)28
-rw-r--r--src/com/p4square/grow/frontend/session/SessionCheckingAuthenticator.java38
-rw-r--r--src/com/p4square/grow/frontend/session/SessionCookieAuthenticator.java59
-rw-r--r--src/com/p4square/grow/frontend/session/SessionCreatingAuthenticator.java45
-rw-r--r--src/com/p4square/grow/frontend/session/Sessions.java80
-rw-r--r--src/com/p4square/restlet/oauth/OAuthAuthenticator.java95
-rw-r--r--src/com/p4square/restlet/oauth/OAuthAuthenticatorHelper.java177
-rw-r--r--src/com/p4square/restlet/oauth/OAuthException.java25
-rw-r--r--src/com/p4square/restlet/oauth/OAuthHelper.java143
-rw-r--r--src/com/p4square/restlet/oauth/OAuthUser.java50
-rw-r--r--src/com/p4square/restlet/oauth/Token.java52
23 files changed, 1400 insertions, 76 deletions
diff --git a/src/com/p4square/f1oauth/F1OAuthHelper.java b/src/com/p4square/f1oauth/F1OAuthHelper.java
new file mode 100644
index 0000000..d75460f
--- /dev/null
+++ b/src/com/p4square/f1oauth/F1OAuthHelper.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.f1oauth;
+
+import java.net.URLEncoder;
+
+import org.apache.log4j.Logger;
+
+import org.restlet.Context;
+import org.restlet.Response;
+import org.restlet.Request;
+import org.restlet.data.ChallengeResponse;
+import org.restlet.data.ChallengeScheme;
+import org.restlet.data.Method;
+import org.restlet.engine.util.Base64;
+import org.restlet.representation.StringRepresentation;
+
+import com.p4square.restlet.oauth.OAuthException;
+import com.p4square.restlet.oauth.OAuthHelper;
+import com.p4square.restlet.oauth.OAuthUser;
+import com.p4square.restlet.oauth.Token;
+
+/**
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class F1OAuthHelper extends OAuthHelper {
+ public enum UserType {
+ WEBLINK, PORTAL;
+ }
+
+ private static final Logger LOG = Logger.getLogger(F1OAuthHelper.class);
+
+ private static final String VERSION_STRING = "/v1/";
+ private static final String REQUESTTOKEN_URL = "Tokens/RequestToken";
+ private static final String AUTHORIZATION_URL = "Login";
+ private static final String ACCESSTOKEN_URL= "Tokens/AccessToken";
+ private static final String TRUSTED_ACCESSTOKEN_URL = "/AccessToken";
+
+ private final String mBaseUrl;
+ private final String mMethod;
+
+ /**
+ * @param method Either WeblinkUser or PortalUser.
+ */
+ public F1OAuthHelper(Context context, String consumerKey, String consumerSecret,
+ String baseUrl, String churchCode, UserType userType) {
+ super(context, consumerKey, consumerSecret);
+
+ switch (userType) {
+ case WEBLINK:
+ mMethod = "WeblinkUser";
+ break;
+ case PORTAL:
+ mMethod = "PortalUser";
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown UserType");
+ }
+
+ mBaseUrl = "https://" + churchCode + "." + baseUrl + VERSION_STRING;
+ }
+
+ /**
+ * @return the URL for the initial RequestToken request.
+ */
+ protected String getRequestTokenUrl() {
+ return mBaseUrl + REQUESTTOKEN_URL;
+ }
+
+ /**
+ * @return the URL to redirect the user to for Authentication.
+ */
+ public String getLoginUrl(Token requestToken, String callback) {
+ String loginUrl = mBaseUrl + mMethod + AUTHORIZATION_URL
+ + "?oauth_token=" + URLEncoder.encode(requestToken.getToken());
+
+ if (callback != null) {
+ loginUrl += "&oauth_callback=" + URLEncoder.encode(callback);
+ }
+
+ return loginUrl;
+ }
+
+
+ /**
+ * @return the URL for the AccessToken request.
+ */
+ protected String getAccessTokenUrl() {
+ return mBaseUrl + ACCESSTOKEN_URL;
+ }
+
+ /**
+ * Request an AccessToken for a particular username and password.
+ *
+ * This is an F1 extension to OAuth:
+ * http://developer.fellowshipone.com/docs/v1/Util/AuthDocs.help#2creds
+ */
+ public OAuthUser getAccessToken(String username, String password) throws OAuthException {
+ Request request = new Request(Method.POST, mBaseUrl + mMethod + TRUSTED_ACCESSTOKEN_URL);
+ request.setChallengeResponse(new ChallengeResponse(ChallengeScheme.HTTP_OAUTH));
+
+ String base64String = Base64.encode((username + " " + password).getBytes(), false);
+ request.setEntity(new StringRepresentation(base64String));
+
+ return processAccessTokenRequest(request);
+ }
+
+ public void createAccount(String firstname, String lastname, String email, String redirect)
+ throws OAuthException {
+ String req = String.format("{\n\"account\":{\n\"firstName\":\"%s\",\n"
+ + "\"lastName\":\"%s\",\n\"email\":\"%s\",\n"
+ + "\"urlRedirect\":\"%s\"\n}\n}",
+ firstname, lastname, email, redirect);
+
+ Request request = new Request(Method.POST, mBaseUrl + "/Accounts");
+ request.setChallengeResponse(new ChallengeResponse(ChallengeScheme.HTTP_OAUTH));
+ request.setEntity(new StringRepresentation(req));
+
+ Response response = getResponse(request);
+
+ if (!response.getStatus().isSuccess()) {
+ throw new OAuthException(response.getStatus());
+ }
+ }
+}
diff --git a/src/com/p4square/f1oauth/SecondPartyAuthenticator.java b/src/com/p4square/f1oauth/SecondPartyAuthenticator.java
new file mode 100644
index 0000000..1983d69
--- /dev/null
+++ b/src/com/p4square/f1oauth/SecondPartyAuthenticator.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.f1oauth;
+
+import org.apache.log4j.Logger;
+
+import com.p4square.restlet.oauth.OAuthException;
+import com.p4square.restlet.oauth.OAuthUser;
+
+import org.restlet.Context;
+import org.restlet.Request;
+import org.restlet.Response;
+import org.restlet.security.Authenticator;
+
+/**
+ * Restlet Authenticator for 2nd
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class SecondPartyAuthenticator extends Authenticator {
+ private static final Logger LOG = Logger.getLogger(SecondPartyAuthenticator.class);
+
+ private final F1OAuthHelper mHelper;
+
+ public SecondPartyAuthenticator(Context context, boolean optional, F1OAuthHelper helper) {
+ super(context, optional);
+
+ mHelper = helper;
+ }
+
+ protected boolean authenticate(Request request, Response response) {
+ if (request.getChallengeResponse() == null) {
+ return false; // no credentials
+ }
+
+ String username = request.getChallengeResponse().getIdentifier();
+ String password = new String(request.getChallengeResponse().getSecret());
+
+ try {
+ OAuthUser user = mHelper.getAccessToken(username, password);
+ request.getClientInfo().setUser(user);
+
+ return true;
+
+ } catch (OAuthException e) {
+ LOG.info("OAuth Exception: " + e);
+ }
+
+ return false; // Invalid credentials
+ }
+}
diff --git a/src/com/p4square/f1oauth/SecondPartyVerifier.java b/src/com/p4square/f1oauth/SecondPartyVerifier.java
new file mode 100644
index 0000000..870fe3e
--- /dev/null
+++ b/src/com/p4square/f1oauth/SecondPartyVerifier.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.f1oauth;
+
+import org.apache.log4j.Logger;
+
+import com.p4square.restlet.oauth.OAuthException;
+import com.p4square.restlet.oauth.OAuthUser;
+
+import org.restlet.Context;
+import org.restlet.Request;
+import org.restlet.Response;
+import org.restlet.security.Verifier;
+
+/**
+ * Restlet Verifier for F1 2nd Party Authentication
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class SecondPartyVerifier implements Verifier {
+ private static final Logger LOG = Logger.getLogger(SecondPartyVerifier.class);
+
+ private final F1OAuthHelper mHelper;
+
+ public SecondPartyVerifier(F1OAuthHelper helper) {
+ if (helper == null) {
+ throw new IllegalArgumentException("Helper can not be null.");
+ }
+
+ mHelper = helper;
+ }
+
+ @Override
+ public int verify(Request request, Response response) {
+ if (request.getChallengeResponse() == null) {
+ return RESULT_MISSING; // no credentials
+ }
+
+ String username = request.getChallengeResponse().getIdentifier();
+ String password = new String(request.getChallengeResponse().getSecret());
+
+ try {
+ OAuthUser user = mHelper.getAccessToken(username, password);
+ user.setIdentifier(username);
+ user.setEmail(username);
+
+ // This seems like a hack... but it'll work
+ request.getClientInfo().setUser(user);
+
+ return RESULT_VALID;
+
+ } catch (OAuthException e) {
+ LOG.info("OAuth Exception: " + e, e);
+ }
+
+ return RESULT_INVALID; // Invalid credentials
+ }
+}
diff --git a/src/com/p4square/grow/frontend/ErrorPage.java b/src/com/p4square/grow/frontend/ErrorPage.java
new file mode 100644
index 0000000..4a9380f
--- /dev/null
+++ b/src/com/p4square/grow/frontend/ErrorPage.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.frontend;
+
+import org.restlet.representation.StringRepresentation;
+
+/**
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class ErrorPage extends StringRepresentation {
+ public static final ErrorPage TEMPLATE_NOT_FOUND = new ErrorPage();
+ public static final ErrorPage RENDER_ERROR = new ErrorPage();
+
+
+ public ErrorPage() {
+ super("TODO");
+ }
+
+ public ErrorPage(String s) {
+ super(s);
+ }
+}
diff --git a/src/com/p4square/grow/frontend/GrowFrontend.java b/src/com/p4square/grow/frontend/GrowFrontend.java
index 5c49fe2..36e7544 100644
--- a/src/com/p4square/grow/frontend/GrowFrontend.java
+++ b/src/com/p4square/grow/frontend/GrowFrontend.java
@@ -7,20 +7,32 @@ package com.p4square.grow.frontend;
import java.io.File;
import java.io.IOException;
+import java.util.Arrays;
+import java.util.UUID;
+
import org.restlet.Application;
import org.restlet.Component;
+import org.restlet.Client;
+import org.restlet.Context;
import org.restlet.Restlet;
import org.restlet.data.Protocol;
import org.restlet.resource.Directory;
import org.restlet.routing.Router;
+import org.restlet.security.Authenticator;
import org.apache.log4j.Logger;
-import net.jesterpm.fmfacade.FMFacade;
-import net.jesterpm.fmfacade.FreeMarkerPageResource;
+import com.p4square.fmfacade.FMFacade;
+import com.p4square.fmfacade.FreeMarkerPageResource;
import com.p4square.grow.config.Config;
+import com.p4square.f1oauth.F1OAuthHelper;
+import com.p4square.f1oauth.SecondPartyVerifier;
+
+import com.p4square.grow.frontend.session.SessionCheckingAuthenticator;
+import com.p4square.grow.frontend.session.SessionCreatingAuthenticator;
+
/**
* This is the Restlet Application implementing the Grow project front-end.
* It's implemented as an extension of FMFacade that connects interactive pages
@@ -34,6 +46,8 @@ public class GrowFrontend extends FMFacade {
private Config mConfig;
+ private F1OAuthHelper mHelper;
+
public GrowFrontend() {
mConfig = new Config();
}
@@ -62,14 +76,23 @@ public class GrowFrontend extends FMFacade {
}
}
+ F1OAuthHelper getHelper() {
+ if (mHelper == null) {
+ mHelper = new F1OAuthHelper(getContext(), mConfig.getString("f1ConsumerKey", ""),
+ mConfig.getString("f1ConsumerSecret", ""),
+ mConfig.getString("f1BaseUrl", "staging.fellowshiponeapi.com"),
+ mConfig.getString("f1ChurchCode", "pfseawa"),
+ F1OAuthHelper.UserType.WEBLINK);
+ }
+
+ return mHelper;
+ }
+
@Override
protected Router createRouter() {
Router router = new Router(getContext());
- final String loginPage = getConfig().getString("dynamicRoot", "") + "/login.html";
-
- final LoginAuthenticator defaultGuard =
- new LoginAuthenticator(getContext(), true, loginPage);
+ final Authenticator defaultGuard = new SessionCheckingAuthenticator(getContext(), true);
defaultGuard.setNext(FreeMarkerPageResource.class);
router.attachDefault(defaultGuard);
router.attach("/login.html", LoginPageResource.class);
@@ -81,14 +104,36 @@ public class GrowFrontend extends FMFacade {
accountRouter.attach("/training/{chapter}", TrainingPageResource.class);
accountRouter.attach("/training", TrainingPageResource.class);
- final LoginAuthenticator accountGuard =
- new LoginAuthenticator(getContext(), false, loginPage);
- accountGuard.setNext(accountRouter);
+ final Authenticator accountGuard = createAuthenticatorChain(accountRouter);
router.attach("/account", accountGuard);
return router;
}
+ private Authenticator createAuthenticatorChain(Restlet last) {
+ final Context context = getContext();
+ final String loginPage = getConfig().getString("dynamicRoot", "") + "/login.html";
+
+ // This is used to check for an existing session
+ SessionCheckingAuthenticator sessionChk = new SessionCheckingAuthenticator(context, true);
+
+ // This is used to authenticate the user
+ SecondPartyVerifier f1Verifier = new SecondPartyVerifier(getHelper());
+ LoginFormAuthenticator loginAuth = new LoginFormAuthenticator(context, false, f1Verifier);
+ loginAuth.setLoginFormUrl(loginPage);
+ loginAuth.setLoginPostUrl("/account/authenticate");
+
+ // This is used to create a new session for a newly authenticated user.
+ SessionCreatingAuthenticator sessionCreate = new SessionCreatingAuthenticator(context);
+
+ sessionChk.setNext(loginAuth);
+ loginAuth.setNext(sessionCreate);
+
+ sessionCreate.setNext(last);
+
+ return sessionChk;
+ }
+
/**
* Stand-alone main for testing.
*/
@@ -98,14 +143,16 @@ public class GrowFrontend extends FMFacade {
component.getServers().add(Protocol.HTTP, 8085);
component.getClients().add(Protocol.HTTP);
component.getClients().add(Protocol.FILE);
-
+ component.getClients().add(new Client(null, Arrays.asList(Protocol.HTTPS), "org.restlet.ext.httpclient.HttpClientHelper"));
+
// Static content
try {
component.getDefaultHost().attach("/images/", new FileServingApp("./build/images/"));
component.getDefaultHost().attach("/scripts", new FileServingApp("./build/scripts"));
component.getDefaultHost().attach("/style.css", new FileServingApp("./build/style.css"));
+ component.getDefaultHost().attach("/favicon.ico", new FileServingApp("./build/favicon.ico"));
} catch (IOException e) {
- cLog.error("Could not create directory for static resources: "
+ cLog.error("Could not create directory for static resources: "
+ e.getMessage(), e);
}
@@ -139,7 +186,7 @@ public class GrowFrontend extends FMFacade {
cLog.fatal("Could not start: " + e.getMessage(), e);
}
}
-
+
private static class FileServingApp extends Application {
private final String mPath;
diff --git a/src/com/p4square/grow/frontend/LoginFormAuthenticator.java b/src/com/p4square/grow/frontend/LoginFormAuthenticator.java
new file mode 100644
index 0000000..d5a3c22
--- /dev/null
+++ b/src/com/p4square/grow/frontend/LoginFormAuthenticator.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.frontend;
+
+import org.apache.log4j.Logger;
+
+import org.restlet.Context;
+import org.restlet.Request;
+import org.restlet.Response;
+import org.restlet.data.ChallengeResponse;
+import org.restlet.data.ChallengeScheme;
+import org.restlet.data.Form;
+import org.restlet.data.Reference;
+import org.restlet.security.Authenticator;
+import org.restlet.security.Verifier;
+
+/**
+ * LoginFormAuthenticator changes
+ *
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class LoginFormAuthenticator extends Authenticator {
+ private static final Logger LOG = Logger.getLogger(LoginFormAuthenticator.class);
+
+ private final Verifier mVerifier;
+
+ private String mLoginPage = "/login.html";
+ private String mLoginPostUrl = "/authenticate";
+ private String mDefaultRedirect = "/index.html";
+
+ public LoginFormAuthenticator(Context context, boolean optional, Verifier verifier) {
+ super(context, false, optional, null);
+
+ mVerifier = verifier;
+ }
+
+ public void setLoginFormUrl(String url) {
+ mLoginPage = url;
+ }
+
+ public void setLoginPostUrl(String url) {
+ mLoginPostUrl = url;
+ }
+
+ @Override
+ protected int beforeHandle(Request request, Response response) {
+ if (request.getClientInfo().isAuthenticated()) {
+ // TODO: Logout
+ LOG.debug("Already authenticated. Skipping");
+ return CONTINUE;
+
+ } else {
+ return super.beforeHandle(request, response);
+ }
+ }
+
+
+ @Override
+ protected boolean authenticate(Request request, Response response) {
+ String requestPath = request.getResourceRef().getPath();
+ boolean isLoginAttempt = mLoginPostUrl.equals(requestPath);
+
+ Form query = request.getOriginalRef().getQueryAsForm();
+ String redirect = query.getFirstValue("redirect");
+ if (redirect == null) {
+ if (isLoginAttempt) {
+ redirect = mDefaultRedirect;
+ } else {
+ redirect = request.getResourceRef().getRelativePart();
+ }
+ }
+
+ boolean authenticationFailed = false;
+
+ if (isLoginAttempt) {
+ LOG.debug("Attempting authentication");
+
+ // Process login form
+ final Form form = new Form(request.getEntity());
+ final String email = form.getFirstValue("email");
+ final String password = form.getFirstValue("password");
+
+ boolean authenticated = false;
+
+ if (email != null && !"".equals(email) &&
+ password != null && !"".equals(password)) {
+
+ LOG.debug("Got login request from " + email);
+
+ request.setChallengeResponse(
+ new ChallengeResponse(ChallengeScheme.HTTP_BASIC, email, password.toCharArray()));
+
+ // We expect the verifier to setup the User object.
+ int result = mVerifier.verify(request, response);
+ if (result == Verifier.RESULT_VALID) {
+ // TODO: Ensure redirect is a relative url.
+ response.redirectSeeOther(redirect);
+ return true;
+ }
+ }
+
+ authenticationFailed = true;
+ }
+
+ if (!isOptional() || authenticationFailed) {
+ Reference ref = new Reference(mLoginPage);
+ ref.addQueryParameter("redirect", redirect);
+
+ if (authenticationFailed) {
+ ref.addQueryParameter("retry", "t");
+ }
+
+ LOG.debug("Redirecting to " + ref.toString());
+ response.redirectSeeOther(ref.toString());
+ }
+ LOG.debug("Failing authentication.");
+ return false;
+ }
+}
diff --git a/src/com/p4square/grow/frontend/LoginPageResource.java b/src/com/p4square/grow/frontend/LoginPageResource.java
index 70caa3e..e645c1b 100644
--- a/src/com/p4square/grow/frontend/LoginPageResource.java
+++ b/src/com/p4square/grow/frontend/LoginPageResource.java
@@ -17,7 +17,7 @@ import org.restlet.ext.freemarker.TemplateRepresentation;
import org.apache.log4j.Logger;
-import net.jesterpm.fmfacade.FreeMarkerPageResource;
+import com.p4square.fmfacade.FreeMarkerPageResource;
/**
* LoginPageResource presents a login page template and processes the response.
@@ -57,7 +57,11 @@ public class LoginPageResource extends FreeMarkerPageResource {
Map<String, Object> root = getRootObject();
- root.put("errorMessage", mErrorMessage);
+ Form query = getRequest().getOriginalRef().getQueryAsForm();
+ String retry = query.getFirstValue("retry");
+ if ("t".equals("retry")) {
+ root.put("errorMessage", "Invalid email or password.");
+ }
return new TemplateRepresentation(t, root, MediaType.TEXT_HTML);
@@ -68,36 +72,4 @@ public class LoginPageResource extends FreeMarkerPageResource {
}
}
- /**
- * Process login and authenticate the user.
- */
- @Override
- protected Representation post(Representation entity) {
- final Form form = new Form(entity);
- final String email = form.getFirstValue("email");
- final String password = form.getFirstValue("password");
-
- boolean authenticated = false;
-
- // TODO: Do something real here
- if (email != null && !"".equals(email)) {
- cLog.debug("Got login request from " + email);
-
- // TODO: Encrypt user info
- getResponse().getCookieSettings().add(LoginAuthenticator.COOKIE_NAME, email);
-
- authenticated = true;
- }
-
- if (authenticated) {
- // TODO: Better return url.
- getResponse().redirectSeeOther(mGrowFrontend.getConfig().getString("dynamicRoot", "") + "/index.html");
- return null;
-
- } else {
- // Send them back to the login page...
- mErrorMessage = "Incorrect Email or Password.";
- return get();
- }
- }
}
diff --git a/src/com/p4square/grow/frontend/NewAccountResource.java b/src/com/p4square/grow/frontend/NewAccountResource.java
new file mode 100644
index 0000000..b72680a
--- /dev/null
+++ b/src/com/p4square/grow/frontend/NewAccountResource.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.frontend;
+
+import java.util.Map;
+
+import freemarker.template.Template;
+
+import org.restlet.data.Form;
+import org.restlet.data.MediaType;
+import org.restlet.data.Status;
+import org.restlet.resource.ServerResource;
+import org.restlet.representation.Representation;
+import org.restlet.representation.StringRepresentation;
+import org.restlet.ext.freemarker.TemplateRepresentation;
+
+import org.apache.log4j.Logger;
+
+import com.p4square.f1oauth.F1OAuthHelper;
+import com.p4square.restlet.oauth.OAuthException;
+
+import com.p4square.fmfacade.FreeMarkerPageResource;
+
+/**
+ * This resource creates a new InFellowship account.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class NewAccountResource extends FreeMarkerPageResource {
+ private static Logger LOG = Logger.getLogger(NewAccountResource.class);
+
+ private GrowFrontend mGrowFrontend;
+ private F1OAuthHelper mHelper;
+
+ private String mErrorMessage;
+
+ private String mLoginPageUrl;
+ private String mVerificationPage;
+
+ @Override
+ public void doInit() {
+ super.doInit();
+
+ mGrowFrontend = (GrowFrontend) getApplication();
+ mHelper = mGrowFrontend.getHelper();
+
+ mErrorMessage = null;
+
+ mLoginPageUrl = "";
+ mVerificationPage = "";
+ }
+
+ /**
+ * Return the login page.
+ */
+ @Override
+ protected Representation get() {
+ Template t = mGrowFrontend.getTemplate("pages/newaccount.html.ftl");
+
+ try {
+ if (t == null) {
+ setStatus(Status.CLIENT_ERROR_NOT_FOUND);
+ return ErrorPage.TEMPLATE_NOT_FOUND;
+ }
+
+ Map<String, Object> root = getRootObject();
+ root.put("errorMessage", mErrorMessage);
+
+ return new TemplateRepresentation(t, root, MediaType.TEXT_HTML);
+
+ } catch (Exception e) {
+ LOG.fatal("Could not render page: " + e.getMessage(), e);
+ setStatus(Status.SERVER_ERROR_INTERNAL);
+ return ErrorPage.RENDER_ERROR;
+ }
+ }
+
+ @Override
+ protected Representation post(Representation rep) {
+ Form form = new Form(rep);
+
+ String firstname = form.getFirstValue("firstname");
+ String lastname = form.getFirstValue("lastname");
+ String email = form.getFirstValue("email");
+
+ if (isEmpty(firstname)) {
+ mErrorMessage += "First Name is a required field. ";
+ }
+ if (isEmpty(lastname)) {
+ mErrorMessage += "Last Name is a required field. ";
+ }
+ if (isEmpty(email)) {
+ mErrorMessage += "Email is a required field. ";
+ }
+
+ if (mErrorMessage.length() > 0) {
+ return get();
+ }
+
+ try {
+ mHelper.createAccount(firstname, lastname, email, mLoginPageUrl);
+ getResponse().redirectSeeOther(mVerificationPage);
+ return new StringRepresentation("Redirecting to " + mVerificationPage);
+
+ } catch (OAuthException e) {
+ return new ErrorPage(e.getStatus().getDescription());
+ }
+ }
+
+ private boolean isEmpty(String s) {
+ return s != null && s.trim().length() > 0;
+ }
+}
diff --git a/src/com/p4square/grow/frontend/SurveyPageResource.java b/src/com/p4square/grow/frontend/SurveyPageResource.java
index 351eade..8a3b5a5 100644
--- a/src/com/p4square/grow/frontend/SurveyPageResource.java
+++ b/src/com/p4square/grow/frontend/SurveyPageResource.java
@@ -18,10 +18,10 @@ import org.restlet.resource.ServerResource;
import org.apache.log4j.Logger;
-import net.jesterpm.fmfacade.json.JsonRequestClient;
-import net.jesterpm.fmfacade.json.JsonResponse;
+import com.p4square.fmfacade.json.JsonRequestClient;
+import com.p4square.fmfacade.json.JsonResponse;
-import net.jesterpm.fmfacade.FreeMarkerPageResource;
+import com.p4square.fmfacade.FreeMarkerPageResource;
import com.p4square.grow.config.Config;
diff --git a/src/com/p4square/grow/frontend/TrainingPageResource.java b/src/com/p4square/grow/frontend/TrainingPageResource.java
index 6c89ac9..459eb9a 100644
--- a/src/com/p4square/grow/frontend/TrainingPageResource.java
+++ b/src/com/p4square/grow/frontend/TrainingPageResource.java
@@ -20,10 +20,10 @@ import org.restlet.resource.ServerResource;
import org.apache.log4j.Logger;
-import net.jesterpm.fmfacade.json.JsonRequestClient;
-import net.jesterpm.fmfacade.json.JsonResponse;
+import com.p4square.fmfacade.json.JsonRequestClient;
+import com.p4square.fmfacade.json.JsonResponse;
-import net.jesterpm.fmfacade.FreeMarkerPageResource;
+import com.p4square.fmfacade.FreeMarkerPageResource;
import com.p4square.grow.config.Config;
diff --git a/src/com/p4square/grow/frontend/VideosResource.java b/src/com/p4square/grow/frontend/VideosResource.java
index fed315b..cdb2fb4 100644
--- a/src/com/p4square/grow/frontend/VideosResource.java
+++ b/src/com/p4square/grow/frontend/VideosResource.java
@@ -17,8 +17,8 @@ import org.restlet.resource.ServerResource;
import org.apache.log4j.Logger;
-import net.jesterpm.fmfacade.json.JsonRequestClient;
-import net.jesterpm.fmfacade.json.JsonResponse;
+import com.p4square.fmfacade.json.JsonRequestClient;
+import com.p4square.fmfacade.json.JsonResponse;
import com.p4square.grow.config.Config;
diff --git a/src/com/p4square/grow/frontend/session/Session.java b/src/com/p4square/grow/frontend/session/Session.java
new file mode 100644
index 0000000..3a241ef
--- /dev/null
+++ b/src/com/p4square/grow/frontend/session/Session.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.frontend.session;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+import org.restlet.security.User;
+
+/**
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class Session {
+ private static final long LIFETIME = 86400;
+
+ private final String mSessionId;
+ private final User mUser;
+ private final Map<String, String> mData;
+ private long mExpires;
+
+ Session(User user) {
+ mUser = user;
+ mSessionId = UUID.randomUUID().toString();
+ mExpires = System.currentTimeMillis() + LIFETIME;
+ mData = new HashMap<String, String>();
+ }
+
+ void touch() {
+ mExpires = System.currentTimeMillis() + LIFETIME;
+ }
+
+ boolean isExpired() {
+ return System.currentTimeMillis() > mExpires;
+ }
+
+ public String getId() {
+ return mSessionId;
+ }
+
+ public String get(String key) {
+ return mData.get(key);
+ }
+
+ public void put(String key, String value) {
+ mData.put(key, value);
+ }
+
+ public User getUser() {
+ return mUser;
+ }
+}
diff --git a/src/com/p4square/grow/frontend/LoginAuthenticator.java b/src/com/p4square/grow/frontend/session/SessionAuthenticator.java
index 64f5827..ac194af 100644
--- a/src/com/p4square/grow/frontend/LoginAuthenticator.java
+++ b/src/com/p4square/grow/frontend/session/SessionAuthenticator.java
@@ -2,9 +2,7 @@
* Copyright 2013 Jesse Morgan
*/
-package com.p4square.grow.frontend;
-
-import org.apache.log4j.Logger;
+package com.p4square.grow.frontend.session;
import org.restlet.Context;
import org.restlet.Request;
@@ -13,27 +11,12 @@ import org.restlet.security.Authenticator;
import org.restlet.security.User;
/**
- * LoginAuthenticator decrypts a cookie containing the user's session info
- * and makes that information available as the ClientInfo's User object.
- *
- * If this Authenticator is not optional, the user will be redirected to a
- * login page.
- *
+ *
* @author Jesse Morgan <jesse@jesterpm.net>
*/
-public class LoginAuthenticator extends Authenticator {
- private static Logger cLog = Logger.getLogger(LoginAuthenticator.class);
-
- public static final String COOKIE_NAME = "growsession";
-
- private final String mLoginPage;
-
- public LoginAuthenticator(Context context, boolean optional, String loginPage) {
- super(context, optional);
-
- mLoginPage = loginPage;
- }
-
+public class SessionAuthenticator /*extends Authenticator*/ {
+ /*
+ @Override
protected boolean authenticate(Request request, Response response) {
// Check for authentication cookie
final String cookie = request.getCookies().getFirstValue(COOKIE_NAME);
@@ -49,4 +32,5 @@ public class LoginAuthenticator extends Authenticator {
response.redirectSeeOther(mLoginPage);
return false;
}
+ */
}
diff --git a/src/com/p4square/grow/frontend/session/SessionCheckingAuthenticator.java b/src/com/p4square/grow/frontend/session/SessionCheckingAuthenticator.java
new file mode 100644
index 0000000..8382aff
--- /dev/null
+++ b/src/com/p4square/grow/frontend/session/SessionCheckingAuthenticator.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.frontend.session;
+
+import org.apache.log4j.Logger;
+
+import org.restlet.Context;
+import org.restlet.Request;
+import org.restlet.Response;
+import org.restlet.security.Authenticator;
+
+/**
+ * Authenticator which succeeds if a valid Session exists.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class SessionCheckingAuthenticator extends Authenticator {
+ private static final Logger LOG = Logger.getLogger(SessionCheckingAuthenticator.class);
+
+ public SessionCheckingAuthenticator(Context context, boolean optional) {
+ super(context, optional);
+ }
+
+ protected boolean authenticate(Request request, Response response) {
+ Session s = Sessions.getInstance().get(request);
+
+ if (s != null) {
+ request.getClientInfo().setUser(s.getUser());
+ return true;
+
+ } else {
+ return false;
+ }
+ }
+
+}
diff --git a/src/com/p4square/grow/frontend/session/SessionCookieAuthenticator.java b/src/com/p4square/grow/frontend/session/SessionCookieAuthenticator.java
new file mode 100644
index 0000000..789f58e
--- /dev/null
+++ b/src/com/p4square/grow/frontend/session/SessionCookieAuthenticator.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.frontend.session;
+
+import org.apache.log4j.Logger;
+
+import org.restlet.Context;
+import org.restlet.Request;
+import org.restlet.Response;
+import org.restlet.security.Authenticator;
+
+/**
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class SessionCookieAuthenticator extends Authenticator {
+ private static final Logger LOG = Logger.getLogger(SessionCookieAuthenticator.class);
+
+ private static final String COOKIE_NAME = "S";
+
+ private final Sessions mSessions;
+
+ public SessionCookieAuthenticator(Context context, boolean optional, Sessions sessions) {
+ super(context, optional);
+
+ mSessions = sessions;
+ }
+
+ protected boolean authenticate(Request request, Response response) {
+ final String cookie = request.getCookies().getFirstValue(COOKIE_NAME);
+
+ if (request.getClientInfo().isAuthenticated()) {
+ // Request is already authenticated... create session if it doesn't exist.
+ if (cookie == null) {
+ Session s = mSessions.create(request.getClientInfo().getUser());
+ response.getCookieSettings().add(COOKIE_NAME, s.getId());
+ }
+
+ return true;
+
+ } else {
+ // Check for authentication cookie
+ if (cookie != null) {
+ LOG.debug("Got cookie: " + cookie);
+
+ Session s = mSessions.get(cookie);
+ if (s != null) {
+ request.getClientInfo().setUser(s.getUser());
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+
+}
diff --git a/src/com/p4square/grow/frontend/session/SessionCreatingAuthenticator.java b/src/com/p4square/grow/frontend/session/SessionCreatingAuthenticator.java
new file mode 100644
index 0000000..ce6024c
--- /dev/null
+++ b/src/com/p4square/grow/frontend/session/SessionCreatingAuthenticator.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.frontend.session;
+
+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 creates a Session for the request and adds a cookie
+ * to the response.
+ *
+ * The Request MUST be Authenticated and MUST have a User object associated.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class SessionCreatingAuthenticator extends Authenticator {
+ private static final Logger LOG = Logger.getLogger(SessionCreatingAuthenticator.class);
+
+ public SessionCreatingAuthenticator(Context context) {
+ super(context, true);
+ }
+
+ protected boolean authenticate(Request request, Response response) {
+ if (Sessions.getInstance().get(request) != null) {
+ return true;
+ }
+
+ User user = request.getClientInfo().getUser();
+
+ if (request.getClientInfo().isAuthenticated() && user != null) {
+ Sessions.getInstance().create(request, response);
+ return true;
+ }
+
+ return false;
+ }
+
+}
diff --git a/src/com/p4square/grow/frontend/session/Sessions.java b/src/com/p4square/grow/frontend/session/Sessions.java
new file mode 100644
index 0000000..094d2f0
--- /dev/null
+++ b/src/com/p4square/grow/frontend/session/Sessions.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.grow.frontend.session;
+
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.Map;
+
+import org.restlet.Response;
+import org.restlet.Request;
+import org.restlet.security.User;
+
+/**
+ * Singleton Session Manager.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class Sessions {
+ private static final String COOKIE_NAME = "S";
+
+ private static final Sessions THE = new Sessions();
+ public static Sessions getInstance() {
+ return THE;
+ }
+
+ private final Map<String, Session> mSessions;
+
+ private Sessions() {
+ mSessions = new ConcurrentHashMap<String, Session>();
+ }
+
+ public Session get(String sessionid) {
+ Session s = mSessions.get(sessionid);
+
+ if (s != null && !s.isExpired()) {
+ s.touch();
+ return s;
+ }
+
+ return null;
+ }
+
+ /**
+ * Get the Session associated with the Request.
+ * @return A session or null if no session is found.
+ */
+ public Session get(Request request) {
+ final String cookie = request.getCookies().getFirstValue(COOKIE_NAME);
+
+ if (cookie != null) {
+ return get(cookie);
+ }
+
+ return null;
+ }
+
+ public Session create(User user) {
+ if (user == null) {
+ throw new IllegalArgumentException("Can not create session for null user.");
+ }
+
+ Session s = new Session(user);
+ mSessions.put(s.getId(), s);
+
+ return s;
+ }
+
+ /**
+ * Create a new Session and add the Session cookie to the response.
+ */
+ public Session create(Request request, Response response) {
+ Session s = create(request.getClientInfo().getUser());
+
+ request.getCookies().add(COOKIE_NAME, s.getId());
+ response.getCookieSettings().add(COOKIE_NAME, s.getId());
+
+ return s;
+ }
+}
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);
+ }
+}