JpaTicketRegistry

New CAS documentation site

CAS documentation has moved over to apereo.github.io/cas, starting with CAS version 4.x. The wiki will no longer be maintained. For the most recent version of the documentation, please refer to the aforementioned link.

If you have completed implementing the JpaTicketRegistry in CAS and are having problems with the JpaTicketRegistry in production please read the section on Avoiding Deadlocks. This could likely be the source of your problem.

Overview

The JpaTicketRegistry allows CAS to store client authenticated state data (tickets) in a database back-end such as MySQL. This is useful for creating highly available (HA) CAS clusters consisting of multiple application nodes such as Tomcat.

This guide will help you configure the JpaTicketRegistry in CAS. While the documentation provided primarily focuses on connecting the JpaTicketRegistry to MySQL, with some modification this information should provide a good start for connecting to other database back-ends as well.

Spring Configuration (ticketRegistry.xml)

This is where the bulk of your configuration for the JpaTicketRegistry goes.

If you're using the Maven2 WAR Overlay process then you will need to create this file in the src/main/webapp/WEB-INF/spring-configuration folder of your project directory.

If you're not using the Maven2 WAR Overlay process then you can edit this file in the cas-server-webapp/src/main/webapp/WEB-INF/spring-configuration folder of the CAS source tree.

The ticketRegistry.xml example below will only work for CAS 3.4 as it uses the Spring 3.0 schema locations. This example must be adapted to use the Spring 2.0 namespace locations for previous versions of CAS.

src/main/webapp/WEB-INF/spring-configuration/ticketRegistry.xml
<?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"/>

  <!-- Use the following for versions prior to 3.4.11 -->
  <!--
  <bean id="ticketRegistry"
    class="org.jasig.cas.ticket.registry.JpaTicketRegistry">
    <constructor-arg index="0" ref="entityManagerFactory" />
  </bean>
  -->

  <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>
        <!-- Use MySQLDialect at your own risk.  See MySQL section below for details. -->
        <prop key="hibernate.dialect">org.hibernate.dialect.MySQLInnoDBDialect</prop>
		<!-- For MySQL 5.x, use the following instead
		<prop key="hibernate.dialect">org.hibernate.dialect.MySQL5InnoDBDialect</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" />

  <bean id="dataSource"
    class="org.apache.commons.dbcp.BasicDataSource"
    p:driverClassName="com.mysql.jdbc.Driver"
    p:url="jdbc:mysql://localhost:3306/test?autoReconnect=true"
    p:password="notthepassword"
    p:username="mysql_user" />

  <!-- 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" />

  <!--
    Use JdbcLockingStrategy on 3.4.10 and before for HA setups where multiple nodes are attempting
    cleanup on a shared database.  This bean requires additional configuration:
     1. Creation of LOCKS table (see below)
     2. Setting ticket.cleaner.database.platform property
    Note that the ticket.cleaner.database.platform property should be set in cas.properties
    or some other properties file that is loaded by a PropertyPlaceholderConfigurer bean in
    the Spring context.
    Allowed values for ticket.cleaner.database.platform:
      * SqlServer for Microsoft SQL Server
      * HSQL for HSQLDB
      * SQL92 for any platform that supports the SQL-92 FOR UPDATE clause. (e.g. PostgreSQL, Oracle)
  -->
  <!--
  <bean id="cleanerLock"
    class="org.jasig.cas.ticket.registry.support.JdbcLockingStrategy"
    p:uniqueId="${host.name}"
    p:platform="${ticket.cleaner.database.platform}"
    p:applicationId="cas-ticket-registry-cleaner"
    p:dataSource-ref="dataSource" />
  -->

  <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>

Be sure to replace the dataSource information with your own. You may reference other data sources external to this file. For example, if you define an entityManagerFactory, transactionManager and dataSource in deployerConfigContext.xml, e.g., for the services registry, then there's no need to enter those here.

Persistence Configuration with Spring 3.1

