Tuesday, August 23, 2011

Shibboleth and Liferay Part 5: Extending Liferay

This is part 5 of a series of blog posts detailing an approach to using Shibboleth for Web SSO with Liferay.

This is the fun part if you like writing code.

If you've created Ext plugins for Liferay before this will be a piece of cake. If you haven't, check out this post on how to create an Ext plugin project.

So, create your Ext project. I called mine shibauthentication-ext.

Now, within that project, create a folder structure to match Liferay. We're going after the WEB-INF/ext-impl/src/com/liferay/portal/security/auth folder. That'll be in the docroot folder of your project. Now, if you look at the corresponding folder in your Liferay source code you'll notice a series of auto login classes corresponding to the various Web SSO providers Liferay is designed to interact with out of the box. We're going to build one just like them for Shibboleth.

Your new ShibbolethAutoLogin class should be of package com.liferay.portal.security.auth and it should implement AutoLogin.

Here's the code:

public class ShibbolethAutoLogin implements AutoLogin {
private static Log _log = LogFactoryUtil.getLog(ShibbolethAutoLogin.class);
Note the class we're getting the log from. This will be set in the Liferay Admin screen later.
public String[] login(HttpServletRequest req, HttpServletResponse res)
throws AutoLoginException {
String[] credentials = null;
String userEmail = (String) req.getAttribute("[ATTRIBUTE_FROM_IDP]");
_log.info("User Login received:" + userEmail);
The IdP will send the user meta data by a series of header or attribute names. This is the tricky part because depending on how your IdP is configured, it could come in as either Attributes or Headers. Also, this code is assuming you're getting an E-mail address. It could be a username or user ID depending on your IdP. You'll also need to know the name of the attribute itself.

Sometimes people like to iterate through the Enumeration of Attribues or Headers that come in from the HTTP request to see what's coming in. That's fine, but there's a quirk in the servlet running this that will not show the added Attribute from Shibboleth. You need to specify the name in order to access it, which means you have to get your IdP team to tell you what it is or view the Shibboleth Status page to see the list of what's coming in.
if (Validator.isNull(userEmail) || userEmail.length() < 1) {
_log.error("Did not receive user login information from Shibboleth");
_log.error("Data returned from attribute:" + userEmail);
return credentials;
}
try {
credentials = new String[3];
_log.debug("Company:" + PortalUtil.getCompany(req).getCompanyId());
User user = UserLocalServiceUtil.getUserByEmailAddress(PortalUtil
.getCompany(req).getCompanyId(), userEmail);
_log.debug("UserId:" + String.valueOf(user.getUserId()));
credentials[0] = String.valueOf(user.getUserId());
credentials[1] = user.getPassword();
credentials[2] = Boolean.TRUE.toString();
The credentials array is what gets passed to Liferay with the information about the user to be logged in.
_log.info("Logging in user " + userEmail);
return credentials;
} catch (NoSuchUserException e) {
} catch (Exception e) {
_log.error(StackTraceUtil.getStackTrace(e));
throw new AutoLoginException(e);
}
return credentials;
}
}
Again, your situation may be a little different so it's important to understand how this code works so you can adjust it to fit your needs.
Now, there's one more thing that needs to happen before this will work. You have to go into your portal-ext.properties file and tell it to use this module.
#Shibboleth Auto Login Setup
auto.login.hooks=com.liferay.portal.security.auth.ShibbolethAutoLogin, com.liferay.portal.security.auth.RememberMeAutoLogin
auth.pipeline.enable.liferay.check=false
Last, it's nice to get logging from this module so from in Liferay, logged in as an Admin, go to the Control Panel and then go into "Server Administration." Click "Log Levels."

Click "Add Category."

In the box, add com.liferay.portal.security.auth.ShibbolethAutoLogin and click "save."

I'm still working on a hook that would allow the user to select Shibboleth from the Authentication configuration menu just like any other SSO provider. Once it's finished I'll post it here as a final step to make this solution truly integrated.
---
This project incorporates tools whose development was funded in part by the NIH through the NHLBI grant: The Cardiovascular Research Grid (R24HL085343)

