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
Comments:

Nice Prashant. I'll add it into our next release so that our users can easily integrate with OpenSSO with fewer steps.

Posted by Brian Chan on October 15, 2007 at 08:12 AM PDT #

Hi Prashant, nice article. Thanks for writing it, and for tagging it 'opensso'. I'm adding you to Planet OpenSSO ( http://planets.sun.com/OpenSSO/group/blogs/ ), so any more opensso tagged articles should be picked up there.

Posted by Pat Patterson on October 18, 2007 at 02:07 AM PDT #

Hello

I am trying to use above example to integrate openSSO and liferay in my environment. However I am receiving an error java.net.UnknownServiceException: no content-type.

I will appreciate , if somebody can help me resolving the above error

With Regards
Rahul

Posted by Rahul Vadera on January 15, 2008 at 09:22 PM PST #

Rahul,

You can use the latest 4.3.x Liferay bits that has this feature already built-in via the Enterprise Admin portlet.

Posted by Prashant Dighe on January 16, 2008 at 09:53 AM PST #

The integration code with Liferay that Brian Chan did for the release does not create the session variable for the subject ID. Is there something missing with the "hooks" for autologin? Filter does not seem to run.

Posted by Jay Mar on February 28, 2008 at 03:07 AM PST #

How can we return user to the page he originated from so that he remains localized to the organization he is associated with using OpenSSO in Liferay.

Posted by sri on June 17, 2009 at 12:51 AM PDT #

I have been able to integrate Liferay with OpenSSO's authentication piece (by enabling the authentication interface that is provided within Liferay's administration portlet) . However I am not sure if there is a reasonable way to integrate Liferay with OpenSSO authorization (roles, groups etc). From the conclusion of the above article I am inclined to infer that there isn't one.

Is it even reasonable to attempt to setup communities, pages for a given user dynamically - by using the groups/roles information in OpenSSO, or does the portal need to be "configured" by hand with a default set of communities (that may mirror groups within OpenSSO) to function properly?

Posted by Pradeep Balachandran on September 14, 2009 at 07:43 AM PDT #

Post a Comment:
  • HTML Syntax: NOT allowed
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