If you're using Spring 3.1 and above in your CAS overlay and your persistence configuration involves using only one unit, you might be able to reduce your configuration by taking advantages of the Spring 3.1's new "packagesToScan" property. For more information on Spring functionality, please review this document,item 3.12. If you're interested to see this configuration in action, please review the code used for unit tests of the CAS project at this link

Configuring JPA Dependencies

In order for the JpaTicketRegistry to work you will need to add hibernate-core and hibernate-entitymanager to the dependencies listed in your pom.xml file.

If you're using the Maven2 WAR Overlay process then your pom.xml file should be in the root of your project directory. The hibernate.core.version property will need to be set by looking at the root POM of your CAS version's source tree.

If you're not using the Maven2 WAR Overlay process you can edit this file in the cas-server-webapp folder of the CAS source tree. The hibernate.core.version property should be picked up automatically from the root POM.

pom.xml (Using Maven2 WAR Overlay)
<dependencies>
  <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>
  <!--
    The following version is for demonstration only.
    Use the version defined in the properties section of the CAS root pom.xml file.
  -->
  <hibernate.core.version>3.6.0.Final</hibernate.core.version>
</properties>
pom.xml (Not Using Maven2 WAR Overlay)
<dependencies>
  <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>

Database Configuration

JDBC Driver

CAS must have access to the appropriate JDBC driver for your database. The example in the ticketRegistry.xml depends on the MySQL JDBC Driver.

You can download the MySQL JDBC Driver from the MySQL developers website.
http://www.mysql.com/downloads/connector/j/

Download the zip file and extract the mysql-connector JAR to your $TOMCAT_HOME/lib directory.

It is not a best practice to include the JDBC driver in with your cas.war build. It is best to place the JDBC driver in your Tomcat server's lib folder.

Database Creation

If your user has sufficient privileges (do this on a test/dev tier first), on start up, the database tables should be created.

Schema Generation

The database user must have CREATE/ALTER privileges to take advantage of automatic schema generation and schema updates.

Database Modifications

Avoiding Deadlocks

In some cases the Hibernate SchemaExport DDL creation tool will fail to create two very import indices when generating the ticket tables. The adbsence of these indices dramatically increases the potential for database deadlocks under load.

If the indices were not created you should manually create them before placing your CAS configuration into a production environment.

The following is an example of what you should find in MySQL when checking for these indices.

mysql> show index from SERVICETICKET where column_name='ticketGrantingTicket_ID';
+---------------+------------+--------------------+--------------+-------------------------+
| Table         | Non_unique | Key_name           | Seq_in_index | Column_name             |
+---------------+------------+--------------------+--------------+-------------------------+
| SERVICETICKET |          1 | FK7645ADE132A2C0E5 |            1 | ticketGrantingTicket_ID |
+---------------+------------+--------------------+--------------+-------------------------+
-----------+-------------+----------+--------+------+------------+---------+
 Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment |
-----------+-------------+----------+--------+------+------------+---------+
 A         |        NULL |     NULL | NULL   | YES  | BTREE      |         |
-----------+-------------+----------+--------+------+------------+---------+
1 row in set (0.00 sec)

mysql> show index from TICKETGRANTINGTICKET where column_name='ticketGrantingTicket_ID';
+----------------------+------------+--------------------+--------------+-------------------------+
| Table                | Non_unique | Key_name           | Seq_in_index | Column_name             |
+----------------------+------------+--------------------+--------------+-------------------------+
| TICKETGRANTINGTICKET |          1 | FKB4C4CDDE32A2C0E5 |            1 | ticketGrantingTicket_ID |
+----------------------+------------+--------------------+--------------+-------------------------+
-----------+-------------+----------+--------+------+------------+---------+
 Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment |
-----------+-------------+----------+--------+------+------------+---------+
 A         |        NULL |     NULL | NULL   | YES  | BTREE      |         |
