IKANOW
This blog post was written by Caleb Burch, Product Engineer at Ikanow. Ikanow is a software organization that has created the world's first open source analytics platform.

This blog will demonstrate how we connected the Infinit.e platform's sign on with Nuxeo's to allow a seamless interaction (single sign on) for our clients using Nuxeo's case management capabilities. Infinit.e is an open source analytic platform designed for documents not database records of which it provides a wide range of activities including search, visualization, and data science. Our customers use the platform for financial fraud detection, cyber security, geopolitical analysis, marketing, and other fields involving large sets of unstructured data. We use Nuxeo as a case manager to allow users to map abstract concepts like data sources, query sets, statistical recommendations, and important entities back to "real life" artifacts like suspects, leads, or evidence. The need to communicate with Infinit.e and manage user/group permissions meant we had to develop a single-sign on capability between the platforms.

The typical interaction between the two platforms usually involves mapping some entity discovered in a document to our Case Visualizer (pictured below). This is a lot like a working canvas for analysis so users can store data while they work between queries, exploring data. Once the user decides an entity is important, the entity can be promoted to a target.

When a target is created, we auto create Nuxeo-documents (pictured below) for these targets so a user can manage them more easily. We also have some more integrated actions that allow users to kick off Infinit.e data collection based on the targets in Nuxeo.

Based on these typical user interactions, the goals of connecting our 2 platforms were to:


  1. Allow users to automatically move from our web application into Nuxeo without having log in twice

  2. Transfer permissions from the Infinit.e community system directly to Nuxeo groups (they are similar authentication schemes of grouping users into permission groups)


Steps to connect Infinit.e and Nuxeo:


  1. Create a custom Authenticator that implements NuxeoAuthenticationPlugin, NuxeoAuthenticationPluginLogoutExtension (This authenticator will check we have an active connection with Infinit.e, get our user groups, and log us out)

  2. Create a custom Login Plugin that implements LoginPlugin

  3. Create a custom configuration file that is external to our extensions so we can adjust it for different Nuxeo instances.

  4. Adjust the default authentication chain order for both default login and automation

  5. Deploy to your Nuxeo server

Infinit.e architecture


Infinit.e is a web application powered by a RESTful API service. We used three of our API calls to authenticate users and get their communities (groups).

We authenticate login via the Auth - Login. This is a restful call in which we return a cookie named "infinitecookie" that will be active for 15 minutes from the most recent API call (e.g. if you keep making subsequent API calls, the timer will be updated for 15 more minutes).

> Login Example




http://infinite.ikanow.com/api/auth/login/[email protected]/encryptedpasswordand returned in the response would be:





{ "response" : { "action" : "Login",
"success" : true,
"time" : 0
} }

your cookie will be attached named "infinitecookie":



Once logged we get a user and their groups by calling Social - Person - Get

> Get Person Example

http://infinite.ikanow.com/api/social/person/get

 { "data" : { "WPUserID" : "[email protected]",
 "_id" : "5069dd90e4b09156bad31aba",
 "accountStatus" : "active",
 "communities" : [ { "_id" : "5069dd90e4b09156bad31aba",
 "name" : "Caleb Burch's Personal Community"
 },
 { "_id" : "4c927585d591d31d7b37097a",
 "name" : "Infinit.e System"
 }
 ],
 "created" : "Oct 1, 2012 06:14:41 PM UTC",
 "displayName" : "Caleb Burch",
 "email" : "[email protected]",
 "firstName" : "Caleb",
 "lastName" : "Burch",
 "modified" : "Oct 25, 2013 02:59:21 PM UTC",
 "phone" : "0"
 },
 "response" : { "action" : "Person Info",
 "message" : "Person info returned successfully",
 "success" : true,
 "time" : 4
 }
 }

We also use the Auth-Keep Alive call, which just checks that an Infinit.e cookie is active, and adds another 15m of activity to the clock > Keep Alive Example
http://infinite.ikanow.com/api/auth/keepalive and if the cookie is valid the response will return something like:
{ "response" : { "action" : "Keep Alive", "message" : "Cookie kept alive, 15min left.", "success" : true, "time" : 1 } }

Overriding Nuxeo's default login architecture

