diff options
41 files changed, 1713 insertions, 98 deletions
@@ -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" /> @@ -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..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); + } +} 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/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! ➙</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 ➙</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! ➙</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 ➙</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! ➙</a> + <#else> + <a class="greenbutton" href="login.html">Get Started! ➙</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 ➙</a> + </div> +</@commonpage> diff --git a/web/favicon.ico b/web/favicon.ico Binary files differnew file mode 100644 index 0000000..200f311 --- /dev/null +++ b/web/favicon.ico 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; |