-----------+-------------+----------+--------+------+------------+---------+
1 row in set (0.00 sec)

If you find the indices are missing the following SQL commands should create them.

For MySQL Databases
CREATE INDEX ST_TGT_FK_I ON SERVICETICKET (ticketGrantingTicket_ID);

CREATE INDEX ST_TGT_FK_I ON TICKETGRANTINGTICKET (ticketGrantingTicket_ID);
For Oracle Databases
CREATE INDEX "ST_TGT_FK_I"
  ON SERVICETICKET ("TICKETGRANTINGTICKET_ID")
  COMPUTE STATISTICS;

/** Create index on TGT self-referential foreign-key constraint */
CREATE INDEX "TGT_TGT_FK_I"
  ON TICKETGRANTINGTICKET ("TICKETGRANTINGTICKET_ID")
  COMPUTE STATISTICS;

Ticket Cleanup

The use of either JpaLockingStrategy or JdbcLockingStrategy (deprecated as of 3.4.11) is strongly recommended for HA environments where multiple nodes are attempting ticket cleanup on a shared database.  The LOCKS table must be created manually if using JdbcLockingStrategy.  (JpaLockingStrategy can auto-generate schema for the target platform.)  A representative schema is provided below.

Sample LOCKS Schema (PostgreSQL)
CREATE TABLE locks (
 application_id VARCHAR(50) NOT NULL,
 unique_id VARCHAR(50) NULL,
 expiration_date TIMESTAMP NULL
);
ALTER TABLE locks ADD CONSTRAINT pk_locks
 PRIMARY KEY (application_id);

Platform-Specific Issues

The exact DDL to create the LOCKS table may differ from the above.  For example, on Oracle platforms the expiration_date column must be of type DATE.  Use JpaLockingStrategy, which can create and update the schema automatically, to avoid platform-specific schema issues.

 

Database Connection Pooling

It is strongly recommended that database connection pooling be used in a production environment. The following configuration makes use of the c3p0 connection pooling library. Virginia Tech has used this connection pooling library in preference to the Apache Jakarta project's commons-dbcp library in production for months as of this writing (2010-01-20) and have been very pleased with performance and configurability.

deployerConfigContext.xml

The following configuration would replace the dataSource bean found in the ticketRegistry.xml example above.

src/main/webapp/WEB-INF/deployerConfigContext.xml
<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}"
  />

The following pool configuration parameters are provided for information only and may serve as a reasonable starting point for configuring a production database connection pool.  Note the health check query is specific to PostgreSQL.

Virginia Tech Pool Configuration
 # == Basic database connection pool configuration ==
database.dialect=org.hibernate.dialect.PostgreSQLDialect
database.driverClass=org.postgresql.Driver
database.url=jdbc:postgresql://somehost.vt.edu/cas?ssl=true
database.user=somebody
database.password=meaningless
database.pool.minSize=6
database.pool.maxSize=18

# 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=6

# == 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 aquire retry attempts.
database.pool.acquireRetryDelay=2000

pom.xml

In order to use c3p0, the maven dependency for the library must be included in your Maven War Overlay POM.

pom.xml
<dependencies>
  <dependency>
    <groupId>c3p0</groupId>
    <artifactId>c3p0</artifactId>
    <version>0.9.1.2</version>
    <scope>runtime</scope>
  </dependency>
</dependencies>

Platform-Specific Considerations

MySQL

Use InnoDB Tables

The use of InnoDB tables is strongly recommended for the MySQL platform for a couple reasons:

  • InnoDB provides referential integrity that is helpful for preventing orphaned records in ticket tables.
  • Provides better locking semantics (e.g. support for SELECT ... FOR UPDATE) than the default MyISAM table type.

InnoDB tables are easily specified via the use of the following Hibernate dialect:

<prop key="hibernate.dialect">org.hibernate.dialect.MySQLInnoDBDialect</prop>

