Configuring LifeRay and CAS to work with LDAP

I saw many tutorials on CAS, Liferay and LDAP – but unfortunetly, none of them worked for me. So I decided to document what does work (at least for me).
Note that my environment is based on LifeRay 6.0.5 and CAS 3.5.1.

  1. Configure Tomcat for SSL. I have used port 443. You can read all about it here
    1. After creating the certificates, I just ended up with adding the following tag in TOMCAT_HOME/conf/server.xml
    2. <Connector
                 port="443" maxThreads="200"
                 scheme="https" secure="true" SSLEnabled="true"
                 keystoreFile="/root/.keystore" keystorePass="password"
                 clientAuth="false" sslProtocol="TLS"/>  
      
    3. IMPORTANT I did not manage to make CAS work with a self signed certificate, so I’ve used a temporary free one.
  2. Configure LifeRay for LDAP
    1. Login to LifeRay
    2. Go to the Control Panel–>Settings–>Authentication–>LDAP
    3. Ensure the “Enabled” check box is selected
    4. I strongly suggest enabling the “Import” checkbox and ensure Import is enabled for server startup.
    5. Add a server
    6. Fill in the LDAP server details (it’s easy to check them with an LDAP browser like jxplorer)
    7. Save your configuration
    8. I usually restart Tomcat after that change, and view the log to see all users were successfully imported
  3. Build CAS
    1. Download CAS (I downloaded it from here)
    2. Unzip the file
    3. Edit the CAS_HOME/cas-server-webapp/pom.xml file and add the following:
    4. <dependency>
           <groupId>org.jasig.cas</groupId>
           <artifactId>cas-server-support-ldap</artifactId>
           <version>3.5.1</version>
      </dependency>
      
    5. Build CAS using maven. The command to run is mvn clean install
  4. Deploy CAS
    1. Copy the newly created WAR file from CAS_HOME/cas-server-webapp/target/cas.war to TOMCAT_HOME/webapps
  5. Configure CAS for LDAP
    1. Edit the TOMCAT_HOME/webapps/cas/WEB-INF/deployerConfigContext.xml
    2. Add the following at the end of the file (just before the /beans tag)
    3. <bean id="contextSource" class="org.springframework.ldap.core.support.LdapContextSource">
        <!-- DO NOT enable JNDI pooling for context sources that perform LDAP bind operations. -->
        <property name="pooled" value="false"/>
      
        <!--
          Although multiple URLs may defined, it's strongly recommended to avoid this configuration
          since the implementation attempts hosts in sequence and requires a connection timeout
          prior to attempting the next host, which incurs unacceptable latency on node failure.
          A proper HA setup for LDAP directories should use a single virtual host that maps to multiple
          real hosts using a hardware load balancer.
        -->
        <property name="url" value="ldap://LDAP_SERVER:389" />
      
        <!--
          Manager credentials are only required if your directory does not support anonymous searches.
          Never provide these credentials for FastBindLdapAuthenticationHandler since the user's
          credentials are used for the bind operation.
        -->
        <property name="userDn" value="cn=Manager"/>
        <property name="password" value="test"/>
      
        <!-- Place JNDI environment properties here. -->
        <property name="baseEnvironmentProperties">
          <map>
            <!-- Three seconds is an eternity to users. -->
            <entry key="com.sun.jndi.ldap.connect.timeout" value="3000" />
            <entry key="com.sun.jndi.ldap.read.timeout" value="3000" />
      
            <!-- Explained at http://download.oracle.com/javase/1.3/docs/api/javax/naming/Context.html#SECURITY_AUTHENTICATION -->
            <entry key="java.naming.security.authentication" value="simple" />
          </map>
        </property>
      </bean>
      
    4. Add the following under the list tag of the authenticationHandlers tag
    5.         <bean class="org.jasig.cas.adaptors.ldap.BindLdapAuthenticationHandler"
        p:filter="mail=%u"
        p:searchBase="ou=people,dc=test,dc=com"
        p:contextSource-ref="contextSource" />
                              </list>
                      </property>
              </bean>
      
      
  6. Configure LifeRay for CAS
    1. Login to LifeRay
    2. Go to the Control Panel–>Settings–>Authentication–>CAS
    3. Ensure the “Enabled” check box is selected
    4. Ensure the “LDAP Import” check box is selected
    5. Enter the URLs of the CAS server
    6. Save
    7. Add the following line to TOMCAT_HOME/webapps/ROOT/WEB-INF/classes/system-ext.properties
    8. com.liferay.filters.sso.cas.CASFilter=true
      
    9. Add the following line to TOMCAT_HOME/webapps/ROOT/WEB-INF/classes/portal-ext.properties
    10. auto.login.hooks=com.liferay.portal.security.auth.CASAutoLogin
      
    11. Restart Tomcat

