In this part of the series I will cover how to setup CAS to authenticate using LDAP and store the ticket information in PostgreSQL, instead of the default memory ticket manager.
We will also be setting up connection pooling for:
- LDAP
- PostgreSQL (using C3Po)
Prerequisite Tasks
Letting Tomcat Know How to Use PostgreSQL
Tomcat doesn’t know how to work with PostgreSQL right off the bat, so we need to grab the JDBC4 driver file for it “postgresql-9.3-1102.jdbc4.jar”
- Download the file from here
wget -c http://central.maven.org/maven2/org/postgresql/postgresql/9.3-1102-jdbc4/postgresql-9.3-1102-jdbc4.jar
- Move the file to /var/lib/tomcat7/lib
- Restart Tomcat
Setting Up Your PostgreSQL Server to Use SSL
To use SSL with PostgreSQL, you need to generate (or copy) and put in place to files sever.crt and server.key theses can be the star cert for your domain or self-signed. Either way, once you have them you will need to place them in your $PGDATA folder. On Ubuntu, this folder is typically /var/lib/postgresql/<version>/main/
Another option is to place these files in /etc/ssl/private and create a soft link to them in /var/lib/postgresql/<version>/main/ like so:
ln -s /etc/ssl/private/your_cert.key /var/lib/postgresql/<version>/main/server.key
ln -s /etc/ssl/private/your_cert.crt /var/lib/postgresql/<version>/main/server.crt
For more information on this setup, please refer to PostgreSQL’s documentation here.
Adding the SSL Certs For PostgreSQL and LDAPS
We intent to use SSL for both PostgreSQL and LDAP, so we will need to add the appropriate certs to our cacerts file.
- Get the certs for Postgres and your LDAP servers in pem format (here is a post on converting crt and key files to a pem, if you don’t have one)If you will only be using certain servers for LDAP and do not have a cert that covers each of them, you can pull the cert from the server directly, like so:
echo -n | openssl s_client -connect ldap_server_ip:636 | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > ldap_server.pem
- Once you have the pem files to you cacerts file like so:
keystore -import -alias <server alias> -file <your_pem_file> -keystore $JAVA_HOME/jre/lib/security/cacerts
Note: the default password is “changeit”
If your JAVA_HOME is not set, you can find what it should by by issuing the following command to find your cacerts file:
find /usr/lib/jvm -name cacerts
Setting Up The Application User and Database For CAS
- Open your favorite tool for working with Postgresql (phpPgAdmin, pgAdminIII, psql)
- Create the application user by running the following SQL, let’s call the user example_app:
CREATE ROLE example_app LOGIN
WITH PASSWORD 'some_password' NOSUPERUSER INHERIT NOCREATEDB NOCREATEROLE NOREPLICATION;
- Create the database for CAS (we don’t have to create any tables in it, CAS will do this for us)
CREATE DATABASE cas
WITH OWNER example_app
ENCODING = 'UTF-8'
TABLESPACE = pg_default
LC_COLLATE = 'en_US.UTF-8'
LC_CTYPE = 'en_US.URF-8'
CONNECTION LIMIT = -1;
- If you are limiting connections to your database with the pg_hba.conf file, you should add a line to it for this user and database, like so:
- Open /etc/postgresql/<version>/main/pg_hba.conf
- Add the following line in the IPv4 section:
host cas example_app <cas_server_ip>/<host bits> md5
- Save the file
- Reload the configuration for PostgreSQL by running the following command
service postgresql reload
Altering the Maven WAR Overlay
In this part we will be altering the Maven WAR project from part 1.
Adding the LDAP and SQL Connection Pooling Properties
- In your $project_home (/opt/work/cas-local) open src/main/webapp/WEB-INF/cas.properties and add the following to the end (filling in your details)
# ========================================
# == LDAP Connection Pooling Properties ==
# ========================================
ldap.pool.minIdle=3
ldap.pool.maxIdle=5
ldap.pool.maxSize=10
# Max time in ms to wait for a connection to
# become available under pool exhausted condition
ldap.pool.maxWait=10000
# -- Evictor Settings --
# Period in ms at which the evictor process runs
ldap.pool.evictionPeriod=600000
# Max time in ms that connection can remain idle
# before becoming available for eviction
ldap.pool.idleTime=1200000
# -- Connection Testing Settings --
# Set to true to enable connection liveliness testing on evictor process runs.
# Probably results in best performance
ldap.pool.testWhileIdle=true
# Set to true to enable connection liveliness testing before every request
# to borrow an object from the pool
ldap.pool.testOnBorrow=false
# ===========================================
# == Database Connection Pooling Properties ==
# ===========================================
# == Basic database connection pool configuration ==
database.dialect=org.hibernate.dialect.PostgreSQLDialect
database.driverClass=org.postgresql.Driver
database.url=jdbc:postgresql://<postgresql_server_address>/cas?ssl=true
database.user=example_app
database.password=example_app_password
database.pool.minSize=3
database.pool.maxSize=15
# Maximum amount of time to wait in ms for a connection to become
# available when the pool is exhausted
database.pool.maxWait=10000
# Amount of time in seconds after which idle connections
# in excess of minimum size are pruned.
database.pool.maxIdleTime=120
# Number of connections to obtain on pool exhaustion condition.
# The maximum pool size is always respected when acquiring
# new connections.
database.pool.acquireIncrement=3
# == Connection testing settings ==
# Period in s at which a health query will be issued on idle
# connections to determine connection liveliness.
database.pool.idleConnectionTestPeriod=30
# Query executed periodically to test health
database.pool.connectionHealthQuery=select 1
# == Database recovery settings ==
# Number of times to retry acquiring a _new_ connection
# when an error is encountered during acquisition.
database.pool.acquireRetryAttempts=5
# Amount of time in ms to wait between successive acquire retry attempts.
database.pool.acquireRetryDelay=2000
- Save the file
- Replace the following section to tell CAS we will authenticate with LDAP:
<!--
| This is the authentication handler declaration that every CAS deployer will need to change before deploying CAS
| into production. The default SimpleTestUsernamePasswordAuthenticationHandler authenticates UsernamePasswordCredentials
| where the username equals the password. You will need to replace this with an AuthenticationHandler that implements your
| local authentication strategy. You might accomplish this by coding a new such handler and declaring
| edu.someschool.its.cas.MySpecialHandler here, or you might use one of the handlers provided in the adaptors modules.
+-->
<bean
class="org.jasig.cas.authentication.handler.support.SimpleTestUsernamePasswordAuthenticationHandler" />
with
<!--
| Use the LDAP auhentication handler
+-->
<bean
class="org.jasig.cas.adaptors.ldap.BindLdapAuthenticationHandler"
p:filter="cn=%u"
p:searchBase="<your_base_OU. ex. o=some_ou or blank for searching everything>"
p:contextSource-ref="contextSource"
p:searchContextSource-ref="pooledContextSource" />
- Bellow that bean add the following bean to setup the LDAPContextSouce:
<!--
Define contextSource for LDAP
-->
<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="urls">
<list>
<value>ldaps://<your ldap server address></value>
<value>ldaps://<second ldap server address></value>
</list>
</property>
<!-- 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="30000" />
<entry key="com.sun.jndi.ldap.read.timeout" value="30000" />
<!-- Explained at http://docs.oracle.com/javase/jndi/tutorial/ldap/security/auth.html -->
<entry key="java.naming.security.authentication" value="simple" />
</map>
</property>
</bean>
- Add the following beans for setting up the LDAP pool context and the context validator:
<!-- This uses the variables from cas.properties -->
<bean id="pooledContextSource"
class="org.springframework.ldap.pool.factory.PoolingContextSource"
p:minIdle="${ldap.pool.minIdle}"
p:maxIdle="${ldap.pool.maxIdle}"
p:maxActive="${ldap.pool.maxSize}"
p:maxWait="${ldap.pool.maxWait}"
p:timeBetweenEvictionRunsMillis="${ldap.pool.evictionPeriod}"
p:minEvictableIdleTimeMillis="${ldap.pool.idleTime}"
p:testOnBorrow="${ldap.pool.testOnBorrow}"
p:testWhileIdle="${ldap.pool.testWhileIdle}"
p:dirContextValidator-ref="dirContextValidator"
p:contextSource-ref="contextSource" />
<!-- used by PoolingContextSource. Does a search of the root pulling
back a single record to check against the specified context.
This is used to ensure that pooled connections are still properly
connected. -->
<bean id="dirContextValidator"
class="org.springframework.ldap.pool.validation.DefaultDirContextValidator"
p:base=""
p:filter="objectclass=*">
<property name="searchControls">
<bean class="javax.naming.directory.SearchControls"
p:timeLimit="1000"
p:countLimit="1"
p:searchScope="0"
p:returningAttributes="" />
</property>
</bean>
- In the next section change the “@@THIS SHOILD BE REPLACED@@” with the username of the person you want to be able to access the “/service” section of CAS
- In the serviceRegistryDao bean, under the <property name=”evaluationOrder” value=”0″ /> or <property name=”evaluationOrder” value=”10000001″ /> line (depending on the one you are using), add the following:
<property name="allowedAttributes">
<list>
<value>cn</value>
<value>telephoneNumber</value>
<value>groupMembership</value>
<value>sn</value>
<value>givenName</value>
<value>loginExpirationTime</value>
</list>
</property>
- After the last bean in the file, add the following bean to setup the data source for your database
<!-- Connection pooling for database -->
<bean
id="dataSource"
class="com.mchange.v2.c3p0.ComboPooledDataSource"
p:driverClass="${database.driverClass}"
p:jdbcUrl="${database.url}"
p:user="${database.user}"
p:password="${database.password}"
p:initialPoolSize="${database.pool.minSize}"
p:minPoolSize="${database.pool.minSize}"
p:maxPoolSize="${database.pool.maxSize}"
p:maxIdleTimeExcessConnections="${database.pool.maxIdleTime}"
p:checkoutTimeout="${database.pool.maxWait}"
p:acquireIncrement="${database.pool.acquireIncrement}"
p:acquireRetryAttempts="${database.pool.acquireRetryAttempts}"
p:acquireRetryDelay="${database.pool.acquireRetryDelay}"
p:idleConnectionTestPeriod="${database.pool.idleConnectionTestPeriod}"
p:preferredTestQuery="${database.pool.connectionHealthQuery}"
/>
- Save the file
- Create the file “$project_home/src/main/webapp/WEB-INF/spring-configuration/ticketRegistry.xml” and populate it with the following:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-3.0.xsd" >
<description>
Configuration for the Jpa TicketRegistry which stores the tickets in a
database and cleans them out at specified intervals.
</description>
<!-- Ticket Registry -->
<bean id="ticketRegistry" class="org.jasig.cas.ticket.registry.JpaTicketRegistry" />
<!--
Injects EntityManager/Factory instances into beans with
@PersistenceUnit and @PersistenceContext
-->
<bean class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor"/>
<bean id="entityManagerFactory"
class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="jpaVendorAdapter">
<bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
<property name="generateDdl" value="true"/>
<property name="showSql" value="true" />
</bean>
</property>
<property name="jpaProperties">
<props>
<!-- Set the database dialect -->
<prop key="hibernate.dialect">${database.dialect}</prop>
<prop key="hibernate.hbm2ddl.auto">update</prop>
</props>
</property>
</bean>
<bean id="transactionManager"
class="org.springframework.orm.jpa.JpaTransactionManager"
p:entityManagerFactory-ref="entityManagerFactory" />
<tx:annotation-driven transaction-manager="transactionManager" />
<!-- TICKET REGISTRY CLEANER -->
<bean id="ticketRegistryCleaner"
class="org.jasig.cas.ticket.registry.support.DefaultTicketRegistryCleaner"
p:ticketRegistry-ref="ticketRegistry"
p:lock-ref="cleanerLock" />
<!--
Use JpaLockingStrategy for 3.4.11 and later.
This bean is only needed for HA setups where multiple nodes are attempting
cleanup on a shared database, but it doesn't substantially impact performance
and is easy to setup and is therefore recommended for all JpaTicketRegistry deployments.
This component automatically creates the LOCKS table so no further configuration
is required.
-->
<bean id="cleanerLock"
class="org.jasig.cas.ticket.registry.support.JpaLockingStrategy"
p:uniqueId="${host.name}"
p:applicationId="cas-ticket-registry-cleaner" />
<bean id="ticketRegistryCleanerJobDetail"
class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean"
p:targetObject-ref="ticketRegistryCleaner"
p:targetMethod="clean" />
<bean id="periodicTicketRegistryCleanerTrigger"
class="org.springframework.scheduling.quartz.SimpleTriggerBean"
p:jobDetail-ref="ticketRegistryCleanerJobDetail"
p:startDelay="20000"
p:repeatInterval="1800000" />
</beans>
- Save the file
- Open your pom.xml file and make it look like the following (with your own value in the groupId property/tag)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" >
<modelVersion>4.0.0</modelVersion>
<strong><groupId>org.example.cas</groupId></strong>
<artifactId>local-cas</artifactId>
<packaging>war</packaging>
<version>1.0-SNPAPSHOT</version>
<build>
<plugins>
<plugin>
<artifactId>maven-war-plugin</artifactId>
<configuration>
<warName>cas</warName>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.jasig.cas</groupId>
<artifactId>cas-server-webapp</artifactId>
<version>${cas.version}</version>
<type>war</type>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.jasig.cas</groupId>
<artifactId>cas-server-support-ldap</artifactId>
<version>${cas.version}</version>
</dependency>
<!--
The below is for connection pooling.
Leave this commented out if you do not intend to use it -->
<dependency>
<groupId>commons-pool</groupId>
<artifactId>commons-pool</artifactId>
<version>${apache.commons.pool.version}</version>
</dependency>
<!-- database pooling -->
<dependency>
<groupId>c3p0</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.1.2</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>${hibernate.core.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>${hibernate.core.version}</version>
<scope>runtime</scope>
</dependency>
</dependencies>
<properties>
<cas.version>3.5.2</cas.version>
<apache.commons.pool.version>1.6</apache.commons.pool.version>
<hibernate.validator.version>4.2.0.Final</hibernate.validator.version>
<hibernate.core.version>4.1.0.Final</hibernate.core.version>
</properties>
<repositories>
<repository>
<id>ja-sig</id>
<url>http://oss.sonatype.org/content/repositories/releases/ </url>
</repository>
<repository>
<id>mvn-repo</id>
<url>http://http://mvnrepository.com/artifact/ </url>
</repository>
<repository>
<id>jboss</id>
<url>http://repository.jboss.org/nexus/content/groups/public-jboss/</url>
</repository>
</repositories>
</project>
- Save the file
- Got to $project_home
- (optional) In a different window watch your catalina.out log for problems (tail -f /var/log/tomcat7/catalina.out)
- Build the cas.war file
mvn clean package
- Wait for Tomcat to recognize the new war file, then stop and restart tomcat
service tomcat stop
service tomcat start
- At this point, if your SSL, LDAP, and PostgreSQL are setup correctly, you shouldn’t see any errors when Tomcat starts (you will see errors before you stop Tomcat).
Navigate to your CAS server in a web browser and try to authenticate with a valid account. If everything is working, you should be sent to the logged in screen and see the validation successfully occur in your Catalina.out log.
Stoping PostgreSQL’s Database From Getting Bloated
The database created by CAS in Postgres has several “oid” fields. This really shouldn’t be a problem, except that the JDBC and ODBC drivers for PostgreSQL don’t handle unlinking them correctly. So when your tickets get cleaned up, the referenced object get orphaned, bloating the largeobject, shdepend, and largeobject_meta_data tables in the pg_catatlog.
To make this not happen, we need to inform the large object manager that these objects need to be cleaned.
Run the following SQL statements to create the needed triggers:
CREATE EXTENSION lo;
CREATE TRIGGER t_st_expiration_policy BEFORE UPDATE OR DELETE ON public.serviceticket
FOR EACH ROW EXECUTE PROCEDURE lo_manage(expiration_policy);
CREATE TRIGGER t_st_service BEFORE UPDATE OR DELETE ON public.serviceticket
FOR EACH ROW EXECUTE PROCEDURE lo_manage(service);
CREATE TRIGGER t_tgt_expiration_policy BEFORE UPDATE OR DELETE ON public.ticketgrantingticket
FOR EACH ROW EXECUTE PROCEDURE lo_manage(expiration_policy);
CREATE TRIGGER t_tgt_authentication BEFORE UPDATE OR DELETE ON public.ticketgrantingticket
FOR EACH ROW EXECUTE PROCEDURE lo_manage(authentication);
CREATE TRIGGER t_tgt_services_g_a_t BEFORE UPDATE OR DELETE ON public.ticketgrantingticket
FOR EACH ROW EXECUTE PROCEDURE lo_manage(services_granted_access_to);
I have noticed that in the log, there are periodic exceptions from C3P0 where an unlinked OID is referenced again by the ticket cleaner; however, we have not experienced any problems from our users in relation to this and the database we have hovers around 35-40MB in size, unlike the 6.8GB it grew to with out these triggers.