summaryrefslogtreecommitdiff
path: root/src/main/java/com/p4square/f1oauth
diff options
context:
space:
mode:
authorJesse Morgan <jesse@jesterpm.net>2016-04-09 14:22:20 -0700
committerJesse Morgan <jesse@jesterpm.net>2016-04-09 15:48:01 -0700
commit3102d8bce3426d9cf41aeaf201c360d342677770 (patch)
tree38c4f1e8828f9af9c4b77a173bee0d312b321698 /src/main/java/com/p4square/f1oauth
parentbbf907e51dfcf157bdee24dead1d531122aa25db (diff)
Switching from Ivy+Ant to Maven.
Diffstat (limited to 'src/main/java/com/p4square/f1oauth')
-rw-r--r--src/main/java/com/p4square/f1oauth/Attribute.java90
-rw-r--r--src/main/java/com/p4square/f1oauth/F1API.java56
-rw-r--r--src/main/java/com/p4square/f1oauth/F1Access.java594
-rw-r--r--src/main/java/com/p4square/f1oauth/F1Exception.java15
-rw-r--r--src/main/java/com/p4square/f1oauth/F1ProgressReporter.java57
-rw-r--r--src/main/java/com/p4square/f1oauth/F1User.java70
-rw-r--r--src/main/java/com/p4square/f1oauth/FellowshipOneIntegrationDriver.java55
-rw-r--r--src/main/java/com/p4square/f1oauth/SecondPartyAuthenticator.java52
-rw-r--r--src/main/java/com/p4square/f1oauth/SecondPartyVerifier.java72
9 files changed, 1061 insertions, 0 deletions
diff --git a/src/main/java/com/p4square/f1oauth/Attribute.java b/src/main/java/com/p4square/f1oauth/Attribute.java
new file mode 100644
index 0000000..64f2507
--- /dev/null
+++ b/src/main/java/com/p4square/f1oauth/Attribute.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2014 Jesse Morgan
+ */
+
+package com.p4square.f1oauth;
+
+import java.util.Date;
+
+/**
+ * F1 Attribute Data.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class Attribute {
+ private final String mAttributeName;
+ private String mId;
+ private Date mStartDate;
+ private Date mEndDate;
+ private String mComment;
+
+ /**
+ * @param name The attribute name.
+ */
+ public Attribute(final String name) {
+ mAttributeName = name;
+ }
+
+ /**
+ * @return the Attribute name.
+ */
+ public String getAttributeName() {
+ return mAttributeName;
+ }
+
+ /**
+ * @return the id of this specific attribute instance.
+ */
+ public String getId() {
+ return mId;
+ }
+
+ /**
+ * Set the attribute id to id.
+ */
+ public void setId(final String id) {
+ mId = id;
+ }
+
+ /**
+ * @return the start date for the attribute.
+ */
+ public Date getStartDate() {
+ return mStartDate;
+ }
+
+ /**
+ * Set the start date for the attribute.
+ */
+ public void setStartDate(final Date date) {
+ mStartDate = date;
+ }
+
+ /**
+ * @return the end date for the attribute.
+ */
+ public Date getEndDate() {
+ return mEndDate;
+ }
+
+ /**
+ * Set the end date for the attribute.
+ */
+ public void setEndDate(final Date date) {
+ mEndDate = date;
+ }
+
+ /**
+ * @return The comment on the Attribute.
+ */
+ public String getComment() {
+ return mComment;
+ }
+
+ /**
+ * Set the comment on the attribute.
+ */
+ public void setComment(final String comment) {
+ mComment = comment;
+ }
+}
diff --git a/src/main/java/com/p4square/f1oauth/F1API.java b/src/main/java/com/p4square/f1oauth/F1API.java
new file mode 100644
index 0000000..a525c3f
--- /dev/null
+++ b/src/main/java/com/p4square/f1oauth/F1API.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2014 Jesse Morgan
+ */
+
+package com.p4square.f1oauth;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+import com.p4square.restlet.oauth.OAuthException;
+import com.p4square.restlet.oauth.OAuthUser;
+
+/**
+ * F1 API methods which require an authenticated user.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public interface F1API {
+ /**
+ * Fetch information about a user.
+ *
+ * @param user The user to fetch information about.
+ * @return An F1User object.
+ */
+ F1User getF1User(OAuthUser user) throws OAuthException, IOException;
+
+ /**
+ * Fetch a list of all attributes ids and names.
+ *
+ * @return A Map of attribute name to attribute id.
+ */
+ Map<String, String> getAttributeList() throws F1Exception;
+
+ /**
+ * Add an attribute to the user.
+ *
+ * @param user The user to add the attribute to.
+ * @param attributeName The attribute to add.
+ * @param attribute The attribute to add.
+ */
+ boolean addAttribute(String userId, Attribute attribute) throws F1Exception;
+
+ /**
+ * Return attributes assigned to user.
+ *
+ * A user may be assigned multiple attributes with the same name, thus even if
+ * attributeName is specified, multiple attributes may be returned.
+ *
+ * @param userId The user to query.
+ * @param attributeName A specific attribute to return, null for all.
+ * @return A list of Attributes
+ */
+ List<Attribute> getAttribute(String userId, String attributeName) throws F1Exception;
+
+}
diff --git a/src/main/java/com/p4square/f1oauth/F1Access.java b/src/main/java/com/p4square/f1oauth/F1Access.java
new file mode 100644
index 0000000..c3307f1
--- /dev/null
+++ b/src/main/java/com/p4square/f1oauth/F1Access.java
@@ -0,0 +1,594 @@
+/*
+ * Copyright 2014 Jesse Morgan
+ */
+
+package com.p4square.f1oauth;
+
+import java.io.IOException;
+import java.net.URLEncoder;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.codahale.metrics.Counter;
+import com.codahale.metrics.MetricRegistry;
+import com.codahale.metrics.Timer;
+
+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.MediaType;
+import org.restlet.data.Method;
+import org.restlet.data.Status;
+import org.restlet.engine.util.Base64;
+import org.restlet.ext.jackson.JacksonRepresentation;
+import org.restlet.representation.Representation;
+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;
+
+/**
+ * F1 API Access.
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class F1Access {
+ public enum UserType {
+ WEBLINK, PORTAL;
+ }
+
+ private static final Logger LOG = Logger.getLogger(F1Access.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 static final SimpleDateFormat DATE_FORMAT =
+ new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
+
+ private final String mBaseUrl;
+ private final String mMethod;
+
+ private final OAuthHelper mOAuthHelper;
+
+ private final Map<String, String> mAttributeIdByName;
+
+ private MetricRegistry mMetricRegistry;
+
+ /**
+ */
+ public F1Access(Context context, String consumerKey, String consumerSecret,
+ String baseUrl, String churchCode, UserType userType) {
+
+ switch (userType) {
+ case WEBLINK:
+ mMethod = "WeblinkUser";
+ break;
+ case PORTAL:
+ mMethod = "PortalUser";
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown UserType");
+ }
+
+ mBaseUrl = "https://" + churchCode + "." + baseUrl + VERSION_STRING;
+
+ // Create the OAuthHelper. This implicitly registers the helper to
+ // handle outgoing requests which need OAuth authentication.
+ mOAuthHelper = new OAuthHelper(context, consumerKey, consumerSecret) {
+ @Override
+ protected String getRequestTokenUrl() {
+ return mBaseUrl + REQUESTTOKEN_URL;
+ }
+
+ @Override
+ 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;
+ }
+
+ @Override
+ protected String getAccessTokenUrl() {
+ return mBaseUrl + ACCESSTOKEN_URL;
+ }
+ };
+
+ mAttributeIdByName = new HashMap<>();
+ }
+
+ /**
+ * Set the MetricRegistry to get metrics recorded.
+ */
+ public void setMetricRegistry(MetricRegistry metrics) {
+ mMetricRegistry = metrics;
+ }
+
+ /**
+ * 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 {
+ Timer.Context timer = getTimer("F1Access.getAccessToken.time");
+ boolean success = true;
+
+ try {
+ 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 mOAuthHelper.processAccessTokenRequest(request);
+
+ } catch (Exception e) {
+ success = false;
+ throw e;
+
+ } finally {
+ if (timer != null) {
+ timer.stop();
+ }
+ if (success) {
+ incrementCounter("F1Access.getAccessToken.success");
+ } else {
+ incrementCounter("F1Access.getAccessToken.failure");
+ }
+ }
+ }
+
+ /**
+ * Create a new Account.
+ *
+ * @param firstname The user's first name.
+ * @param lastname The user's last name.
+ * @param email The user's email address.
+ * @param redirect The URL to send the user to after confirming his address.
+ *
+ * @return true if created, false if the account already exists.
+ */
+ public boolean createAccount(String firstname, String lastname, String email, String redirect)
+ throws OAuthException {
+ Timer.Context timer = getTimer("F1Access.createAccount.time");
+ boolean success = true;
+
+ try {
+ 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, MediaType.APPLICATION_JSON));
+
+ Response response = mOAuthHelper.getResponse(request);
+
+ Status status = response.getStatus();
+ if (Status.SUCCESS_NO_CONTENT.equals(status)) {
+ return true;
+
+ } else if (Status.CLIENT_ERROR_CONFLICT.equals(status)) {
+ return false;
+
+ } else {
+ throw new OAuthException(status);
+ }
+
+ } catch (Exception e) {
+ success = false;
+ throw e;
+
+ } finally {
+ if (timer != null) {
+ timer.stop();
+ }
+ if (success) {
+ incrementCounter("F1Access.createAccount.success");
+ } else {
+ incrementCounter("F1Access.createAccount.failure");
+ }
+ }
+ }
+
+ /**
+ * @return An F1API authenticated by the given user.
+ */
+ public F1API getAuthenticatedApi(OAuthUser user) {
+ return new AuthenticatedApi(user);
+ }
+
+ private class AuthenticatedApi implements F1API {
+ private final OAuthUser mUser;
+
+ public AuthenticatedApi(OAuthUser user) {
+ mUser = user;
+ }
+
+ /**
+ * Fetch information about a user.
+ *
+ * @param user The user to fetch information about.
+ * @return An F1User object.
+ */
+ @Override
+ public F1User getF1User(OAuthUser user) throws OAuthException, IOException {
+ Timer.Context timer = getTimer("F1Access.getF1User.time");
+ boolean success = true;
+
+ try {
+ Request request = new Request(Method.GET, user.getLocation() + ".json");
+ request.setChallengeResponse(mUser.getChallengeResponse());
+ Response response = mOAuthHelper.getResponse(request);
+
+ try {
+ Status status = response.getStatus();
+ if (status.isSuccess()) {
+ JacksonRepresentation<Map> entity =
+ new JacksonRepresentation<Map>(response.getEntity(), Map.class);
+ Map data = entity.getObject();
+ return new F1User(user, data);
+
+ } else {
+ throw new OAuthException(status);
+ }
+
+ } finally {
+ if (response.getEntity() != null) {
+ response.release();
+ }
+ }
+
+ } catch (Exception e) {
+ success = false;
+ throw e;
+
+ } finally {
+ if (timer != null) {
+ timer.stop();
+ }
+ if (success) {
+ incrementCounter("F1Access.getF1User.success");
+ } else {
+ incrementCounter("F1Access.getF1User.failure");
+ }
+ }
+ }
+
+ @Override
+ public Map<String, String> getAttributeList() throws F1Exception {
+ // Note: this list is shared by all F1 users.
+ synchronized (mAttributeIdByName) {
+ if (mAttributeIdByName.size() == 0) {
+ Timer.Context timer = getTimer("F1Access.getAttributeList.time");
+ boolean success = true;
+
+ try {
+ // Reload attributes. Maybe it will be there now...
+ Request request = new Request(Method.GET,
+ mBaseUrl + "People/AttributeGroups.json");
+ request.setChallengeResponse(mUser.getChallengeResponse());
+ Response response = mOAuthHelper.getResponse(request);
+
+ Representation representation = response.getEntity();
+ try {
+ Status status = response.getStatus();
+ if (status.isSuccess()) {
+ JacksonRepresentation<Map> entity =
+ new JacksonRepresentation<Map>(response.getEntity(), Map.class);
+
+ Map attributeGroups = (Map) entity.getObject().get("attributeGroups");
+ List<Map> groups = (List<Map>) attributeGroups.get("attributeGroup");
+
+ for (Map group : groups) {
+ List<Map> attributes = (List<Map>) group.get("attribute");
+ if (attributes != null) {
+ for (Map attribute : attributes) {
+ String id = (String) attribute.get("@id");
+ String name = ((String) attribute.get("name"));
+ mAttributeIdByName.put(name.toLowerCase(), id);
+ LOG.debug("Caching attribute '" + name
+ + "' with id '" + id + "'");
+ }
+ }
+ }
+ }
+
+ } catch (IOException e) {
+ throw new F1Exception("Could not parse AttributeGroups.", e);
+
+ } finally {
+ if (representation != null) {
+ representation.release();
+ }
+ }
+
+ } catch (Exception e) {
+ success = false;
+ throw e;
+
+ } finally {
+ if (timer != null) {
+ timer.stop();
+ }
+ if (success) {
+ incrementCounter("F1Access.getAttributeList.success");
+ } else {
+ incrementCounter("F1Access.getAttributeList.failure");
+ }
+ }
+ }
+
+ return mAttributeIdByName;
+ }
+ }
+
+ /**
+ * Add an attribute to the user.
+ *
+ * @param user The user to add the attribute to.
+ * @param attributeName The attribute to add.
+ * @param attribute The attribute to add.
+ */
+ public boolean addAttribute(String userId, Attribute attribute)
+ throws F1Exception {
+
+ // Get the attribute id.
+ String attributeId = getAttributeId(attribute.getAttributeName());
+ if (attributeId == null) {
+ throw new F1Exception("Could not find id for " + attribute.getAttributeName());
+ }
+
+ // Get Attribute Template
+ Map attributeTemplate = null;
+
+ Timer.Context timer = getTimer("F1Access.addAttribute.GET.time");
+ boolean success = true;
+
+ try {
+ Request request = new Request(Method.GET,
+ mBaseUrl + "People/" + userId + "/Attributes/new.json");
+ request.setChallengeResponse(mUser.getChallengeResponse());
+ Response response = mOAuthHelper.getResponse(request);
+
+ Representation representation = response.getEntity();
+ try {
+ Status status = response.getStatus();
+ if (status.isSuccess()) {
+ JacksonRepresentation<Map> entity =
+ new JacksonRepresentation<Map>(response.getEntity(), Map.class);
+ attributeTemplate = entity.getObject();
+
+ } else {
+ throw new F1Exception("Failed to retrieve attribute template: "
+ + status);
+ }
+
+ } catch (IOException e) {
+ throw new F1Exception("Could not parse attribute template.", e);
+
+ } finally {
+ if (representation != null) {
+ representation.release();
+ }
+ }
+ } catch (Exception e) {
+ success = false;
+ throw e;
+
+ } finally {
+ if (timer != null) {
+ timer.stop();
+ }
+ if (success) {
+ incrementCounter("F1Access.addAttribute.GET.success");
+ } else {
+ incrementCounter("F1Access.addAttribute.GET.failure");
+ }
+ }
+
+ if (attributeTemplate == null) {
+ throw new F1Exception("Could not retrieve attribute template.");
+ }
+
+ // Populate Attribute Template
+ Map attributeMap = (Map) attributeTemplate.get("attribute");
+ Map attributeGroup = (Map) attributeMap.get("attributeGroup");
+
+ Map<String, String> attributeIdMap = new HashMap<>();
+ attributeIdMap.put("@id", attributeId);
+ attributeGroup.put("attribute", attributeIdMap);
+
+ if (attribute.getStartDate() != null) {
+ attributeMap.put("startDate", DATE_FORMAT.format(attribute.getStartDate()));
+ }
+
+ if (attribute.getStartDate() != null) {
+ attributeMap.put("endDate", DATE_FORMAT.format(attribute.getStartDate()));
+ }
+
+ attributeMap.put("comment", attribute.getComment());
+
+ // POST new attribute
+ Status status;
+ timer = getTimer("F1Access.addAttribute.POST.time");
+ success = true;
+
+ try {
+ Request request = new Request(Method.POST,
+ mBaseUrl + "People/" + userId + "/Attributes.json");
+ request.setChallengeResponse(mUser.getChallengeResponse());
+ request.setEntity(new JacksonRepresentation<Map>(attributeTemplate));
+ Response response = mOAuthHelper.getResponse(request);
+
+ Representation representation = response.getEntity();
+ try {
+ status = response.getStatus();
+
+ if (status.isSuccess()) {
+ return true;
+ }
+
+ } finally {
+ if (representation != null) {
+ representation.release();
+ }
+ }
+ } catch (Exception e) {
+ success = false;
+ throw e;
+
+ } finally {
+ if (timer != null) {
+ timer.stop();
+ }
+ if (success) {
+ incrementCounter("F1Access.addAttribute.POST.success");
+ } else {
+ incrementCounter("F1Access.getAccessToken.POST.failure");
+ }
+ }
+
+ LOG.debug("addAttribute failed POST: " + status);
+ return false;
+ }
+
+ @Override
+ public List<Attribute> getAttribute(String userId, String attributeNameFilter)
+ throws F1Exception {
+
+ Map attributesResponse;
+
+ // Get Attributes
+ Timer.Context timer = getTimer("F1Access.getAttribute.time");
+ boolean success = true;
+
+ try {
+ Request request = new Request(Method.GET,
+ mBaseUrl + "People/" + userId + "/Attributes.json");
+ request.setChallengeResponse(mUser.getChallengeResponse());
+ Response response = mOAuthHelper.getResponse(request);
+
+ Representation representation = response.getEntity();
+ try {
+ Status status = response.getStatus();
+ if (status.isSuccess()) {
+ JacksonRepresentation<Map> entity =
+ new JacksonRepresentation<Map>(response.getEntity(), Map.class);
+ attributesResponse = entity.getObject();
+
+ } else {
+ throw new F1Exception("Failed to retrieve attributes: "
+ + status);
+ }
+
+ } catch (IOException e) {
+ throw new F1Exception("Could not parse attributes.", e);
+
+ } finally {
+ if (representation != null) {
+ representation.release();
+ }
+ }
+ } catch (Exception e) {
+ success = false;
+ throw e;
+
+ } finally {
+ if (timer != null) {
+ timer.stop();
+ }
+ if (success) {
+ incrementCounter("F1Access.getAttribute.success");
+ } else {
+ incrementCounter("F1Access.getAttribute.failure");
+ }
+ }
+
+ // Parse Response
+ List<Attribute> result = new ArrayList<>();
+
+ try {
+ // I feel like I'm writing lisp here...
+ Map attributesMap = (Map) attributesResponse.get("attributes");
+ if (attributesMap == null) {
+ return result;
+ }
+
+ List<Map> attributes = (List<Map>) (attributesMap).get("attribute");
+ for (Map attributeMap : attributes) {
+ String id = (String) attributeMap.get("@id");
+ String startDate = (String) attributeMap.get("startDate");
+ String endDate = (String) attributeMap.get("endDate");
+ String comment = (String) attributeMap.get("comment");
+
+ Map attributeIdMap = (Map) ((Map) attributeMap.get("attributeGroup"))
+ .get("attribute");
+ String attributeName = (String) attributeIdMap.get("name");
+
+ if (attributeNameFilter == null
+ || attributeNameFilter.equalsIgnoreCase(attributeName)) {
+
+ Attribute attribute = new Attribute(attributeName);
+ attribute.setId(id);
+ if (startDate != null) {
+ attribute.setStartDate(DATE_FORMAT.parse(startDate));
+ }
+ if (endDate != null) {
+ attribute.setEndDate(DATE_FORMAT.parse(endDate));
+ }
+ attribute.setComment(comment);
+ result.add(attribute);
+ }
+ }
+ } catch (Exception e) {
+ throw new F1Exception("Failed to parse attributes response.", e);
+ }
+
+ return result;
+ }
+
+ /**
+ * @return an attribute id for the given attribute name.
+ */
+ private String getAttributeId(String attributeName) throws F1Exception {
+ Map<String, String> attributeMap = getAttributeList();
+
+ return attributeMap.get(attributeName.toLowerCase());
+ }
+
+ }
+
+ private Timer.Context getTimer(String name) {
+ if (mMetricRegistry != null) {
+ return mMetricRegistry.timer(name).time();
+ } else {
+ return null;
+ }
+ }
+
+ private void incrementCounter(String name) {
+ if (mMetricRegistry != null) {
+ mMetricRegistry.counter(name).inc();
+ }
+ }
+}
diff --git a/src/main/java/com/p4square/f1oauth/F1Exception.java b/src/main/java/com/p4square/f1oauth/F1Exception.java
new file mode 100644
index 0000000..54c1a77
--- /dev/null
+++ b/src/main/java/com/p4square/f1oauth/F1Exception.java
@@ -0,0 +1,15 @@
+/*
+ * Copyright 2014 Jesse Morgan
+ */
+
+package com.p4square.f1oauth;
+
+public class F1Exception extends Exception {
+ public F1Exception(String message) {
+ super(message);
+ }
+
+ public F1Exception(String message, Exception cause) {
+ super(message, cause);
+ }
+}
diff --git a/src/main/java/com/p4square/f1oauth/F1ProgressReporter.java b/src/main/java/com/p4square/f1oauth/F1ProgressReporter.java
new file mode 100644
index 0000000..8382020
--- /dev/null
+++ b/src/main/java/com/p4square/f1oauth/F1ProgressReporter.java
@@ -0,0 +1,57 @@
+package com.p4square.f1oauth;
+
+import com.p4square.grow.frontend.ProgressReporter;
+import org.apache.log4j.Logger;
+import org.restlet.security.User;
+
+import java.util.Date;
+
+/**
+ * A ProgressReporter implementation to record progress in F1.
+ */
+public class F1ProgressReporter implements ProgressReporter {
+
+ private static final Logger LOG = Logger.getLogger(F1ProgressReporter.class);
+
+ private F1Access mF1Access;
+
+ public F1ProgressReporter(final F1Access f1access) {
+ mF1Access = f1access;
+ }
+
+ @Override
+ public void reportAssessmentComplete(final User user, final String level, final Date date, final String results) {
+ String attributeName = "Assessment Complete - " + level;
+ Attribute attribute = new Attribute(attributeName);
+ attribute.setStartDate(date);
+ attribute.setComment(results);
+ addAttribute(user, attribute);
+ }
+
+ @Override
+ public void reportChapterComplete(final User user, final String chapter, final Date date) {
+ final String attributeName = "Training Complete - " + chapter;
+ final Attribute attribute = new Attribute(attributeName);
+ attribute.setStartDate(date);
+ addAttribute(user, attribute);
+ }
+
+ private void addAttribute(final User user, final Attribute attribute) {
+ if (!(user instanceof F1User)) {
+ throw new IllegalArgumentException("User must be an F1User, but got " + user.getClass().getName());
+ }
+
+ try {
+ final F1User f1User = (F1User) user;
+ final F1API f1 = mF1Access.getAuthenticatedApi(f1User);
+
+ if (!f1.addAttribute(user.getIdentifier(), attribute)) {
+ LOG.error("addAttribute failed for " + user.getIdentifier() + " with attribute "
+ + attribute.getAttributeName());
+ }
+ } catch (Exception e) {
+ LOG.error("addAttribute failed for " + user.getIdentifier() + " with attribute "
+ + attribute.getAttributeName(), e);
+ }
+ }
+}
diff --git a/src/main/java/com/p4square/f1oauth/F1User.java b/src/main/java/com/p4square/f1oauth/F1User.java
new file mode 100644
index 0000000..e5ab487
--- /dev/null
+++ b/src/main/java/com/p4square/f1oauth/F1User.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2014 Jesse Morgan
+ */
+
+package com.p4square.f1oauth;
+
+import java.util.Map;
+
+import com.p4square.restlet.oauth.OAuthException;
+import com.p4square.restlet.oauth.OAuthUser;
+
+/**
+ *
+ * @author Jesse Morgan <jesse@jesterpm.net>
+ */
+public class F1User extends OAuthUser {
+ public static final String ID = "@id";
+ public static final String FIRST_NAME = "firstName";
+ public static final String LAST_NAME = "lastName";
+ public static final String ICODE = "@iCode";
+
+ private final Map mData;
+
+ /**
+ * Copy the user information from user into a new F1User.
+ *
+ * @param user Original user.
+ * @param data F1 Person Record.
+ * @throws IllegalStateException if data.get("person") is null.
+ */
+ public F1User(OAuthUser user, Map data) {
+ super(user.getLocation(), user.getToken());
+
+ mData = (Map) data.get("person");
+ if (mData == null) {
+ throw new IllegalStateException("Bad data");
+ }
+
+ setIdentifier(getString(ID));
+ setFirstName(getString(FIRST_NAME));
+ setLastName(getString(LAST_NAME));
+ }
+
+ /**
+ * Get a String from the map.
+ *
+ * @param key The map key.
+ * @return The value associated with the key, or null.
+ */
+ public String getString(String key) {
+ Object blob = get(key);
+
+ if (blob instanceof String) {
+ return (String) blob;
+
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Fetch an object from the F1 record.
+ *
+ * @param key The map key
+ * @return The object in the map or null.
+ */
+ public Object get(String key) {
+ return mData.get(key);
+ }
+}
diff --git a/src/main/java/com/p4square/f1oauth/FellowshipOneIntegrationDriver.java b/src/main/java/com/p4square/f1oauth/FellowshipOneIntegrationDriver.java
new file mode 100644
index 0000000..865f5d6
--- /dev/null
+++ b/src/main/java/com/p4square/f1oauth/FellowshipOneIntegrationDriver.java
@@ -0,0 +1,55 @@
+package com.p4square.f1oauth;
+
+import com.codahale.metrics.MetricRegistry;
+import com.p4square.grow.config.Config;
+import com.p4square.grow.frontend.IntegrationDriver;
+import com.p4square.grow.frontend.ProgressReporter;
+import org.restlet.Context;
+import org.restlet.security.Verifier;
+
+/**
+ * The FellowshipOneIntegrationDriver creates implementations of various
+ * objects to support integration with Fellowship One.
+ */
+public class FellowshipOneIntegrationDriver implements IntegrationDriver {
+
+ private final Context mContext;
+ private final MetricRegistry mMetricRegistry;
+ private final Config mConfig;
+ private final F1Access mAPI;
+
+ private final ProgressReporter mProgressReporter;
+
+ public FellowshipOneIntegrationDriver(final Context context) {
+ mContext = context;
+ mConfig = (Config) context.getAttributes().get("com.p4square.grow.config");
+ mMetricRegistry = (MetricRegistry) context.getAttributes().get("com.p4square.grow.metrics");
+
+ mAPI = new F1Access(context,
+ mConfig.getString("f1ConsumerKey", ""),
+ mConfig.getString("f1ConsumerSecret", ""),
+ mConfig.getString("f1BaseUrl", "staging.fellowshiponeapi.com"),
+ mConfig.getString("f1ChurchCode", "pfseawa"),
+ F1Access.UserType.WEBLINK);
+ mAPI.setMetricRegistry(mMetricRegistry);
+
+ mProgressReporter = new F1ProgressReporter(mAPI);
+ }
+
+ /**
+ * @return An F1Access instance.
+ */
+ public F1Access getF1Access() {
+ return mAPI;
+ }
+
+ @Override
+ public Verifier newUserAuthenticationVerifier() {
+ return new SecondPartyVerifier(mContext, mAPI);
+ }
+
+ @Override
+ public ProgressReporter getProgressReporter() {
+ return mProgressReporter;
+ }
+}
diff --git a/src/main/java/com/p4square/f1oauth/SecondPartyAuthenticator.java b/src/main/java/com/p4square/f1oauth/SecondPartyAuthenticator.java
new file mode 100644
index 0000000..8deefec
--- /dev/null
+++ b/src/main/java/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 F1Access mHelper;
+
+ public SecondPartyAuthenticator(Context context, boolean optional, F1Access 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/main/java/com/p4square/f1oauth/SecondPartyVerifier.java b/src/main/java/com/p4square/f1oauth/SecondPartyVerifier.java
new file mode 100644
index 0000000..882c7e7
--- /dev/null
+++ b/src/main/java/com/p4square/f1oauth/SecondPartyVerifier.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2013 Jesse Morgan
+ */
+
+package com.p4square.f1oauth;
+
+import java.io.IOException;
+import java.util.Map;
+
+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.Restlet;
+import org.restlet.data.Method;
+import org.restlet.data.Status;
+import org.restlet.ext.jackson.JacksonRepresentation;
+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 Restlet mDispatcher;
+ private final F1Access mHelper;
+
+ public SecondPartyVerifier(Context context, F1Access helper) {
+ if (helper == null) {
+ throw new IllegalArgumentException("Helper can not be null.");
+ }
+
+ mDispatcher = context.getClientDispatcher();
+ 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 ouser = mHelper.getAccessToken(username, password);
+
+ // Once we have a user, fetch the people record to get the user id.
+ F1User user = mHelper.getAuthenticatedApi(ouser).getF1User(ouser);
+ user.setEmail(username);
+
+ // This seems like a hack... but it'll work
+ request.getClientInfo().setUser(user);
+
+ return RESULT_VALID;
+
+ } catch (Exception e) {
+ LOG.info("OAuth Exception: " + e, e);
+ }
+
+ return RESULT_INVALID; // Invalid credentials
+ }
+
+}