You can now access your LifeRay instance, and get the CAS login instead…

RuntimeException in Action for tag [rollingPolicy] java.lang.IndexOutOfBoundsException: No group 1

A customer of mine got sometimes the following exception in his catalina.out log.

ERROR in ch.qos.logback.core.joran.spi.Interpreter@23:25 - RuntimeException in Action for tag [rollingPolicy] java.lang.IndexOutOfBoundsException: No group 1
at java.lang.IndexOutOfBoundsException: No group 1
at at java.util.regex.Matcher.group(Matcher.java:470)
at at ch.qos.logback.core.rolling.helper.FileFilterUtil.extractCounter(FileFilterUtil.java:109)
at at ch.qos.logback.core.rolling.helper.FileFilterUtil.findHighestCounter(FileFilterUtil.java:93)
at at ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP.computeCurrentPeriodsHighestCounterValue(SizeAndTimeBasedFNATP.java:65)
at at ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP.start(SizeAndTimeBasedFNATP.java:49)
at at ch.qos.logback.core.rolling.TimeBasedRollingPolicy.start(TimeBasedRollingPolicy.java:87)
at at ch.qos.logback.core.joran.action.NestedComplexPropertyIA.end(NestedComplexPropertyIA.java:167)
at at ch.qos.logback.core.joran.spi.Interpreter.callEndAction(Interpreter.java:318)
at at ch.qos.logback.core.joran.spi.Interpreter.endElement(Interpreter.java:197)
at at ch.qos.logback.core.joran.spi.Interpreter.endElement(Interpreter.java:183)
at at ch.qos.logback.core.joran.spi.EventPlayer.play(EventPlayer.java:62)
at at ch.qos.logback.core.joran.GenericConfigurator.doConfigure(GenericConfigurator.java:147)
at at ch.qos.logback.core.joran.GenericConfigurator.doConfigure(GenericConfigurator.java:133)
at at ch.qos.logback.core.joran.GenericConfigurator.doConfigure(GenericConfigurator.java:96)
at at ch.qos.logback.core.joran.GenericConfigurator.doConfigure(GenericConfigurator.java:55)
at at ch.qos.logback.classic.util.ContextInitializer.configureByResource(ContextInitializer.java:75)
at at ch.qos.logback.classic.util.ContextInitializer.autoConfig(ContextInitializer.java:148)
at at org.slf4j.impl.StaticLoggerBinder.init(StaticLoggerBinder.java:84)
at at org.slf4j.impl.StaticLoggerBinder.<clinit>(StaticLoggerBinder.java:54)
at at org.slf4j.LoggerFactory.bind(LoggerFactory.java:121)
at at org.slf4j.LoggerFactory.performInitialization(LoggerFactory.java:111)
at at org.slf4j.LoggerFactory.getILoggerFactory(LoggerFactory.java:268)
at at org.slf4j.LoggerFactory.getLogger(LoggerFactory.java:241)
at at org.apache.commons.logging.impl.SLF4JLogFactory.getInstance(SLF4JLogFactory.java:156)
at at org.apache.commons.logging.impl.SLF4JLogFactory.getInstance(SLF4JLogFactory.java:132)
at at org.apache.commons.logging.LogFactory.getLog(LogFactory.java:645)
at at org.springframework.web.context.ContextLoader.initWebApplicationContext(ContextLoader.java:184)
at at org.springframework.web.context.ContextLoaderListener.contextInitialized(ContextLoaderListener.java:47)
at at org.apache.catalina.core.StandardContext.listenerStart(StandardContext.java:3972)
at at org.apache.catalina.core.StandardContext.start(StandardContext.java:4467)
at at org.apache.catalina.core.ContainerBase.addChildInternal(ContainerBase.java:791)
at at org.apache.catalina.core.ContainerBase.addChild(ContainerBase.java:771)
at at org.apache.catalina.core.StandardHost.addChild(StandardHost.java:546)
at at org.apache.catalina.startup.HostConfig.deployDirectory(HostConfig.java:1041)
at at org.apache.catalina.startup.HostConfig.deployDirectories(HostConfig.java:964)
at at org.apache.catalina.startup.HostConfig.deployApps(HostConfig.java:502)
at at org.apache.catalina.startup.HostConfig.start(HostConfig.java:1277)
at at org.apache.catalina.startup.HostConfig.lifecycleEvent(HostConfig.java:321)
at at org.apache.catalina.util.LifecycleSupport.fireLifecycleEvent(LifecycleSupport.java:119)
at at org.apache.catalina.core.ContainerBase.start(ContainerBase.java:1053)
at at org.apache.catalina.core.StandardHost.start(StandardHost.java:785)
at at org.apache.catalina.core.ContainerBase.start(ContainerBase.java:1045)
at at org.apache.catalina.core.StandardEngine.start(StandardEngine.java:443)
at at org.apache.catalina.core.StandardService.start(StandardService.java:519)
at at org.apache.catalina.core.StandardServer.start(StandardServer.java:710)
at at org.apache.catalina.startup.Catalina.start(Catalina.java:581)

