Jen-Ming Chung

Integrating Apache Shiro With CAS Authentication via LDAP

In this post I want to share how to setup SSO with CAS and LDAP authentication, and then demonstrate how to integrate Apache Shiro with CAS in the web application.

An Example of An Individual LDAP Entry

The following entry which represented in LDIF would be used to login the CAS. In this case, the username will be the full email address (case-insensitive) and the password is the value of userPassword attribute.

dn: cn=John Doe,dc=example,dc=com
objectClass: top
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
cn: John Doe
sn: Doe
mail: john@example.com
userPassword:: e21kNX1HaDNKSEpCekpjYVNjZDN3eVVTOGNnPT0=

Maven Overlay Installation

Here we use Maven WAR overlay project to arrange customizations such as authentication handlers and UI design. Thanks Unicon Labs for providing a great starting simple-cas4-overlay-template. Next, we setup the local environment with the following instructions to meet the minimum requirements, including JDK 1.7+, Apache Maven 3+ and Servlet container supporting Servlet 3+ spec (e.g. Apache Tomcat 7+).

sudo add-apt-repository ppa:webupd8team/java
sudo apt-get update
sudo apt-get install oracle-java7-installer
sudo apt-get install tomcat7
sudo apt-get install maven
sudo apt-get install git

Next, we clone the repository via:

git clone https://github.com/UniconLabs/simple-cas4-overlay-template.git

Note that we need to configure files in etc such as log4j.xml and cas.properties to satisfy local CAS installation needs, and then copy the files to the default external file system location /etc/cas. Finally, we run the chown command to change the owner and owning group of files with appropriate privilege.

sudo chown tomcat7:adm -R /etc/cas

Executing mvn clean package and then deploy the target/cas.war to a servlet container.

Non-Secure Connection

You will have Non-secure Connection message indicates you MUST log in over HTTPS. We need to prepare the certificate keystore and edit the Tomcat configuration file to support SSL.

keytool -genkey -alias tomcat -keypass changeit -keyalg RSA
keytool -export -alias tomcat -keypass changeit -file server.crt
sudo keytool -import -file server.crt -keypass changeit -keystore $JAVA_HOME/jre/lib/security/cacerts

We remove the comment tags from around the original example SSL connector and add the keystorePass and keystoreFile configurations.

/var/lib/tomcat7/conf/server.xml
1
2
3
4
5
6
7
8
<!-- Define a SSL HTTP/1.1 Connector on port 8443
       This connector uses the JSSE configuration, when using APR, the
       connector should be using the OpenSSL style configuration
       described in the APR documentation -->
    <Connector port="8443" protocol="HTTP/1.1" SSLEnabled="true"
                maxThreads="150" scheme="https" secure="true"
                keystorePass="changeit" keystoreFile="/home/ubuntu/.keystore"
                clientAuth="false" sslProtocol="TLS" />

Remember to restart tomcat using sudo service tomcat7 restart and visit CAS over HTTPS, we can except that:

Enable SSL/TLS

Now we can login the CAS with the default username casuser and password Mellon which defined in deployerConfigContext.xml.

CAS Log In Successful

LDAP Supporting Anonymous Search

In order to enable the LDAP integration in CAS we include the following dependency in pom.xml,

1
2
3
4
5
<dependency>
    <groupId>org.jasig.cas</groupId>
    <artifactId>cas-server-support-ldap</artifactId>
    <version>${cas.version}</version>
</dependency>

Let LDAP authentication performed with an anonymous search with the following configuration in deployerConfigContext.xml.

/home/ubuntu/cas-server-4.0.0/cas-server-webapp/src/main/webapp/WEB-INF/deployerConfigContext.xml
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
...

<bean id="authenticationManager" class="org.jasig.cas.authentication.PolicyBasedAuthenticationManager">
    <constructor-arg>
        <map>
            <entry key-ref="ldapAuthenticationHandler" value="#{null}"/>
            <entry key-ref="proxyAuthenticationHandler" value-ref="proxyPrincipalResolver" />
        </map>
    </constructor-arg>
    <property name="authenticationPolicy">
        <bean class="org.jasig.cas.authentication.AnyAuthenticationPolicy" />
    </property>
</bean>

...

<bean id="ldapAuthenticationHandler"
      class="org.jasig.cas.authentication.LdapAuthenticationHandler"
      p:principalIdAttribute="mail"
      c:authenticator-ref="authenticator">
    <property name="principalAttributeMap">
        <map>
            <entry key="member" value="member" />
            <entry key="mail" value="mail" />
            <entry key="displayName" value="displayName" />
        </map>
    </property>
</bean>

<bean id="authenticator" class="org.ldaptive.auth.Authenticator"
      c:resolver-ref="dnResolver"
      c:handler-ref="authHandler" />

<bean id="dnResolver" class="org.ldaptive.auth.PooledSearchDnResolver"
      p:baseDn="${ldap.baseDn}"
      p:allowMultipleDns="false"
      p:connectionFactory-ref="searchPooledLdapConnectionFactory"
      p:userFilter="${ldap.searchFilter}" />