<!-- OR for MySQL 5.x use the following instead -->
<prop key="hibernate.dialect">org.hibernate.dialect.MySQL5InnoDBDialect</prop>

BLOB vs LONGBLOB

Hibernate on recent versions of MySQL (e.g. 5.1) properly map the @Lob JPA annotation onto type LONGBLOB, which is very important since these fields commonly store serialized graphs of Java objects that grow proportionally with CAS SSO session lifetime. Under some circumstances, e.g. old versions of MySQL prior to 5.1, Hibernate may treat these columns as type BLOB, which have storage limits that are easily exceeded. It is recommended that the generated schema be reviewed and any BLOB type columns be converted to LONGBLOB.

Sample MySQL Schema Review
mysql> use cas;
Database changed
mysql> show tables;
+-----------------------+
| Tables_in_cas         |
+-----------------------+
| LOCKS                 |
| RegisteredServiceImpl |
| SERVICETICKET         |
| TICKETGRANTINGTICKET  |
| rs_attributes         |
+-----------------------+
5 rows in set (0.00 sec)

mysql> show columns from SERVICETICKET;
+-------------------------+--------------+------+-----+---------+-------+
| Field                   | Type         | Null | Key | Default | Extra |
+-------------------------+--------------+------+-----+---------+-------+
| ID                      | varchar(255) | NO   | PRI | NULL    |       |
| NUMBER_OF_TIMES_USED    | int(11)      | YES  |     | NULL    |       |
| CREATION_TIME           | bigint(20)   | YES  |     | NULL    |       |
| EXPIRATION_POLICY       | longblob     | NO   |     | NULL    |       |
| LAST_TIME_USED          | bigint(20)   | YES  |     | NULL    |       |
| PREVIOUS_LAST_TIME_USED | bigint(20)   | YES  |     | NULL    |       |
| FROM_NEW_LOGIN          | bit(1)       | NO   |     | NULL    |       |
| TICKET_ALREADY_GRANTED  | bit(1)       | NO   |     | NULL    |       |
| SERVICE                 | longblob     | NO   |     | NULL    |       |
| ticketGrantingTicket_ID | varchar(255) | YES  | MUL | NULL    |       |
+-------------------------+--------------+------+-----+---------+-------+
10 rows in set (0.00 sec)

mysql> show columns from TICKETGRANTINGTICKET;
+----------------------------+--------------+------+-----+---------+-------+
| Field                      | Type         | Null | Key | Default | Extra |
+----------------------------+--------------+------+-----+---------+-------+
| ID                         | varchar(255) | NO   | PRI | NULL    |       |
| NUMBER_OF_TIMES_USED       | int(11)      | YES  |     | NULL    |       |
| CREATION_TIME              | bigint(20)   | YES  |     | NULL    |       |
| EXPIRATION_POLICY          | longblob     | YES  |     | NULL    |       |
| LAST_TIME_USED             | bigint(20)   | YES  |     | NULL    |       |
| PREVIOUS_LAST_TIME_USED    | bigint(20)   | YES  |     | NULL    |       |
| AUTHENTICATION             | longblob     | NO   |     | NULL    |       |
| EXPIRED                    | bit(1)       | NO   |     | NULL    |       |
| SERVICES_GRANTED_ACCESS_TO | blob         | YES  |     | NULL    |       |
| ticketGrantingTicket_ID    | varchar(255) | YES  | MUL | NULL    |       |
+----------------------------+--------------+------+-----+---------+-------+
10 rows in set (0.00 sec)

In the example above you can see that column SERVICES_GRANTED_ACCESS_TO in the TICKETGRANTINGTICKET table is set to BLOB. The following MySQL statement would change this column's type to LONGBLOB.

ALTER TABLE TICKETGRANTINGTICKET MODIFY SERVICES_GRANTED_ACCESS_TO LONGBLOB;

Case Sensitive Schema

It may necessary to force lowercase schema names in the MySQL configuration:

my.cnf
lower-case-table-names = 1