Thursday Oct 11, 2007

OpenSSO and Liferay Integration Prototype

Introduction
I would prefer to write a short blog and document this somewhere rather than put all of this in a blog, but was not sure where to put it. So this has become a blogument ;)
OpenSSO is an open source project for Single Sign-On. Liferay is an open source portal from Liferay, Inc.
Liferay portal already integrates with CAS single sign-on server. This blogument describes how Liferay portal can be integrated with OpenSSO for single sign-on.

OpenSSO server
  • Download the OpenSSO server
  • For this prototype, FAM 8.0 Build 1 Zip was used. (FAM stands for Federated Access Manager)
  • Turn off the security manager. On Glassfish v2, it is off by default. On AS9.1, access the admin console and turn it off.
  • If security manager needs to be on, then server.policy must be edited as described here. You may need a few more permissions than listed here.
  • Unzip the file and deploy the deployable-war/fam.war as /opensso
  • Access the server (http://opensso-host:port/opensso) to invoke the configurator. Once configured, it will take you to login page where you can login as "amadmin" user.
OpenSSO client
There are 2 ways for an application to leverage OpenSSO as a client
  1. Using client sdk from the downloaded zip (libraries/jars/famclientsdk.jar)
  2. Using web services or REST based services.
There are advantages/disadvantages in both. Client sdk comes with its own cache and a comprehensive set of Java APIs. So you can register SSO token event listners etc. Also you need to configure AMConfig.properties into your classpath.
If you use REST based identity services, then application is responsible for maintaining its own sessions and data cache. But using REST, the client does not have build and runtime dependency on OpenSSO jars. Thanks to Aravindan for providing the info on this latest and greatest feature.

The REST based services were used for this prototype. Currently, only authenticate/authorize/attributes/log are the REST operations available. So there is no way to validate a client session with server or to get a subjectid for an authenticated user. An issue 1079 has been opened with OpenSSO for this enhancement.
For the time being, the subjectid is extracted from the sso cookie and the following REST operation is used,
http://host:port/opensso/identity/attributes?subjectid=ssoTokenId

This returns user details in the following form:
userdetails.token.id=Et2RTHUb+C9TTNipgRqR0MECgg=@AAJTSQACMDE=# userdetails.attribute.name=sn userdetails.attribute.value=user1 userdetails.attribute.name=cn userdetails.attribute.value=user1 userdetails.attribute.name=objectclass userdetails.attribute.value=person userdetails.attribute.value=inetorgperson userdetails.attribute.value=top userdetails.attribute.value=organizationalperson userdetails.attribute.value=inetuser userdetails.attribute.name=employeenumber userdetails.attribute.value=5 userdetails.attribute.name=uid userdetails.attribute.value=user1 userdetails.attribute.name=userpassword userdetails.attribute.value=s9qne0wEqVUbh4HQMZH+CY8yXmc= userdetails.attribute.name=givenname userdetails.attribute.value=user1 userdetails.attribute.name=mail userdetails.attribute.value=user1@fam.com userdetails.attribute.name=inetuserstatus userdetails.attribute.value=Active

It is possible to get this information in the form of xml by using this url:
http://host:port/opensso/identity/xml/attributes?subjectid=ssoTokenId

Authentication filter
First, write an auth filter which redirects a non-authenticated user to the OpenSSO server's login page. An authenticated user is the one who has the sso cookie.
The code is shown below:
package com.liferay.portal.servlet.filters.sso.fam; import java.io.IOException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import javax.servlet.http.Cookie; import javax.servlet.Filter; import javax.servlet.FilterConfig; import javax.servlet.FilterChain; public class FAMFilter implements Filter { String loginUrl = null; String logoutUrl = null; String idServicesUrl = null; String ssoCookieName = null; public void init(FilterConfig filterConfig) throws ServletException { loginUrl = filterConfig.getInitParameter("loginUrl"); logoutUrl = filterConfig.getInitParameter("logoutUrl"); ssoCookieName = filterConfig.getInitParameter("ssoCookieName"); } public void destroy() {} public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { // If any of the filter params are null or empty // then skip doing anything because it is misconfig if(loginUrl == null || loginUrl.length() == 0 || logoutUrl == null || logoutUrl.length() == 0 || ssoCookieName == null || ssoCookieName.length() == 0) { chain.doFilter(request, response); } HttpServletRequest httpReq = (HttpServletRequest)request; HttpServletResponse httpRes = (HttpServletResponse)response; String pathInfo = httpReq.getPathInfo(); if (pathInfo != null && pathInfo.indexOf("/portal/logout") != -1) { HttpSession httpSes = httpReq.getSession(); httpSes.invalidate(); httpRes.sendRedirect(logoutUrl); } else { if(isAuthenticated(httpReq)) { chain.doFilter(request, response); } else { httpRes.sendRedirect(loginUrl); } } } private boolean isAuthenticated(HttpServletRequest request) { boolean authenticated = false; Cookie cookie = null; Cookie[] cookies = request.getCookies(); int nCookies = cookies == null ? 0 : cookies.length; for(int i = 0; i < nCookies; i++) { if(ssoCookieName.equalsIgnoreCase(cookies[i].getName())) { cookie = cookies[i]; break; } } if(cookie != null) { authenticated = true; request.getSession().setAttribute("subjectid", cookie.getValue()); } return authenticated; } }

Next add the filter to web.xml:

<filter>
<filter-name>FAM Filter</filter-name>
<filter-class>com.liferay.portal.servlet.filters.sso.fam.FAMFilter</filter-class>
<init-param>
<param-name>logoutUrl</param-name>
<param-value>http://opensso-host:port/opensso/UI/Logout?goto=http://liferay-host:port/</param-value>
</init-param>
<init-param>
<param-name>loginUrl</param-name>
<param-value>http://opensso-host:port/opensso/UI/Login?goto=http://liferay-host:port/</param-value>
</init-param>
<init-param>
<param-name>ssoCookieName</param-name>
<param-value>iPlanetDirectoryPro</param-value>
</init-param>
</filter>


<filter-mapping>
<filter-name>FAM Filter</filter-name>
<url-pattern>/\*</url-pattern>
</filter-mapping>

Autologin
Now write a FAMAutoLogin class which implements the AutoLogin interface provided by Liferay. This class implements the "login" method of the interface.
In the implementation, it gets the subjectid from the session (which has been stored off by the auth filter).
Then makes a REST call to get the user attribtues.
Liferay needs firstName, lastName, screenName, email for creating a user profile dynamically in its database, if one does not exist. If the authenticated user (from OpenSSO) is not found, then UserLocalServiceUtil is used to add the user to Liferay database.

The source is shown below:
package com.liferay.portal.security.auth; import java.util.Map; import java.util.HashMap; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.liferay.portal.PortalException; import com.liferay.portal.NoSuchUserException; import com.liferay.portal.model.User; import com.liferay.portal.SystemException; import com.liferay.portal.service.UserLocalServiceUtil; import com.liferay.portal.util.PortalUtil; import com.liferay.portal.util.PrefsPropsUtil; import com.liferay.util.PwdGenerator; import com.liferay.util.ldap.LDAPUtil; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import java.net.URLEncoder; import java.net.URLConnection; import java.io.BufferedReader; import java.net.URL; import java.io.InputStreamReader; import java.io.InputStream; public class FAMAutoLogin implements AutoLogin { public FAMAutoLogin() {} public String[] login(HttpServletRequest req, HttpServletResponse res) throws AutoLoginException { String[] credentials = null; long companyId = PortalUtil.getCompanyId(req); String idServicesUrl = null; try { idServicesUrl = PrefsPropsUtil.getString(companyId, "fam.idservices.url"); } catch(PortalException pe) { throw new AutoLoginException(pe); } catch(SystemException se) { throw new AutoLoginException(se); } String subjectid = (String)req.getSession().getAttribute("subjectid"); if(subjectid == null) { //this should not happen since filter will have already blocked return credentials; } String errorMsg = null; Map nameValues = new HashMap(); String line = null; try { subjectid = URLEncoder.encode(subjectid, "UTF-8"); String url = idServicesUrl + "/attributes?subjectid=" + subjectid; URL iurl = new URL(url); URLConnection connection = iurl.openConnection(); BufferedReader reader = new BufferedReader( new InputStreamReader((InputStream)connection.getContent())); while((line = reader.readLine()) != null) { //each line is returned as x=y String[] parts = line.split("="); //should not happen but we never know if(parts == null || parts.length != 2) { //skip this line continue; } String attrName = null; String attrValue = null; if(parts[0].endsWith("name")) { attrName = parts[1]; //next line must be value line = reader.readLine(); if(line == null) { //something wrong - name must be followed by value throw new AutoLoginException( "Error reading user attributes"); } //each line is returned as x=y parts = line.split("="); //should not happen but we never know if(parts == null || parts.length != 2 || !parts[0].endsWith("value")) { attrValue = null; } else { attrValue = parts[1]; } nameValues.put(attrName, attrValue); } } } catch(java.io.UnsupportedEncodingException use) { throw new AutoLoginException(use); } catch(java.net.MalformedURLException me) { throw new AutoLoginException(me); } catch(java.io.IOException ioe) { throw new AutoLoginException(ioe); } //Liferay user must have these attrs String firstName = (String)nameValues.get("cn"); String lastName = (String)nameValues.get("sn"); String screenName = (String)nameValues.get("givenname"); String email = (String)nameValues.get("mail"); if(email == null || email.length() == 0) { throw new AutoLoginException("No email set for user"); } User user = null; try { user = UserLocalServiceUtil.getUserByEmailAddress(companyId, email); } catch(NoSuchUserException nsue) { try { user = addUser(companyId, screenName, firstName, lastName, email); } catch(Exception e) { throw new AutoLoginException(e); } } catch(Exception e) { throw new AutoLoginException(e); } credentials = new String[3]; credentials[0] = String.valueOf(user.getUserId()); credentials[1] = user.getPassword(); credentials[2] = Boolean.TRUE.toString(); return credentials; } protected User addUser(long companyId, String screenName, String firstName, String lastName, String email) throws PortalException, SystemException { long creatorUserId = UserLocalServiceUtil.getDefaultUserId(companyId); User user = null; try { user = UserLocalServiceUtil.addUser(creatorUserId, companyId, true, "", "", false, screenName, email, java.util.Locale.ENGLISH, firstName, "", lastName, 0, 0, true, 1, 1, 1970, "", 0, 0, false); } catch (Exception e){ _log.error( "Problem adding user with screen name " + screenName + " and email address " + email, e); } return user; } private static Log _log = LogFactory.getLog(FAMAutoLogin.class); }

Liferay Hooks
Liferay portal provides hooks to plugin auto login classes.
Edit (or create if one does not exist in the deployment) portal-ext.properties and add the following:
auto.login.hooks=com.liferay.portal.security.auth.FAMAutoLogin,com.liferay.portal.security.auth.CASAutoLogin,com.liferay.portal.security.auth.NtlmAutoLogin,com.liferay.portal.security.auth.OpenIdAutoLogin,com.liferay.portal.security.auth.RememberMeAutoLogin fam.idservices.url=http://opessso-host:port/opensso/identity

Testing
  1. Access http://opensso-host:port/opensso/
  2. Login as amadmin
  3. Click on the root realm and goto Subjects tab
  4. Add a new user
  5. Now click on the new user's name to edit user profile
  6. Enter an email for the new user (this is important since Liferay needs email)
  7. Logout from OpenSSO and try login as the new user to verify
  8. Access Liferay portal to be redirected to OpenSSO login page
  9. Login as the new user to be redirected back to Liferay portal
  10. Accept the terms and conditions (first time only) to see the Liferay portal pages
Conclusion
The prototype demonstrates how Liferay can be integrated to leverage OpenSSO. For production, use of client sdk may be considered. Liferay can also integrate with LDAP and import membership information. OpenSSO can integrate with various user repositories, so similar implementation can be provided to import membership from OpenSSO user repositories. Even more desirable scenario would be if Liferay can fetch membership at runtime instead of importing it to a local datastore and then struggling to keep it in sync with corporate user repository. The auth filter can be further enhanced to allow anonymous/guest access and then writing an OpenSSO login portlet.

References
  1. Setting_up the Extension Environment
  2. Integrating Liferay With CAS
  3. Developing a Custom Authentication System
  4. Liferay LDAP integration
  5. OpenSSO project page
About

Prashant Dighe

Search

Categories
Archives
« April 2014
SunMonTueWedThuFriSat
  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
   
       
Today