Step 1: Create a custom Authenticator that implements NuxeoAuthenticationPlugin, NuxeoAuthenticationPluginLogoutExtension - this authenticator will check we have an active connection with Infinit.e, get our user groups, and log us out

We first needed to create our own Authenticator that will check if a user is logged into Infinit.e already, grab the user object, and create a user/groups in Nuxeo as necessary. We accomplished this by creating a new Nuxeo plugin and creating a class that implements both NuxeoAuthenticationPlugin and NuxeoAuthenticationPluginLogoutExtension, thus overriding those extension points. The methods we needed to override included:
  1. handleRetrieveIdentity This is the meat of the Authenticator. Here we check if a user has a valid cookie in the httpRequest, then we grab an Infinit.e user object with that cookie and create a Nuxeo user if one does not exist, as well as any Nuxeo groups that do not exist. After we create all that, a UserIdentificationInfo object is returned for that user. This object will be sent off to the Login Plugin to validate the account is valid, but we are going to over ride that to make sure it always thinks it is valid so we can just put whatever password we like.NOTE: On as user's first call to Nuxeo you may not yet have an active jsp session, so we must call httpRequest.getSession(true); to make sure one is created.
  1. initPlugin This method gets called when Nuxeo starts up, allows us to grab config params from the configuration file we are going to create later. (e.g. we get the Infinit.e API url and the Infinit.e login url so we can redirect unauthenticated users)
  2. needLoginPrompt Checks if an httprequest needs to be sent to handleLoginPrompt, we always return true
  3. handleLoginPrompt Here we check if a user is authenticated, if not we redirect them to the login url (we received from initPlugin). Return false if there is nothing to do
  4. getUnAuthenticatedURLPrefix Block any urls you don't want to allow access, we just return null here
  5. handleLogout Occurs when someone pushes the logout button in Nuxeo, we send a request to log the user out of Infinit.e and redirect them to the Infinit.e login page.
>InfiniteAuthenticator.java
import java.net.URLEncoder;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.ecm.core.api.DocumentModel;
import org.nuxeo.ecm.core.api.NuxeoPrincipal;
import org.nuxeo.ecm.platform.api.login.UserIdentificationInfo;
import org.nuxeo.ecm.platform.ui.web.auth.interfaces.NuxeoAuthenticationPlugin;
import org.nuxeo.ecm.platform.ui.web.auth.interfaces.NuxeoAuthenticationPluginLogoutExtension;
import org.nuxeo.ecm.platform.usermanager.UserManager;
import org.nuxeo.runtime.api.Framework;

import com.ikanow.infinit.e.data_model.api.ResponsePojo.ResponseObject;
import com.ikanow.infinit.e.data_model.driver.InfiniteDriver;
import com.ikanow.infinit.e.data_model.store.social.person.PersonCommunityPojo;
import com.ikanow.infinit.e.data_model.store.social.person.PersonPojo;