Turns out the problem was with his logback.xml configuration file –

    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${env.PANORAMA_HOME}/var/log/panorama-sl.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- daily rollover -->
            <fileNamePattern>${env.PANORAMA_HOME}/var/log/panorama-sl.%d{yyyy-MM-dd}.log.gz</fileNamePattern>
                                            
            <timeBasedFileNamingAndTriggeringPolicy   class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                    <!-- or whenever the file size reaches 100MB -->
                <maxFileSize>100MB</maxFileSize>
           </timeBasedFileNamingAndTriggeringPolicy>
        <!-- keep 10 files worth of history -->
            <maxHistory>10</maxHistory>
        </rollingPolicy>
                  
        <Append>true</Append>
        <encoder>
            <!-- default ISO date format enables lexical sorting of dates -->
            <pattern>%-30.-30(%date %level) [%-50.-50thread] %logger{25} %msg%n</pattern>
        </encoder>
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>ALL</level>
        </filter>
    </appender>

Since sometimes the log file size was bigger than 100MB, QOS needed to roll the log file – but couldn’t, since the log file format didn’t include a counter. The correct configuration is:

    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${env.PANORAMA_HOME}/var/log/panorama-sl.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- daily rollover -->
            <fileNamePattern>${env.PANORAMA_HOME}/var/log/panorama-sl.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
                                            
            <timeBasedFileNamingAndTriggeringPolicy   class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                    <!-- or whenever the file size reaches 100MB -->
                <maxFileSize>100MB</maxFileSize>
           </timeBasedFileNamingAndTriggeringPolicy>
        <!-- keep 10 files worth of history -->
            <maxHistory>10</maxHistory>
        </rollingPolicy>
                  
        <Append>true</Append>
        <encoder>
            <!-- default ISO date format enables lexical sorting of dates -->
            <pattern>%-30.-30(%date %level) [%-50.-50thread] %logger{25} %msg%n</pattern>
        </encoder>
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>ALL</level>
        </filter>
    </appender>