Single Sign-On with Apereo CAS
This tutorial walks a newcomer through standing up an Apereo CAS server for single sign-on and wiring a Java web application to it as a client. The complete CAS 7.1 server is available as a companion WAR overlay on GitLab.
Note: This post has been rewritten for Apereo CAS 7.x. It was originally written for CAS 4.x, which you set up by copying the server source and hand-editing a pile of XML files (
deployerConfigContext.xml,ticketGrantingTicketCookieGenerator.xml, …). CAS has changed completely since: theorg.jasig.caspackages becameorg.apereo.cas, and the server is now built as a Spring Boot WAR overlay configured through a singlecas.propertiesfile.
What’s CAS?
CAS (Central Authentication Service) is an enterprise single sign-on server. A user logs in once, at the CAS server, and every application that trusts that server lets them in without asking for credentials again. The project started in higher education and is now used everywhere from small installations to Fortune 500 companies.
What you get out of the box:
- An open, well-documented protocol (the CAS protocol), plus support for SAML, OAuth, and OpenID Connect
- A Java server component you deploy yourself
- Pluggable authentication: LDAP, database (JDBC), X.509, multi-factor, and more
- Client libraries for Java, .NET, PHP, Apache, and others
How CAS single sign-on works
Before configuring anything, it helps to know the round trip you are setting up:
- A user hits a protected page in your application. The CAS client sees there is no session and redirects the browser to the CAS server’s
/loginpage. - The user authenticates at the CAS server. The server stores a Ticket-Granting Ticket (TGT) in a cookie so it won’t ask again, and redirects the browser back to your app with a one-time Service Ticket (
?ticket=ST-...) in the URL. - The client takes that Service Ticket, calls the CAS server behind the scenes to validate it, and, if valid, establishes a local session.
Everything below is just teaching each side how to play its part: the server (who can log in, which apps are allowed) and the client (how to hand off to the server and validate tickets).
Setting up the CAS server
Modern CAS is not an application you download and unzip. You generate a thin Gradle project, the WAR overlay, that pulls CAS in as a dependency and lets you layer your own configuration and modules on top. This keeps your customizations cleanly separated from the upstream code, so upgrades are mostly a version bump.
1. Generate the WAR overlay
Grab the official template (or generate one from the CAS Initializr) and check out the branch matching the CAS version you want:
git clone --branch 7.1 https://github.com/apereo/cas-overlay-template.git cas
cd cas
2. Add the modules you need
CAS ships as dozens of optional modules; the base overlay includes almost nothing on purpose. Declare the features you want in build.gradle. For database login and file-based service registration we need two:
dependencies {
// Authenticate users against a relational database
implementation "org.apereo.cas:cas-server-support-jdbc"
// Register trusted applications as JSON files
implementation "org.apereo.cas:cas-server-support-json-service-registry"
// JDBC driver for your database (MySQL shown here)
runtimeOnly "com.mysql:mysql-connector-j"
}
You don’t pin versions; the CAS BOM keeps every module in lockstep with the CAS version from step 1.
3. Authenticate against a database
CAS configuration lives in etc/cas/config/cas.properties. Suppose users are stored in a users table:
| Column | Type |
|---|---|
| username | VARCHAR(255) |
| password | VARCHAR(255) |
Tell CAS how to reach the database and how to verify a password:
# Where the users live
cas.authn.jdbc.query[0].url=jdbc:mysql://localhost:3306/cas?useSSL=false
cas.authn.jdbc.query[0].user=cas
cas.authn.jdbc.query[0].password=changeit
cas.authn.jdbc.query[0].driver-class=com.mysql.cj.jdbc.Driver
cas.authn.jdbc.query[0].dialect=org.hibernate.dialect.MySQLDialect
# The query receives the username and must return the password column
cas.authn.jdbc.query[0].sql=SELECT * FROM users WHERE username = ?
cas.authn.jdbc.query[0].field-password=password
# How the stored password was hashed
cas.authn.jdbc.query[0].password-encoder.type=BCRYPT
A couple of things worth calling out. The [0] is an index: CAS lets you chain several authentication sources, and this is just the first. And note password-encoder.type=BCRYPT: the original version of this post used MD5, which is no longer safe for storing passwords. Hash new passwords with BCrypt (or PBKDF2/SCrypt) and let CAS compare against that.
4. Register your applications
CAS will only issue tickets to applications it has been told to trust. With the JSON service registry, each trusted app is a file under etc/cas/services/. The file name follows the convention serviceName-id.json, e.g. MyApplication-10000001.json:
{
"@class": "org.apereo.cas.services.CasRegisteredService",
"serviceId": "^https?://(localhost|127\\.0\\.0\\.1)(:\\d+)?/.*",
"name": "MyApplication",
"id": 10000001,
"description": "Local development application",
"evaluationOrder": 10
}
serviceId is a regex matched against the URL the app asks CAS to redirect back to. The pattern above accepts anything running on localhost. In production, lock this down to your real https:// hosts. evaluationOrder decides which definition wins when several patterns match.
Point CAS at the directory in cas.properties:
cas.service-registry.json.location=file:/etc/cas/services
5. Build and run
Copy your config into place and start the embedded server:
./gradlew copyCasConfiguration # copies etc/ to /etc/cas
./gradlew createKeystore # self-signed cert for HTTPS (password: changeit)
./gradlew run # or: ./gradlew clean build && java -jar build/libs/cas.war
CAS comes up at https://localhost:8443/cas. Because the certificate is self-signed, your browser will warn you the first time; accept it for local development. Modern CAS assumes HTTPS throughout; if you genuinely need the ticket-granting cookie to survive a plain-HTTP service during development, set cas.tgc.secure=false, but don’t carry that into production.
Connecting a client application
The server is useless until an app talks to it. The Java CAS Client is a small set of servlet filters that handle the redirect-and-validate dance for you. Add it to your application’s pom.xml:
<dependency>
<groupId>org.apereo.cas.client</groupId>
<artifactId>cas-client-core</artifactId>
<version>4.0.2</version> <!-- check Maven Central for the latest 4.x -->
</dependency>
The group ID changed from
org.jasig.cas.client(used in the original post) toorg.apereo.cas.client, and the 4.x line targets Java 17. If you already use Spring Security or pac4j in your app, both have first-class CAS support and may be a better fit than wiring filters by hand.
Then declare the filters in web.xml. The minimum useful chain is two filters: one that redirects unauthenticated users to CAS, and one that validates the ticket CAS sends back. Order matters: the validation filter must come before the authentication filter:
<!-- 1. Validates the Service Ticket that CAS appends to the callback URL -->
<filter>
<filter-name>CAS Validation Filter</filter-name>
<filter-class>org.apereo.cas.client.validation.Cas30ProxyReceivingTicketValidationFilter</filter-class>
<init-param>
<param-name>casServerUrlPrefix</param-name>
<param-value>https://localhost:8443/cas</param-value>
</init-param>
<init-param>
<param-name>serverName</param-name>
<param-value>http://localhost:8080</param-value>
</init-param>
</filter>
<!-- 2. Redirects users without a session to the CAS login page -->
<filter>
<filter-name>CAS Authentication Filter</filter-name>
<filter-class>org.apereo.cas.client.authentication.AuthenticationFilter</filter-class>
<init-param>
<param-name>casServerLoginUrl</param-name>
<param-value>https://localhost:8443/cas/login</param-value>
</init-param>
<init-param>
<param-name>serverName</param-name>
<param-value>http://localhost:8080</param-value>
</init-param>
</filter>
<!-- 3. Exposes the authenticated user via request.getRemoteUser() -->
<filter>
<filter-name>CAS HttpServletRequest Wrapper Filter</filter-name>
<filter-class>org.apereo.cas.client.util.HttpServletRequestWrapperFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>CAS Validation Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>CAS Authentication Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>CAS HttpServletRequest Wrapper Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
Two values do all the work here: casServerUrlPrefix/casServerLoginUrl point at the server you started above, and serverName is your application’s own base URL; it’s what CAS redirects back to, and it must match a serviceId pattern you registered in step 4.
Verifying the flow
With both sides running, you can walk the round trip end to end:
- Visit any page in your application. Because you have no session, the authentication filter bounces you to
https://localhost:8443/cas/login. - Log in with a user from your database. CAS validates the password, drops its TGT cookie, and redirects back to your app with
?ticket=ST-.... - The validation filter exchanges that ticket with the CAS server, your session is established, and the
ticketparameter disappears from the URL. - From now on,
request.getRemoteUser()returns the logged-in username, and any other app trusting the same server will let the user straight in, no second login. That’s the single sign-on payoff.
If a login loops back to the CAS page instead of returning, it almost always means your app’s serverName doesn’t match a registered serviceId; revisit step 4.
The Disqus comment system is loading ...
If the message does not appear, please check your Disqus configuration.