public class InfiniteAuthenticator implements NuxeoAuthenticationPlugin,
NuxeoAuthenticationPluginLogoutExtension {

private static final Log log = LogFactory
.getLog(InfiniteAuthenticator.class);

private String infinite_api_url = "";

private String infinite_login_url = "";

private final String INFINITE_COOKIE = "infinitecookie";

@Override
public UserIdentificationInfo handleRetrieveIdentity(
HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
Cookie cookie = getCookie(httpRequest, INFINITE_COOKIE);
PersonPojo user = getInfiniteUser(httpRequest, cookie);
if (user != null) {
String user_email = user.getEmail();
// try to get user, or create one if they don't exist
try {
getOrCreateUser(user_email, user.getCommunities());
} catch (Exception ex) {
log.error("Error get/create user", ex);
return null;
}

httpRequest.getSession(true); // create a session if one does not
// exist, was having some issues w/
// sessions breaking
UserIdentificationInfo uii = new UserIdentificationInfo(user_email,
""); // NO PASSWORD NECESSARY
return uii;
} else {
log.debug("Infinit.e Person was null");
return null;
}
}

private void getOrCreateUser(String username,
List<PersonCommunityPojo> communities) throws Exception {
UserManager userManager = Framework.getService(UserManager.class);
// set up groups:
String[] groups = new String[communities.size() + 1];
groups[0] = "members"; // add default group
for (int i = 0; i < communities.size(); i++) {
PersonCommunityPojo community = communities.get(i);
// add group to this users list
groups[i + 1] = community.getName();
// make sure group exists
DocumentModel group_doc_model = userManager.getGroupModel(community
.getName());
if (group_doc_model == null) {
log.debug("Group: "
+ community.getName()
+ " did not exist, creating and adding this person as user.");
// create it
group_doc_model = userManager.getBareGroupModel();
group_doc_model.setProperty("group", "grouplabel",
community.getName());
group_doc_model.setProperty("group", "groupname",
community.getName());
group_doc_model.setProperty("group", "description",
community.getName());
String[] members = new String[1];
members[0] = username;
group_doc_model.setProperty("group", "members", members);
userManager.createGroup(group_doc_model);
} else {
// make sure this person is a member and update group
@SuppressWarnings("unchecked")
List<String> member_array = (List<String>) group_doc_model
.getProperty("group", "members");
Set<String> members = new HashSet<String>(member_array);
members.add(username);
group_doc_model.setProperty("group", "members",
members.toArray());
userManager.updateGroup(group_doc_model);
}
}

NuxeoPrincipal principal = userManager.getPrincipal(username);
if (principal != null) {

DocumentModel user_doc_model = userManager.getUserModel(username);
user_doc_model.setProperty("user", "groups", groups);
userManager.updateUser(user_doc_model);
} else {
log.debug("principal was null, create a new user");
DocumentModel user_doc_model = userManager.getBareUserModel();
user_doc_model.setProperty("user", "username", username);
user_doc_model.setProperty("user", "email", username);
user_doc_model.setProperty("user", "password", "fakepassword"
+ new Random().nextInt());
user_doc_model.setProperty("user", "groups", groups);
userManager.createUser(user_doc_model);
}
}

@Override
public void initPlugin(Map<String, String> parameters) {
log.info("init Infinite Authenticator");
if (parameters.containsKey("infiniteAPIURL")
&& parameters.containsKey("infiniteLoginURL")) {
log.info("API_URL_PARAM: " + parameters.get("infiniteAPIURL"));
log.info("API_LOGIN_PARAM: " + parameters.get("infiniteLoginURL"));
infinite_api_url = parameters.get("infiniteAPIURL");
infinite_login_url = parameters.get("infiniteLoginURL");
}
log.debug("end init");
}

@Override
public Boolean needLoginPrompt(HttpServletRequest httpRequest) {
return true;
}

@Override
public Boolean handleLoginPrompt(HttpServletRequest httpRequest,
HttpServletResponse httpResponse, String baseURL) {
Cookie cookie = getCookie(httpRequest, "infinitecookie");
Boolean keepalive = isLoggedIn(httpRequest, cookie);
log.debug("keepalive success: " + keepalive);
if (!keepalive) {
try {
String redirect_url = httpRequest.getRequestURL().toString();
String security_header = httpRequest
.getHeader("X-Forwarded-Proto");
if (security_header != null
&& security_header.toLowerCase().equals("https")) {
redirect_url = redirect_url.replace("http://", "https://");
}
redirect_url = URLEncoder.encode(redirect_url, "UTF-8");
httpResponse.sendRedirect(getLoginUrl(httpRequest,
infinite_login_url) + "?redirect=" + redirect_url);
return true;
} catch (Exception ex) {
log.error("unable to redirect", ex);
}
}
return false;
}

@Override
public List<String> getUnAuthenticatedURLPrefix() {
log.debug("In unauth url prefix: there are no urls we deny access");
return null;
}

private Boolean isLoggedIn(HttpServletRequest httpRequest, Cookie cookie) {
if (cookie != null) {
InfiniteDriver inf_driver = new InfiniteDriver(getApiUrl(
httpRequest, infinite_api_url));
inf_driver.useExistingCookie(cookie.getValue());
return inf_driver.sendKeepalive();
}

return false;
}

private PersonPojo getInfiniteUser(HttpServletRequest httpRequest,
Cookie cookie) {
if (cookie != null) {
InfiniteDriver inf_driver = new InfiniteDriver(getApiUrl(
httpRequest, infinite_api_url));
inf_driver.useExistingCookie(cookie.getValue());
ResponseObject ro = new ResponseObject();
return inf_driver.getPerson(null, ro);
}
return null;
}

@Override
public Boolean handleLogout(HttpServletRequest httpRequest,
HttpServletResponse httpResponse) {
// try to invalidate infinite cookie
Cookie cookie = getCookie(httpRequest, "infinitecookie");
if (cookie != null) {
InfiniteDriver inf_driver = new InfiniteDriver(getApiUrl(
httpRequest, infinite_api_url));
inf_driver.useExistingCookie(cookie.getValue());
inf_driver.logout();
}

try {
// redirect to infinit.e login
httpResponse.sendRedirect(getLoginUrl(httpRequest,
infinite_login_url));
return true;
} catch (Exception ex) {
log.error("unable to redirect", ex);
}
return false;
}

private String getApiUrl(HttpServletRequest httpRequest, String property) {
if (property.equals("AUTOMATIC")) {
try {
return "http://" + httpRequest.getServerName() + "/api/";
} catch (Exception ex) {
log.debug("error converting to url");
}

}

return property;
}

private String getLoginUrl(HttpServletRequest httpRequest, String property) {
if (property.equals("AUTOMATIC")) {
try {
return "http://" + httpRequest.getServerName();
} catch (Exception ex) {
log.debug("error converting to url");
}

}

return property;
}

private static Cookie getCookie(HttpServletRequest httpRequest,
String cookieName) {
log.debug("trying to get cookie: " + cookieName);
Cookie cookies[] = httpRequest.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals(cookieName)) {
return cookie;
}
}
}
return null;
}
}

