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;  | 
