Improving LifeRay 6 CAS integration

Lately, I had the dubious pleasure of integrating CAS with LifeRay (the results of which can be seen in my previous posts). Unfortunately, LifeRay assumes that both CAS and LifeRay are connected to the same user store (LDAP server or any similar security store), and thus no user import is necessary. But, as CAS has a much wider range of supported user stores – this is not always the case.
I needed to address this issue, meaning – allow users to login through CAS, even if they are not LifeRay users.

Concept

I replaced LifeRay CAS filter, and made sure that the AttributePrincipal object arriving from CAS client is stored at the HTTPSession.
Then, I replaced LifeRay auto-login class, and used LifeRay API to create a user if a user has logged in but did not exist in the internal LifeRay user database.

July-17, 2013 – Since I got many comments on this topic, I decided to open source the code mentioned here. Please see https://github.com/liranzel/liferay-cas-no-ldap/ for details.

The How

Here’s what I did:

  1. Configure LifeRay for CAS (see my previous post – http://tonaconsulting.com/configuring-liferay-and-cas-to-work-with-ldap/, but DON’T configure the LifeRay for LDAP
  2. Create a new Java project.
  3. As I use Maven, I used the following pom.xml file:
    <?xml version="1.0"?>
    <project
    	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
    	xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    	<modelVersion>4.0.0</modelVersion>
    	<groupId>com.tona.liferay</groupId>
    	<artifactId>Authenticator</artifactId>
    	<version>1.0-SNAPSHOT</version>
    	<packaging>jar</packaging>
    	<name>Authenticator</name>
    	<dependencies>
    
    		<dependency>
    			<groupId>org.slf4j</groupId>
    			<artifactId>slf4j-api</artifactId>
    			<version>1.6.6</version>
    		</dependency>
    
    		<dependency>
    			<groupId>javax.portlet</groupId>
    			<artifactId>portlet-api</artifactId>
    			<version>2.0</version>
    		</dependency>
    
    		<dependency>
    			<groupId>junit</groupId>
    			<artifactId>junit</artifactId>
    			<version>3.8.1</version>
    			<scope>test</scope>
    		</dependency>
    
    		<dependency>
    			<groupId>org.jasig.cas.client</groupId>
    			<artifactId>cas-client-core</artifactId>
    			<version>3.2.1</version>
    		</dependency>
    
    		<dependency>
    			<groupId>log4j</groupId>
    			<artifactId>log4j</artifactId>
    			<version>1.2.14</version>
    		</dependency>
    
    		<dependency>
    			<groupId>com.liferay.portal</groupId>
    			<artifactId>portal-client</artifactId>
    			<version>6.0.4</version>
    		</dependency>
    		<dependency>
    			<groupId>com.liferay.portal</groupId>
    			<artifactId>portal-impl</artifactId>
    			<version>6.0.4</version>
    			<scope>provided</scope>
    		</dependency>
    		<dependency>
    			<groupId>com.liferay.portal</groupId>
    			<artifactId>portal-service</artifactId>
    			<version>6.0.4</version>
    			<scope>provided</scope>
    		</dependency>
    		<dependency>
    			<groupId>com.liferay.portal</groupId>
    			<artifactId>util-java</artifactId>
    			<version>6.0.4</version>
    		</dependency>
    
    		<dependency>
    			<groupId>com.liferay.portal</groupId>
    			<artifactId>util-bridges</artifactId>
    			<version>6.0.4</version>
    			<scope>provided</scope>
    		</dependency>
    
    	</dependencies>
    </project>
    
    
  4. I create a new class, called TonaCASFilter, that derives from CASFilter. Note that I had to copy some code from the parent class, as it was not easily extensible 🙁
    public class TonaCasFilter extends CASFilter {
    
    	public static String LOGIN = CASFilter.class.getName() + "LOGIN";
    
    	public static void reload(long companyId) {
    		_ticketValidators.remove(companyId);
    	}
    
    	protected Log getLog() {
    		return _log;
    	}
    
    	protected TicketValidator getTicketValidator(long companyId)
    		throws Exception {
    
    		TicketValidator ticketValidator = _ticketValidators.get(companyId);
    
    		if (ticketValidator != null) {
    			return ticketValidator;
    		}
    
    		String serverName = PrefsPropsUtil.getString(
    			companyId, PropsKeys.CAS_SERVER_NAME, PropsValues.CAS_SERVER_NAME);
    		String serverUrl = PrefsPropsUtil.getString(
    			companyId, PropsKeys.CAS_SERVER_URL, PropsValues.CAS_SERVER_URL);
    		String loginUrl = PrefsPropsUtil.getString(
    			companyId, PropsKeys.CAS_LOGIN_URL, PropsValues.CAS_LOGIN_URL);
    
    		Saml11TicketValidator cas20ProxyTicketValidator = new Saml11TicketValidator(serverUrl);
    		
    		Map<String, String> parameters = new HashMap<String, String>();
    
    		parameters.put("serverName", serverName);
    		parameters.put("casServerUrlPrefix", serverUrl);
    		parameters.put("casServerLoginUrl", loginUrl);
    		parameters.put("redirectAfterValidation", "false");
    
    		cas20ProxyTicketValidator.setCustomParameters(parameters);
    
    		_ticketValidators.put(companyId, cas20ProxyTicketValidator);
    
    		return cas20ProxyTicketValidator;
    	}
    
    	protected void processFilter(
    			HttpServletRequest request, HttpServletResponse response,
    			FilterChain filterChain)
    		throws Exception {
    
    		long companyId = PortalUtil.getCompanyId(request);
    
    		if (PrefsPropsUtil.getBoolean(
    				companyId, PropsKeys.CAS_AUTH_ENABLED,
    				PropsValues.CAS_AUTH_ENABLED)) {
    
    			HttpSession session = request.getSession();
    
    			String pathInfo = request.getPathInfo();
    
    			if (pathInfo.indexOf("/portal/logout") != -1) {
    				session.invalidate();
    
    				String logoutUrl = PrefsPropsUtil.getString(
    					companyId, PropsKeys.CAS_LOGOUT_URL,
    					PropsValues.CAS_LOGOUT_URL);
    
    				response.sendRedirect(logoutUrl);
    
    				return;
    			}
    			else {
    				String login = (String)session.getAttribute(LOGIN);
    
    				String serverName = PrefsPropsUtil.getString(
    					companyId, PropsKeys.CAS_SERVER_NAME,
    					PropsValues.CAS_SERVER_NAME);
    
    				String serviceUrl = PrefsPropsUtil.getString(
    					companyId, PropsKeys.CAS_SERVICE_URL,
    					PropsValues.CAS_SERVICE_URL);
    
    				if (Validator.isNull(serviceUrl)) {
    					serviceUrl = CommonUtils.constructServiceUrl(
    						request, response, serviceUrl, serverName, "ticket",
    						false);
    				}
    
    				String ticket = ParamUtil.getString(request, "ticket");
    
    				if (Validator.isNull(ticket)) {
    					if (Validator.isNotNull(login)) {
    						processFilter(
    								TonaCasFilter.class, request, response, filterChain);
    					}
    					else {
    						String loginUrl = PrefsPropsUtil.getString(
    							companyId, PropsKeys.CAS_LOGIN_URL,
    							PropsValues.CAS_LOGIN_URL);
    
    						loginUrl = HttpUtil.addParameter(
    							loginUrl, "service", serviceUrl);
    
    						response.sendRedirect(loginUrl);
    					}
    
    					return;
    				}
    
    				TicketValidator ticketValidator = getTicketValidator(
    					companyId);
    
    				Assertion assertion = ticketValidator.validate(
    					ticket, serviceUrl);
    
    				if (assertion != null) {
    					AttributePrincipal attributePrincipal =
    						assertion.getPrincipal();
    
    					login = attributePrincipal.getName();
    
    					session.setAttribute(LOGIN, login);
    					session.setAttribute("principal", attributePrincipal);
    				}
    			}
    		}
    
    		processFilter(TonaCasFilter.class, request, response, filterChain);
    	}
    
    	private static Log _log = LogFactoryUtil.getLog(TonaCasFilter.class);
    
    	private static Map<Long, TicketValidator> _ticketValidators =
    		new ConcurrentHashMap<Long, TicketValidator>();
    
    }
    
  5. I then create the new auto-login class. Again – as it was not very extendible, I had to copy-paste allot of code from the parent class…
    public class TonaCASAutoLogin extends CASAutoLogin {
    	private Logger logger = LoggerFactory.getLogger(TonaCASAutoLogin.class.getName());
    
    	@Override
    	public String[] login(HttpServletRequest request, HttpServletResponse response) {
    		String[] credentials = null;
    
    		try {
    			long companyId = PortalUtil.getCompanyId(request);
    
    			if (!PrefsPropsUtil.getBoolean(companyId, PropsKeys.CAS_AUTH_ENABLED, PropsValues.CAS_AUTH_ENABLED)) {
    
    				return credentials;
    			}
    
    			HttpSession session = request.getSession();
    
    			String login = (String) session.getAttribute(CASFilter.LOGIN);
    
    			if (Validator.isNull(login)) {
    				return credentials;
    			}
    
    			AttributePrincipal principal = (AttributePrincipal) session.getAttribute("principal");
    			if (principal != null) {
    
    				Map<String, Object> attrs = principal.getAttributes();
    
    				Configuration.getInstance().load();
    				
    				Object groupMembership = attrs.get(Configuration.getInstance().getMemberOfProperty());
    
    				if (groupMembership != null) {
    					com.liferay.portal.service.ServiceContext context = new com.liferay.portal.service.ServiceContext();
    
    					User user = null;
    					
    					String email = attrs.get("email").toString();
    					String lastName = attrs.get("lastName").toString();
    					String firstName = attrs.get("firstName").toString();
    
    					try {
    						user = UserLocalServiceUtil.getUserByScreenName(companyId, login);
    					} catch (NoSuchUserException nsue) {
    						// User not found.
    					}
    
    					// The groups the user needs to belong to
    					long[] mapToGroupsArray = getUserGroups(companyId, groupMembership.toString());
    					
    					// The community we want to map the user to
    					long groupId = 10131;
    
    
    					// User not found - create it.
    					if (user == null) {
    						try {
    							UserLocalServiceUtil.addUser(0, companyId, false, "not-used", "not-used", false,
    									fixScreenName(login), email, 0, "", Locale.getDefault(), firstName, "", lastName,
    									0, 0, true, 1, 1, 1970, null, new long[] {groupId}, null, null, mapToGroupsArray, false, context);
    
    						} catch (Exception e) {
    							logger.error("Can't add user", e);
    						}
    					} else {
    						// User exists - remap groups
    						UserGroupLocalServiceUtil.setUserUserGroups(user.getUserId(), mapToGroupsArray);
    						
    						// Ensure user has the right community
    						
    						UserLocalServiceUtil.addGroupUsers(groupId, new long[] { user.getUserId()});
    					}
    				} 
    			} 
    
    			return super.login(request, response);
    
    		} catch (Throwable e) {
    			logger.error("Can't auto-login, reverting to default behavior", e);
    		}
    
    		return super.login(request, response);
    	}
    
    	private String fixScreenName(String loginName) {
    		
    		String name = loginName;
    		
    		if (name.contains("@")) {
    			name = name.substring(0,name.indexOf("@"));
    		}
    
    		return name;
    	}
    
    	private long[] getUserGroups(long companyId, String groupMembership) throws Exception {
    		String[] groups = groupMembership.toString().split(";");
    
    		List<Long> mapToGroups = new ArrayList<Long>();
    
    		for (String group : groups) {
    			if (group.contains("[")) {
    				group = group.replace('[', ' ');
    				group = group.replace(']', ' ');
    				group = group.trim();
    			}
    			String groupName = group;
    
    			if (groupName != null) {
    				UserGroup liferayGroup = UserGroupLocalServiceUtil.getUserGroup(companyId, groupName);
    				if (liferayGroup != null) {
    					logger.debug("Found user group " + liferayGroup.getUserGroupId());
    				mapToGroups.add(liferayGroup.getUserGroupId());
    				} else {
    					logger.debug("Liferay group " + groupName + " not found");
    				}
    			}
    		}
    
    		long[] mapToGroupsArray = new long[mapToGroups.size()];
    		int i = 0;
    		for (long l : mapToGroups) {
    			mapToGroupsArray[i] = l;
    			++i;
    		}
    		
    		return mapToGroupsArray;
    	}
    }
    

    Note that you must make sure CAS sends all the relevant properties in the return SAML response, and that the groups sent exist in LifeRay.

  6. Now, create a JAR file (mvn clean install), and copy the JAR file to TOMCAT_HOME/webapps/ROOT/WEB-INF/lib
  7. Edit the LifeRay web.xml file. It can be found in TOMCAT_HOME/webapps/ROOT/WEB-INF. Replace the line
    <filter-class>com.liferay.portal.servlet.filters.sso.cas.CASFilter</filter-class>
    

    with the following line:

    <filter-class>com.tona.security.TonaCasFilter</filter-class>
    
  8. Edit the LifeRay portal-ext.properties file. It can be found in TOMCAT_HOME/webapps/ROOT/WEB-INF/classes. Add the following line:
    auto.login.hooks=com.tona.security.TonaCASAutoLogin
    
  9. Restart LifeRay. All should work…

22 thoughts on “Improving LifeRay 6 CAS integration

  1. Thanks for this good post. It worked and I can get attributePrincipal in Liferay. Although I have a question. You had mentioned in point 5 towards end, “Note that you must make sure CAS sends all the relevant properties in the return SAML response, and that the groups sent exist in LifeRay. “.
    How to make sure this is done ? I have created my custom AuthenticationHandler class and added that into deployerConfigContext.xml. Also I added additional allowed attributes under bean RegisteredServiceImpl in deployerConfigContext.xml. The last bit I modified is casServiceValidationSuccess.jsp to make these additional attributes available in SAML. Is there anything missing as I am not getting the additional attributes. ( Note: I am not using LDAP in this case.)

  2. It looks OK. Print the properties you get in TonaCASAutoLogin line 27, and make sure you got all relevant properties (sorry – not really a CAS expert, more on the LifeRay side 😉 )

    • Thanks for the quick reply. Earlier I did not use TonaAutoLogin and wrote my logon hook and was getting attributePrincipal in session although it did not contain any attributes. I added TonaAutoLogin class and it is getting called but the I am getting attributePrincipal is null. Although in hook i am getting attributePrincipal.
      I believe I am missing some configuration somewhere.
      My question from your post is “How to make sure CAS sends all the relevant properties in the return SAML response, and that the groups sent exist in LifeRay. “. (this is what is under point 5 in post.)

      • Are you sure TonaCasFilter is getting called? Put some System.out calls there, to make sure it’s being called, and that the response is actually received from CAS

        • Yes, it was getting called and i had few sysout. But I, managed to figure out the issue. It was on CAS side where my additional attributes bean was not getting wired properly. Now I can get the additional attributes in my extended CASFilter. But not on TonaAutologin.But I can set these additional attributes in sesssion in the filter and can use later on. I believe that should do the trick. Thanks for your quick help. Appreciated.

          • Hi, I followed the above procedure and I am getting below exception when I am trying to login in into liferay through CAS.

            PWC4011: Unable to set request character encoding to UTF-8 from context , because request parameters have already been read, or ServletRequest.getReader() has already been called
            2013-09-04 07:10:07,308 ERROR [com.liferay.portal.servlet.filters.sso.cas.CASFilter] –
            org.jasig.cas.client.validation.TicketValidationException:
            ticket ‘ST-189-orces0vhXy6kxBL5HJWu-cas01.example.org’ not recognized.

            Please provide guidance to resolve the issue. Thanks for your time

  3. By Using above code I am getting Configuration.getInstance().load() not found. Why am I getting getInstance() not found compilation error. I am stucked here.Kindly help.

    • Configuration is some internal class in our project. You can remove it all together, and just write a constant String for use with the groupMembership property.

  4. I used above code and TonaCasFilter is not getting called and while I am trying to access values from TonaCASAutoLogin class all values in session are null.
    I am setting TonaCasFilter in liferay-web.xml. Please help here.

  5. No I am only replacing CASFilter in iferay-web.xml with TonaCasFilter. In web.xml file I did not find CasFilter entry to replace with TonaCasFilter.Please suggest what entry is required in web.xml file.

    • YOu got it mixed. Watch point #7 and #8 on the blog post closely – it tells which web.xml entry to update, and which portal-ext.properties to update.

  6. I am using Liferay6.1.1 and there is no com.liferay.portal.servlet.filters.sso.cas.CASFilter
    in web.xml under TOMCAT_HOME/webapps/ROOT/WEB-INF. This CASFilter is in liferay-web.xml. are there different setting for diffrent liferay versions for FIlters?

  7. Didn’t work with Liferay 6.1.1. I’ve just downloaded it – and indeed all configuration is in liferay-web.xml. Weird.
    Anyway, try putting debug prints in the TonaCasFilter code, to make sure the filter is enabled.
    I saw some posts online about Liferay 6.1.1 doesn’t work with Cas…

  8. I tried after putting few SysOuts but TonaCasFilter is not getting called at all. Can you please tell me required changes need to bo done for liferay6.1.1. I really need to get out of here.
    I have one more question can you please tell me in Liferay 6.1.1 how to change Liferay DataSource connection code and where I will found that code.Please notice that I am not looking to change DataBase . I need to change the Datasource connection code itself.Our company has there own customized connection stuff.
    I appriciate if you could torch some light on this.

  9. Your article, “Improving LifeRay 6 CAS integration | Tona Consulting” was worth writing a comment here!
    Simply wanted to announce you truly did a great job. Thanks
    ,Lester

  10. Hi,

    I have one question.

    You mentioned that,
    ” You have replaced LifeRay CAS filter, and made sure that the AttributePrincipal object arriving from CAS client is stored at the HTTPSession. ”

    My question is, without replacing the Liferay Cas Filter, is there a way to get the CAS AttributePrincipal in TonaCASAutoLogin ?
    May be from HttpServletRequest ? or any other object ?
    I have tried from HttpServletRequest , but got null .

    In .net, same has been done using HttpContext.Current.User

  11. It seems impossible that no one on the liferay website is able to suggest those modifications as they are nearly essential when integrating a CAS environment. Maybe they’re assuming both cas and liferay pick data from the same user store, that’s a very simplistic assumption.

    Your article is very helpful and I was wondering if everything (filter and autologin) can be done with a ‘Hook only’ solution, without further modifications to liferay system and without adding libs in liferay root folder.

    Thanks.

  12. I tried the code in my Liferay 6.1 ga2. It seems to work but in the end, after the cas form, I’m prompted with the Liferay login (!?).

    The suspicious part of the code is that :
    return super.login(request, response); //called if everything is OK
    } catch (Throwable e) { … }
    return super.login(request, response); //called in any other case

    Returning the first or the last calls to super.login makes no difference to me. So I’m calling CASAutoLogin unwanted behaviour, it does not log me really in to the system. I think that maybe 6.1.1 ga2 casAutoLogin has different code behaviour and at the same time I think it’s enough to avoid the super.login in the case everyhthing is ok (first return call above), adding instead a return like this :

    credentials[0] = String.valueOf(user.getUserId());
    credentials[1] = user.getPassword();
    credentials[2] = Boolean.TRUE.toString();
    return credentials;
    }catch(….

Leave a Reply

Your email address will not be published. Required fields are marked *