Step 2: Create a custom Login Plugin that implements LoginPlugin


Next we needed to override the Nuxeo default Login Plugin because the default login plugin TrustingLM will not authenticate our users correctly (I think this is due to our using of random passwords but I am not positive)

The login plugin is a new class that implements LoginPlugin and overrides all the methods. It is very basic and we just return the username anytime it comes to validateUserIdentity

>InfiniteLoginPlugin.java


package com.ikanow.infinit.e.nuxeo.auth;

import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.ecm.platform.api.login.UserIdentificationInfo;
import org.nuxeo.ecm.platform.login.LoginPlugin;

public class InfiniteLoginPlugin implements LoginPlugin {

private static final Log log = LogFactory.getLog(InfiniteLoginPlugin.class);
private String name = "InfiniteLoginPlugin";
private Map & lt;
String, String & gt;
params = null;

@Override
public String validatedUserIdentity(UserIdentificationInfo userIdent) {
return userIdent.getUserName();
}

@Override
public Boolean initLoginModule() {
return true;
}

@Override
public Map & lt;
String, String & gt;
getParameters() {
return params;
}

@Override
public void setParameters(Map & lt; String, String & gt; parameters) {
params = parameters;

}

@Override
public String getParameter(String parameterName) {
return params.get(parameterName);
}

@Override
public String getName() {
return name;
}

@Override
public void setName(String pluginName) {
name = pluginName;
}
}

Step 3: Create a custom configuration file that is external to our extensions so we can adjust it for different Nuxeo instances


We could have put the authenticator properties in the extension contribution outlined in the next step, but we wanted the ability to adjust them for different server configurations so we moved some of the pieces outside (namely the API and Login urls). According to Nuxeo, this file's name must end in -config.xml and be placed in NUXEO_INSTALL/nxserver/config/

The important thing to add here are any parameters you want – you can add and give whatever names/values you wish. They will be available in the Authenticator during the initPlugin function. We also let Nuxeo know the name of our Authenticator (INFINITE_AUTH), and the Login Plugin we wish to use (InfiniteLoginPlugin, the piece we made in Step 2).

>infinite-auth-config.xml


<?xml version="1.0″?>
<component name="com.ikanow.infinit.e.nuxeo.auth.InfiniteAuthenticatorConfig">
<require>org.nuxeo.ecm.platform.ui.web.auth.defaultConfig</require>
<extension target="org.nuxeo.ecm.platform.ui.web.auth.service.PluggableAuthenticationService" point="authenticators">
<authenticationPlugin name="INFINITE_AUTH" enabled="true" class="com.ikanow.infinit.e.nuxeo.auth.InfiniteAuthenticator">
<loginModulePlugin>InfiniteLoginPlugin</loginModulePlugin>
<parameters>
<parameter name="infiniteLoginURL">http://infinite.ikanow.com/</parameter&gt;
<parameter name="infiniteAPIURL">http://infinite.ikanow.com/api/</parameter&gt;
</parameters>
</authenticationPlugin>
</extension>
</component>