<bean id="searchPooledLdapConnectionFactory"
      class="org.ldaptive.pool.PooledConnectionFactory"
      p:connectionPool-ref="searchConnectionPool" />

<bean id="searchConnectionPool" parent="abstractConnectionPool" />

<bean id="abstractConnectionPool" abstract="true"
      class="org.ldaptive.pool.BlockingConnectionPool"
      init-method="initialize"
      p:poolConfig-ref="ldapPoolConfig"
      p:blockWaitTime="${ldap.pool.blockWaitTime}"
      p:validator-ref="searchValidator"
      p:pruneStrategy-ref="pruneStrategy"
      p:connectionFactory-ref="connectionFactory" />

<bean id="ldapPoolConfig" class="org.ldaptive.pool.PoolConfig"
      p:minPoolSize="${ldap.pool.minSize}"
      p:maxPoolSize="${ldap.pool.maxSize}"
      p:validateOnCheckOut="${ldap.pool.validateOnCheckout}"
      p:validatePeriodically="${ldap.pool.validatePeriodically}"
      p:validatePeriod="${ldap.pool.validatePeriod}" />

<bean id="connectionFactory" class="org.ldaptive.DefaultConnectionFactory"
      p:connectionConfig-ref="connectionConfig" />

<bean id="connectionConfig" class="org.ldaptive.ConnectionConfig"
      p:ldapUrl="${ldap.url}"
      p:connectTimeout="${ldap.connectTimeout}"
      p:useStartTLS="${ldap.useStartTLS}" />

<bean id="pruneStrategy" class="org.ldaptive.pool.IdlePruneStrategy"
      p:prunePeriod="${ldap.pool.prunePeriod}"
      p:idleTime="${ldap.pool.idleTime}" />

<bean id="searchValidator" class="org.ldaptive.pool.SearchValidator" />

<bean id="authHandler" class="org.ldaptive.auth.PooledBindAuthenticationHandler"
      p:connectionFactory-ref="bindPooledLdapConnectionFactory" />

<bean id="bindPooledLdapConnectionFactory"
      class="org.ldaptive.pool.PooledConnectionFactory"
      p:connectionPool-ref="bindConnectionPool" />

<bean id="bindConnectionPool" parent="abstractConnectionPool" />

...

LDAP Properties Starter

Then we add the corresponding LDAP properties to the WEB-INF/cas.properties configuration file.

...

##
# CAS Logout Behavior
# WEB-INF/cas-servlet.xml
#
# Specify whether CAS should redirect to the specified service parameter on /logout requests
  cas.logout.followServiceRedirects=true

...

#========================================
# General properties
#========================================
ldap.url=ldap://ldapsrv:389

# LDAP connection timeout in milliseconds
ldap.connectTimeout=3000

# Whether to use StartTLS (probably needed if not SSL connection)
ldap.useStartTLS=false

#========================================
# LDAP connection pool configuration
#========================================
ldap.pool.minSize=3
ldap.pool.maxSize=10
ldap.pool.validateOnCheckout=false
ldap.pool.validatePeriodically=true

# Amount of time in milliseconds to block on pool exhausted condition
# before giving up.
ldap.pool.blockWaitTime=3000

# Frequency of connection validation in seconds
# Only applies if validatePeriodically=true
ldap.pool.validatePeriod=300

# Attempt to prune connections every N seconds
ldap.pool.prunePeriod=300

# Maximum amount of time an idle connection is allowed to be in
# pool before it is liable to be removed/destroyed
ldap.pool.idleTime=600

#========================================
# Authentication
#========================================

# Base DN of users to be authenticated
ldap.baseDn=dc=example,dc=com

# Search filter used for configurations that require searching for DNs
#ldap.authn.searchFilter=(&(uid={user})(accountState=active))
ldap.searchFilter=(mail={user})

...

Once an overlay project has been created, remember to deploy the target/cas.war again and restart the Tomcat.

java-servlet3-shiro-cas-client-demo

Finally, we use an example to illustrate the whole process. I created a new Maven project from servlet3-maven-archetype named java-servlet3-shiro-cas-client-demo in github, the file structure as following:

├── java-servlet3-shiro-cas-client-demo.iml
├── pom.xml
└── src
    ├── main
    │   ├── java
    │   │   └── org
    │   │       └── example
    │   │           ├── HelloServlet.java
    │   │           ├── LogoutServlet.java
    │   │           └── ProtectedServlet.java
    │   ├── resources
    │   │   └── shiro.ini
    │   └── webapp
    │       ├── WEB-INF
    │       │   ├── logback.xml
    │       │   └── web.xml
    │       └── index.jsp
    └── test
        ├── java
        └── resources

First add the Apache Shiro dependencies in pom.xml to enable the CAS support. Then we create a shire.ini indicates the Shiro use CasRealm in authentication. The web.xml set the filter to handle login/logout requests sent directly by the CAS server. The protected resources ProtectedServlet.java can be reached after authentication, and then you can check the subject’s principal and roles.

Credits

Comments