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…