Step 4: Adjust the default authentication chain order for both default login and automation


Next we needed to actually tell Nuxeo what extension point we were going to override, and change the order of the Authenticators to use ours first. To do this we needed to edit the config file that should have been automatically created in your plugin project at /src/main/resources/OSGI-INF/extensions/filename.xml

In the file:


  1. We first defined our login plugin and give it a name (the same one you used above in step 3 in the config.xml, e.g. InfiniteLoginPlugin)

  2. Next we overrode the authentication change, we tell Nuxeo to use our new InfiniteAuthenticator first for our platform, then resort to Form Auth if necessary (the only way to get to form Auth is via a direct link to nuxeo/login.jsp because we always redirect invalid logins back to Infinit.e).

  3. Lastly we connected to Nuxeo using the automation client. We needed to override that specfic chain as well, calling InfiniteAuthenticator first, as you can see in the last extension.




NOTE: You may have some code in a .xml file for your Login Plugin, I recommend deleting it all out and putting it in this one configuration file. See example below for how we left the LoginPlugin.xml


>InfiniteAuthenticator.xml


<?xml version="1.0″?>
<component name="com.ikanow.infinit.e.nuxeo.auth.InfiniteAuthenticator">
<require>org.nuxeo.ecm.platform.ui.web.auth.defaultConfig</require>
<!– We need to use the Infinit.e login plugin because trusting lm is doing something weird and failing to authenticate users –>
<extension target="org.nuxeo.ecm.platform.login.LoginPluginRegistry" point="plugin">
<LoginPlugin name="InfiniteLoginPlugin" class="com.ikanow.infinit.e.nuxeo.auth.InfiniteLoginPlugin">
<enabled>true</enabled>
</LoginPlugin>
</extension>
<extension target="org.nuxeo.ecm.platform.ui.web.auth.service.PluggableAuthenticationService" point="chain">
<authenticationChain>
<plugins>
<plugin>INFINITE_AUTH</plugin>
<plugin>FORM_AUTH</plugin>
</plugins>
</authenticationChain>
</extension>
<extension target="org.nuxeo.ecm.platform.ui.web.auth.service.PluggableAuthenticationService" point="specificChains">
<specificAuthenticationChain name="Automation">
<urlPatterns>
<url>(.)/automation.</url>
</urlPatterns>
<replacementChain>
<plugin>INFINITE_AUTH</plugin>
<plugin>AUTOMATION_BASIC_AUTH</plugin>
</replacementChain>
</specificAuthenticationChain>
</extension>
</component>

<?xml version="1.0″?>
<component name="com.ikanow.infinit.e.nuxeo.auth.InfiniteLoginPlugin">
</component>

Step 5: Deploy to Nuxeo


Finally now that we've programmed all this up, we deployed it to Nuxeo to test using the following steps:


  1. Build project by right clicking on project name > Nuxeo > Export Jar

  2. Place this jar in your nuxeo instance at NUXEO_INSTALL/nxserver/plugins/

  3. Place the -config.xml file we created in step 3 at NUXEO_INSTALL/nxserver/config/

  4. Restart Nuxeo


After that point you should be able to login to an external service, then attempt to go to Nuxeo and it will automatically create a user account, groups and let you in.

Conclusion


The Nuxeo platform offers a flexible API and multiple extension points that allowed us to tightly integrate our log in infrastructure and data models with their authentication system. Nuxeo actually offers a few standard solutions to allow SSO such as CAS, Portal Authentication, and Token Authentication. We decided to build our own because we wanted to pass on Infinit.e's user groups permissions without having to manage Nuxeo groups in addition to Infinit.e's as well as be able to perform some automatic actions as users (create documents). Ultimately, capability allows us to build an improved user experience for our users taking advantage of Nuxeo's case management capabilities.