summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJesse Morgan <jesse@jesterpm.net>2013-08-27 08:28:16 -0700
committerJesse Morgan <jesse@jesterpm.net>2013-08-27 08:28:16 -0700
commit362cef066725dec0ffaeacb91e7e3c287d68bc51 (patch)
tree712fa205868adf14ebe08ba24dffdd72bfe616dc
parent9b33aaf27cd8f73402ee9967c6b0fd76a90f8ebe (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.
-rw-r--r--build.xml4
-rw-r--r--ivy.xml1
-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.java67
-rw-r--r--src/com/p4square/grow/frontend/LoginFormAuthenticator.java122
-rw-r--r--src/com/p4square/grow/frontend/LoginPageResource.java38
-rw-r--r--src/com/p4square/grow/frontend/NewAccountResource.java115
-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/grow.properties5
-rw-r--r--src/net/jesterpm/restlet/oauth/OAuthAuthenticator.java95
-rw-r--r--src/net/jesterpm/restlet/oauth/OAuthAuthenticatorHelper.java177
-rw-r--r--src/net/jesterpm/restlet/oauth/OAuthException.java25
-rw-r--r--src/net/jesterpm/restlet/oauth/OAuthHelper.java143
-rw-r--r--src/net/jesterpm/restlet/oauth/OAuthUser.java50
-rw-r--r--src/net/jesterpm/restlet/oauth/Token.java52
-rw-r--r--src/templates/macros/common.ftl1
-rw-r--r--src/templates/macros/textcontent.ftl8
-rw-r--r--src/templates/pages/about.html.ftl69
-rw-r--r--src/templates/pages/assessment.html.ftl44
-rw-r--r--src/templates/pages/contact.html.ftl37
-rw-r--r--src/templates/pages/index.html.ftl15
-rw-r--r--src/templates/pages/login.html.ftl6
-rw-r--r--src/templates/pages/newaccount.html.ftl26
-rw-r--r--src/templates/pages/verification.html.ftl17
-rw-r--r--src/templates/templates/assessment-results.ftl32
-rw-r--r--src/templates/templates/getstarted-button.ftl7
-rw-r--r--src/templates/templates/index-hero.ftl3
-rw-r--r--src/templates/templates/stage-complete.ftl56
-rw-r--r--web/favicon.icobin0 -> 4286 bytes
-rw-r--r--web/style.css4
38 files changed, 1702 insertions, 87 deletions
diff --git a/build.xml b/build.xml
index 7bc76d4..02b70b7 100644
--- a/build.xml
+++ b/build.xml
@@ -3,6 +3,10 @@
<property name="main.class" value="com.p4square.grow.frontend.GrowFrontend" />
+ <property name="deploy.manager.url" value="http://californium.jesterpm.net:9090/manager/text" />
+ <property name="deploy.manager.username" value="deploymentscript" />
+ <property name="deploy.context.path" value="/grow-frontend" />
+
<property file="${user.home}/.jesterpm-build-tools.properties" />
<property name="jesterpm.buildtools.root" value="../jesterpm-build-tools" />
<import file="${jesterpm.buildtools.root}/ant/tomcat-common.xml" />
diff --git a/ivy.xml b/ivy.xml
index bfc7f06..45f843c 100644
--- a/ivy.xml
+++ b/ivy.xml
@@ -14,6 +14,7 @@
<dependencies defaultconf="default,sources">
<dependency org="net.jesterpm" name="fmfacade" rev="[1.0-SNAPSHOT,)" />
+ <dependency org="org.restlet.jse" name="org.restlet.ext.httpclient" rev="[2.1,)" />
<!-- Backend Dependencies -->
<dependency org="com.netflix.astyanax" name="astyanax-core" rev="[1.0,)" />
diff --git a/src/com/p4square/f1oauth/F1OAuthHelper.java b/src/com/p4square/f1oauth/F1OAuthHelper.java
new file mode 100644
index 0000000..4f55043
--- /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 net.jesterpm.restlet.oauth.OAuthException;
+import net.jesterpm.restlet.oauth.OAuthHelper;
+import net.jesterpm.restlet.oauth.OAuthUser;
+import net.jesterpm.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..eec7447
--- /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 net.jesterpm.restlet.oauth.OAuthException;
+import net.jesterpm.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..78bb1bf
--- /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 net.jesterpm.restlet.oauth.OAuthException;
+import net.jesterpm.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..b625d4f 100644
--- a/src/com/p4square/grow/frontend/GrowFrontend.java
+++ b/src/com/p4square/grow/frontend/GrowFrontend.java
@@ -7,12 +7,18 @@ 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;
@@ -21,6 +27,12 @@ import net.jesterpm.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..3eaff65 100644
--- a/src/com/p4square/grow/frontend/LoginPageResource.java
+++ b/src/com/p4square/grow/frontend/LoginPageResource.java
@@ -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..e0b857a
--- /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 net.jesterpm.restlet.oauth.OAuthException;
+
+import net.jesterpm.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/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/grow.properties b/src/grow.properties
index 1f76237..88bee42 100644
--- a/src/grow.properties
+++ b/src/grow.properties
@@ -7,6 +7,11 @@ dev.dynamicRoot =
*.staticRoot = /grow-frontend
*.dynamicRoot = /grow-frontend
+*.f1ConsumerKey = 123
+*.f1ConsumerSecret = password-here
+*.f1BaseUrl = staging.fellowshiponeapi.com
+*.f1ChurchCode = pfseawa
+
# Backend Settings
dev.clusterName = Dev Cluster
diff --git a/src/net/jesterpm/restlet/oauth/OAuthAuthenticator.java b/src/net/jesterpm/restlet/oauth/OAuthAuthenticator.java
new file mode 100644
index 0000000..48ad4d4
--- /dev/null
+++ b/src/net/jesterpm/restlet/oauth/OAuthAuthenticator.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package net.jesterpm.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/net/jesterpm/restlet/oauth/OAuthAuthenticatorHelper.java b/src/net/jesterpm/restlet/oauth/OAuthAuthenticatorHelper.java
new file mode 100644
index 0000000..fb9fe63
--- /dev/null
+++ b/src/net/jesterpm/restlet/oauth/OAuthAuthenticatorHelper.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package net.jesterpm.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/net/jesterpm/restlet/oauth/OAuthException.java b/src/net/jesterpm/restlet/oauth/OAuthException.java
new file mode 100644
index 0000000..d90637c
--- /dev/null
+++ b/src/net/jesterpm/restlet/oauth/OAuthException.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package net.jesterpm.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/net/jesterpm/restlet/oauth/OAuthHelper.java b/src/net/jesterpm/restlet/oauth/OAuthHelper.java
new file mode 100644
index 0000000..a4a05cd
--- /dev/null
+++ b/src/net/jesterpm/restlet/oauth/OAuthHelper.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package net.jesterpm.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/net/jesterpm/restlet/oauth/OAuthUser.java b/src/net/jesterpm/restlet/oauth/OAuthUser.java
new file mode 100644
index 0000000..69f493e
--- /dev/null
+++ b/src/net/jesterpm/restlet/oauth/OAuthUser.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package net.jesterpm.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/net/jesterpm/restlet/oauth/Token.java b/src/net/jesterpm/restlet/oauth/Token.java
new file mode 100644
index 0000000..be82157
--- /dev/null
+++ b/src/net/jesterpm/restlet/oauth/Token.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package net.jesterpm.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);
+ }
+}
diff --git a/src/templates/macros/common.ftl b/src/templates/macros/common.ftl
index 513fc57..966afdb 100644
--- a/src/templates/macros/common.ftl
+++ b/src/templates/macros/common.ftl
@@ -1,4 +1,5 @@
<#include "content.ftl">
+<#include "textcontent.ftl">
<#include "noticebox.ftl">
<#assign dynamicRoot = "">
diff --git a/src/templates/macros/textcontent.ftl b/src/templates/macros/textcontent.ftl
new file mode 100644
index 0000000..408c05c
--- /dev/null
+++ b/src/templates/macros/textcontent.ftl
@@ -0,0 +1,8 @@
+<#macro textcontent>
+ <div id="content">
+ <article class="text">
+ <#nested>
+ </article>
+ </div>
+</#macro>
+
diff --git a/src/templates/pages/about.html.ftl b/src/templates/pages/about.html.ftl
index 3ab2bc0..63cd366 100644
--- a/src/templates/pages/about.html.ftl
+++ b/src/templates/pages/about.html.ftl
@@ -3,22 +3,69 @@
<@commonpage>
<@noticebox>
- The Grow Process focuses on the topic that you want to learn
- about. Out 'Assessment' test will give you the right courses
- fit for your level.
</@noticebox>
- <@content>
+ <@textcontent>
<h1>About</h1>
+
+ <p>
+ GROW is a comprehensive spiritual formation tool that is
+ specifically designed to help you engage in the discipleship
+ process.
+ </p>
+
+ <p>
+ Disciple means “learner” and our goal is to help you be an
+ effective learner and grow in the knowledge of Jesus Christ, the
+ Word of God, the essentials of your faith that will cause the
+ transformation God promises you will experience. And that
+ transformation will in turn change your life!
+ </p>
+
+ <p>
+ Every individual is in one of four stages of spiritual formation:
+ <ul>
+ <li>Seeker</li>
+ <li>Believer</li>
+ <li>Disciple</li>
+ <li>Teacher</li>
+ </ul>
+ </p>
+
+ <p>
+ Our goal is to help you identify where you are, and to help you get
+ to the next stage of development and spiritual maturity. Welcome
+ to GROW.
+ </p>
+
+ <p>
+ GROW is an innovative process that incorporates multiple media
+ elements for learning; video, written material, interactive tools,
+ community forums, etc.
+ </p>
+
+ <p>
+ GROW is web-based process designed to be experienced in a variety
+ of ways to meet all the possible scenarios for learning:
+ <ul>
+ <li>In a community group environment</li>
+ <li>In a small discipleship group of a couple of people</li>
+ <li>Direct one-on-one discipleship</li>
+ <li>Friends with friends</li>
+ <li>Husband and wife</li>
+ <li>At the coffee shop</li>
+ <li>In your kitchen or living room</li>
+ <li>By the pool</li>
+ </ul>
+ </p>
+
<p>
- Curabitur mattis molestie ligula, ac vestibulum Curabitur
- mattis facillisis vel. Iacus facillisis vel. Nam dignissim
- massa luctus ipsum adipiscing dignissim.
+ Now it’s your turn to act! Get started now in your journey by
+ taking the GROW assessment. Identify your starting place as a
+ Seeker, Believer, Disciple or Teacher and be a disciple.
</p>
- </@content>
+ </@textcontent>
- <div id="getstarted">
- <a class="greenbutton" href="index.html">Get Started! &#x2799;</a>
- </div>
+ <#include "/templates/getstarted-button.ftl">
</@commonpage>
diff --git a/src/templates/pages/assessment.html.ftl b/src/templates/pages/assessment.html.ftl
new file mode 100644
index 0000000..7903382
--- /dev/null
+++ b/src/templates/pages/assessment.html.ftl
@@ -0,0 +1,44 @@
+<#include "/macros/common.ftl">
+<#include "/macros/common-page.ftl">
+
+<@commonpage>
+ <@noticebox>
+ </@noticebox>
+
+ <@textcontent>
+ <h1>Assessment</h1>
+
+ <p>
+ Welcome to the start of GROW and the GROW personal assessment. The
+ purpose of this 18-question assessment will identify where you are
+ in the spiritual development process:
+ </p>
+
+ <p>
+ <!-- TODO: Insert Image Here -->
+ **************** Insert the image here. *************************
+ </p>
+
+ <p>
+ Upon completion of the assessment, you will be invited to join the
+ process at the appropriate stage. For example if the assessment
+ returns a “believer” result, you will be invited to start the
+ Believer level of this process.
+ </p>
+
+ <p>
+ Once you have completed this level, you will be invited to begin the next stage of GROW.
+ </p>
+
+ <p>
+ Let’s get with your personal GROW assessment now, it will only take a few minutes.
+ </p>
+
+ </@textcontent>
+
+ <div id="getstarted">
+ <a class="greenbutton" href="${dynamicRoot}/account/assessment">Begin Assessment &#x2799;</a>
+ </div>
+</@commonpage>
+
+
diff --git a/src/templates/pages/contact.html.ftl b/src/templates/pages/contact.html.ftl
index e69de29..40499cc 100644
--- a/src/templates/pages/contact.html.ftl
+++ b/src/templates/pages/contact.html.ftl
@@ -0,0 +1,37 @@
+<#include "/macros/common.ftl">
+<#include "/macros/common-page.ftl">
+
+<@commonpage>
+ <@noticebox>
+ </@noticebox>
+
+ <@textcontent>
+ <h1>Contact Us</h1>
+
+ <p>
+ If you have any questions about GROW, please complete the following
+ form, and we will be in touch soon, or call us at 253-848-9111.
+ </p>
+
+ <form action="${dynamicRoot}/contactus" method="post">
+ <p><label for="firstnameField">First Name:</label> <input id="firstnameField" type="text" name="firstname" /></p>
+ <p><label for="lastnameField">Last Name:</label> <input id="lastnameField" type="text" name="lastname" /></p>
+ <p><label for="emailField">Email:</label> <input id="emailField" type="text" name="email" /></p>
+ <p><label for="phoneField">Phone:</label> <input id="phoneField" type="text" name="phone" /></p>
+ <p>
+ Foursquare Member:
+ <label><input type="radio" name="member" value="yes" id="memberYes"> Yes</label>
+ <label><input type="radio" name="member" value="no" id="memberNo"> No</label>
+ </p>
+ <p>
+ Question:<br />
+ <textarea name="question" rows="10" cols="80"></textarea>
+ </p>
+ <p><input type="submit" value="Send" /></p>
+ </form>
+
+ </@textcontent>
+
+</@commonpage>
+
+
diff --git a/src/templates/pages/index.html.ftl b/src/templates/pages/index.html.ftl
index 4e6ea73..2788e8f 100644
--- a/src/templates/pages/index.html.ftl
+++ b/src/templates/pages/index.html.ftl
@@ -15,13 +15,16 @@
<h1>Grow "Buckets"</h1>
<p>
- Curabitur mattis molestie ligula, ac vestibulum Curabitur
- mattis facillisis vel. Iacus facillisis vel. Nam dignissim
- massa luctus ipsum adipiscing dignissim.
+ We want to help you GROW
+ </p>
+ <p>
+ GROW process is an on-line application and network to help you GROW
+ as a follower of Jesus Christ.
+ </p>
+ Let’s join together in a spiritual formation journey and begin the
+ discipleship process that will transform your life.
</p>
</@content>
- <div id="getstarted">
- <a class="greenbutton" href="index.html">Get Started! &#x2799;</a>
- </div>
+ <#include "/templates/getstarted-button.ftl">
</@commonpage>
diff --git a/src/templates/pages/login.html.ftl b/src/templates/pages/login.html.ftl
index 590649c..2a27858 100644
--- a/src/templates/pages/login.html.ftl
+++ b/src/templates/pages/login.html.ftl
@@ -4,7 +4,7 @@
<@commonpage>
<@noticebox>
<#if errorMessage??>
- ${errorMessage}
+ ${errorMessage?html}
<#else>
Welcome!
</#if>
@@ -12,7 +12,9 @@
<@content>
<p>Welcome! You will need to login with your Foursquare Church InFellowship login.</p>
- <form action="${dynamicRoot}/login.html" method="post">
+ <p>If you do not already have an account,
+ <a href="${dynamicRoot}/newaccount.html">create one here</a>.</p>
+ <form action="${dynamicRoot}/account/authenticate?redirect=${redirect!""}" method="post">
<p><label for="emailField">Email:</label> <input id="emailField" type="text" name="email" /></p>
<p><label for="passwordField">Password:</label> <input id="passwordField" type="password" name="password" /></p>
<p><input type="submit" value="Login" /></p>
diff --git a/src/templates/pages/newaccount.html.ftl b/src/templates/pages/newaccount.html.ftl
new file mode 100644
index 0000000..780a5c8
--- /dev/null
+++ b/src/templates/pages/newaccount.html.ftl
@@ -0,0 +1,26 @@
+<#include "/macros/common.ftl">
+<#include "/macros/common-page.ftl">
+
+<@commonpage>
+ <@noticebox>
+ <#if errorMessage??>
+ ${errorMessage?html}
+ <#else>
+ Welcome!
+ </#if>
+ </@noticebox>
+
+ <@content>
+ <p>
+ Fill out the form below to create a new Puyallup Foursquare InFellowship account.
+ </p>
+ <form action="${dynamicRoot}/createaccount" method="post">
+ <p><label for="firstnameField">First Name:</label> <input id="firstnameField" type="text" name="firstname" /></p>
+ <p><label for="lastnameField">Last Name:</label> <input id="lastnameField" type="text" name="lastname" /></p>
+ <p><label for="emailField">Email:</label> <input id="emailField" type="text" name="email" /></p>
+ <p><input type="submit" value="Create Account" /></p>
+ </form>
+ </@content>
+</@commonpage>
+
+
diff --git a/src/templates/pages/verification.html.ftl b/src/templates/pages/verification.html.ftl
new file mode 100644
index 0000000..e81b005
--- /dev/null
+++ b/src/templates/pages/verification.html.ftl
@@ -0,0 +1,17 @@
+<#include "/macros/common.ftl">
+<#include "/macros/common-page.ftl">
+
+<@commonpage>
+ <@noticebox>
+ </@noticebox>
+
+ <@content>
+ <p>
+ We have sent you a verification email.
+ You will be taken back to the login page after you activate your account.
+ </p>
+ </@content>
+</@commonpage>
+
+
+
diff --git a/src/templates/templates/assessment-results.ftl b/src/templates/templates/assessment-results.ftl
new file mode 100644
index 0000000..98918ba
--- /dev/null
+++ b/src/templates/templates/assessment-results.ftl
@@ -0,0 +1,32 @@
+<#include "/macros/common.ftl">
+<#include "/macros/common-page.ftl">
+
+<@commonpage>
+ <@noticebox>
+ </@noticebox>
+
+ <@textcontent>
+ <p>Congratulations for completing your GROW assessment!</p>
+
+ <p>Based on your responses you have been identified as a ${stage?cap_first}.</p>
+
+ <p>
+ So what’s next? Now you begin the process of GROWing. The button
+ below will take you to the ${stage?cap_first} page.
+ </p>
+
+ <p>Here you will find everything you need to begin the GROW process and start your journey.</p>
+
+ <p>
+ We are genuinely excited for you. Each phase of the GROW process
+ will produce positive quantifiable and quality results in your life, as
+ you learn, and then apply this learning in your life.
+ </p>
+ </@textcontent>
+
+ <div id="getstarted">
+ <a class="greenbutton" href="${dynamicRoot}/account/training/${stage?lower_case}">Begin GROWing &#x2799;</a>
+ </div>
+</@commonpage>
+
+
diff --git a/src/templates/templates/getstarted-button.ftl b/src/templates/templates/getstarted-button.ftl
new file mode 100644
index 0000000..b0baaee
--- /dev/null
+++ b/src/templates/templates/getstarted-button.ftl
@@ -0,0 +1,7 @@
+<div id="getstarted">
+ <#if user??>
+ <a class="greenbutton" href="${dynamicRoot}/account">Get Started! &#x2799;</a>
+ <#else>
+ <a class="greenbutton" href="login.html">Get Started! &#x2799;</a>
+ </#if>
+</div>
diff --git a/src/templates/templates/index-hero.ftl b/src/templates/templates/index-hero.ftl
index a386ac8..f447d53 100644
--- a/src/templates/templates/index-hero.ftl
+++ b/src/templates/templates/index-hero.ftl
@@ -1,7 +1,4 @@
<div id="hero">
<h1>We want to help you Grow.</h1>
- <p>Grow Process is an online application and network that exists to
- help you grow closter to God. Morbi iaculis turpis sit amet
- vehicula sollicitudin, enim dolor condimentum</p>
</div>
diff --git a/src/templates/templates/stage-complete.ftl b/src/templates/templates/stage-complete.ftl
new file mode 100644
index 0000000..ed04681
--- /dev/null
+++ b/src/templates/templates/stage-complete.ftl
@@ -0,0 +1,56 @@
+<#include "/macros/common.ftl">
+<#include "/macros/common-page.ftl">
+
+<@commonpage>
+ <@noticebox>
+ </@noticebox>
+
+ <@textcontent>
+ <h1>Congratulations!</h1>
+
+ <p>
+ Congratulations on the completion of the ${stage?cap_first} level of the GROW process.
+ </p>
+
+ <p>
+ We strongly encourage you move to the next stage of GROW. However,
+ before you do that, let’s review a key component to ensure you
+ experience the transformation and benefits of what you have already
+ learned: Implementation.
+ </p>
+
+ <p>
+ Learning is important, we should all be continuous learners,
+ otherwise we stagnate.
+ </p>
+
+ <p>
+ However to learn new information and then not implement or employ
+ that learning in our life, will make us smarter but not necessarily
+ wiser or better.
+ </p>
+
+ <p>
+ For many there is a “knowing, doing gap” in their lives. They know
+ a lot, but they don’t do anything with what they know, and they remain
+ “stuck”.
+ </p>
+
+ <p>
+ We know that for your new knowledge and learning to have an impact
+ on your life you must implement it.
+ </p>
+
+ <p>
+ You were given some tips for how to act on the new knowledge you
+ have. We encourage you to be proactive and intentional in closing the
+ gap between what you know and what you do, to experience all that God
+ has for you.
+ </p>
+
+ </@textcontent>
+
+ <div id="getstarted">
+ <a class="greenbutton" href="${dynamicRoot}/account/training/${nextstage}">Continue GROWing &#x2799;</a>
+ </div>
+</@commonpage>
diff --git a/web/favicon.ico b/web/favicon.ico
new file mode 100644
index 0000000..200f311
--- /dev/null
+++ b/web/favicon.ico
Binary files differ
diff --git a/web/style.css b/web/style.css
index 09cb5be..39d6756 100644
--- a/web/style.css
+++ b/web/style.css
@@ -118,6 +118,10 @@ nav.primary a.current {
margin: 1em auto 1em auto;
}
+#content article.text {
+ text-align: left;
+}
+
#content h1 {
color: #696969;
font-size: 24pt;