11 comments:

  1. Any chance you could share the code and directory structure on github?

    ReplyDelete
  2. Possibly. Before I do anything like that I'm trying to re-work this plugin to include the stuff in Part 5 all in one Extension Plugin. (The way I have it described here and in the next post, it's a separate Extension and Hook.)

    ReplyDelete
  3. Have you seen this error: 15:19:50,679 ERROR [MainServlet:442] com.liferay.portal.NoSuchUserException: No User exists with the primary key 0
    com.liferay.portal.NoSuchUserException: No User exists with the primary key 0
    at com.liferay.portal.service.persistence.UserPersistenceImpl.findByPrimaryKey(UserPersistenceImpl.java:795)
    at com.liferay.portal.service.impl.UserLocalServiceImpl.getUserById(UserLocalServiceImpl.java:1360)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
    at java.lang.reflect.Method.invoke(Method.java:597)
    at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:309)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:183)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:150)
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:110)
    at com.liferay.portal.dao.jdbc.aop.DynamicDataSourceTransactionInterceptor.invoke(DynamicDataSourceTransactionInterceptor.java:44)
    at com.liferay.portal.spring.aop.ChainableMethodAdvice.invoke(ChainableMethodAdvice.java:58)
    at com.liferay.portal.spring.aop.ChainableMethodAdvice.invoke(ChainableMethodAdvice.java:58)
    at com.liferay.portal.spring.aop.ChainableMethodAdvice.invoke(ChainableMethodAdvice.java:58)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
    at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:89)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:202)
    at $Proxy81.getUserById(Unknown Source)


    It appears that my Liferay Instance (6.0.6) doesn't call the custom Shibboleth class. Any thoughts?

    ReplyDelete
  4. It looks like Liferay is trying to use the service layer to lookup a user where the userId being passed is 0. This doesn't necessarily mean your custom class isn't being called, but it may not be correctly passing along the user data. When I was building this, I got that same error before I realized I wasn't getting the correct metadata elements from my Shibboledth IdP. You may want to add some debug statements to your class to output the values the class is getting and trying to work with.

    ReplyDelete
  5. I had debug statements in and it isn't being called before the MainServlet is being called. This seems to be a common problem apparently. I'm unsure what the difference between my implementation is and yours.

    I wanted to say thanks for posting all of this. It's been a big help.

    ReplyDelete
  6. No problem. Feel free to comment or send me an E-mail if I can be of any help. Not sure what's going on in your implementation either but I wonder if we're working from different versions of Liferay or Tomcat...

    ReplyDelete
  7. I've got things working to part 6 however I get nulls for the attributes although I am seeing those same attributes via php script.

    I tried a remove debug of liferay and I just can't seem to see those attributes i'm referring to? Any ideas?

    ReplyDelete
  8. The first thing I'd do is verify that the incoming values are, in fact, attributes and not headers. Also, try this procedure:

    http://arcticfoxontheweb.blogspot.com/2011/09/shibboleth-attributes-what-am-i-getting.html

    To verify your Attributes as Shibboleth sees them.

    ReplyDelete
  9. Very good description, thanks for that!

    I face a problem with my own AutoLogin Hook. This uses a proprietary service for authentication. Basically, I use an auto login hook instead of an Authenticator since after successful authentication I write some user-specific data into the session.

    My solutions is working fine, yet I still have a question:

    If I set

    auth.pipeline.enable.liferay.check=true

    then the user can login both via autologin as well as with his liferay password. This is what I would like to change: users should be only authenticated against the custom authentication I implemented.

    So in order to deactivate the authentication against the Liferay database, I did this

    auth.pipeline.enable.liferay.check=false

    However, now my users can log in with any password. This is very strange.

    Do you have any ideas or suggestions?

    ReplyDelete
  10. Hello and thanks for the kind words.

    Setting auth.pipeline.enable.liferay.check to false simply tells Liferay that you intend to handle password checking yourself. So when you set that value and a user goes to login manually, Liferay doesn't bother to authenticate them, and since you haven't written any code to verify the password, Liferay lets them through.

    If you want to prevent users from logging in manually, the easiest way is to simply modify the theme to remove the Sign In link and be sure all Sign In portlets have been removed. (I was on a team that was using this approach in Liferay 5.2 and SiteMinder for their SSO.)

    ReplyDelete