How to secure a Java client application with CAS (using Spring Boot)

CAS can mean two things: the open source single sign-on server maintained by the Apereo foundation, or the Central Authentication Service protocol. Here, we’ll use the CAS protocol to connect our Java application to a CAS server.

The flow is easy to follow. Your application redirects the user to the CAS login page. After authentication, CAS sends the browser back with a short-lived service ticket. Your application validates this ticket with the CAS server to find out who the user is.

Let’s see how to do this with pac4j and Spring Boot. We’ll use the public pac4j test server first, then look at service registration for your own CAS server.

What you need:

1) Create the Maven project

Create an empty Spring Boot project with Java 17:

mkdir -p spring-cas-app/src/main/java/org/example
mkdir -p spring-cas-app/src/main/resources
cd spring-cas-app

Create a pom.xml file at the project root. The Spring Boot parent manages the Spring dependencies, and its Maven plugin runs the application:

<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>4.1.1</version>
        <relativePath/>
    </parent>
    <groupId>org.example</groupId>
    <artifactId>spring-cas-app</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <java.version>17</java.version>
    </properties>

    <dependencies>
        <!-- Add the dependencies from section 2 here. -->
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

Save the Java classes below in the org.example package so that Spring Boot discovers the configuration and controllers. Put application properties in src/main/resources/application.properties.

2) Add the Maven dependencies

Inside the <dependencies> element, on top of Spring MVC, you need the pac4j Spring MVC integration and the CAS module:

<!-- Spring Boot web -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- pac4j implementation for Spring MVC so for Spring Boot as well -->
<dependency>
    <groupId>org.pac4j</groupId>
    <artifactId>spring-webmvc-pac4j</artifactId>
    <version>8.0.4</version>
</dependency>
<!-- pac4j support for CAS -->
<dependency>
    <groupId>org.pac4j</groupId>
    <artifactId>pac4j-cas</artifactId>
    <version>6.5.9</version>
</dependency>

pac4j-cas is a full CAS client (based on the official Apereo CAS client): it speaks the CAS protocol versions 1.0, 2.0 and 3.0, and also supports proxy tickets and the CAS REST API, covered at the end of this guide.

3) Configure CAS authentication

Create src/main/java/org/example/SecurityConfig.java:

package org.example;

import org.pac4j.core.config.Config;
import org.pac4j.springframework.config.Pac4jSecurityConfig;
import org.pac4j.cas.client.CasClient;
import org.pac4j.cas.config.CasConfiguration;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;

@Configuration
public class SecurityConfig extends Pac4jSecurityConfig {

    @Value("${app.base-url:http://localhost:8080}")
    private String baseUri;

    @Value("${cas.login-url:https://www.casserverpac4j.dev/login}")
    private String casLoginUrl;

    @Bean
    public Config config() {
        // configuration of the authentication via the CAS protocol
        return new Config(baseUri + "/callback", new CasClient(new CasConfiguration(casLoginUrl)));
    }

    @Override
    public void addInterceptors(final InterceptorRegistry registry) {
        // the /protected/** URLs require the CAS authentication
        addSecurity(registry, "CasClient").addPathPatterns("/protected/**");
    }
}

The CAS configuration starts with a single URL: CasConfiguration(casLoginUrl). pac4j derives the ticket validation URL from it. If validation must use an internal address, you can set it separately with config.setPrefixUrl("http://cas-internal:8080/cas").

Pac4jSecurityConfig registers /callback, where CAS sends the service ticket, and /logout. With new Config(baseUri + "/callback", ...), we tell pac4j where the callback lives. It appends ?client_name=CasClient, and this full URL is the service CAS will see.

We then protect /protected/** with addSecurity(registry, "CasClient"). When an anonymous user requests a protected page, pac4j redirects the browser to casLoginUrl with that service URL as a parameter.

pac4j uses the CAS 3.0 protocol by default, which returns the user attributes with the validation response. Switch with config.setProtocol(CasProtocol.CAS20) for an older server. Two other options are worth knowing: setRenew(true) forces the user to re-enter credentials even with an active SSO session, and setGateway(true) returns silently when the user is not logged in instead of showing the login page.

4) Register the service on the CAS server

A CAS server only issues tickets for services it knows. In the CAS service registry, declare a service whose serviceId pattern matches the callback URL. With the JSON service registry of Apereo CAS v6+, the definition looks like this:

{
  "@class" : "org.apereo.cas.services.CasRegisteredService",
  "serviceId" : "^http://localhost:8080/callback\\?client_name=CasClient.*",
  "name" : "pac4j Spring Boot demo",
  "id" : 1000,
  "attributeReleasePolicy" : {
    "@class" : "org.apereo.cas.services.ReturnAllAttributeReleasePolicy"
  }
}

There are two settings to pay attention to:

See the Apereo CAS service management documentation for the other registries (database, LDAP, Git…) and options.

5) Access the authenticated user

Create src/main/java/org/example/Application.java to expose a public page and a protected page:

package org.example;

import org.pac4j.core.profile.ProfileManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class Application {

    @Autowired
    private ProfileManager profileManager;

    @GetMapping(value = "/", produces = "text/html")
    public String index() {
        return "<h1>Public area</h1><p><a href='/protected/index'>Protected area</a></p>"
            + "<p><a href='/logout'>Logout</a></p>";
    }

    @GetMapping(value = "/protected/index", produces = "text/plain")
    public String secure() {
        return "Protected area\n" + profileManager.getProfiles() + "\nVisit /logout to sign out.";
    }
}

After login, the ProfileManager returns a CasProfile. Its identifier is the CAS principal, usually the username, and its attributes are exactly those released by the service’s attribute release policy:

final var profile = (CasProfile) profileManager.getProfile().orElseThrow();
profile.getId();                 // the CAS principal
profile.getAttribute("email");
profile.getAttributes();         // everything the release policy allowed

No attributes in the profile? Check what the service is allowed to receive. The release policy may filter them out, or the server may still use CAS 2.0, which does not carry attributes.

6) Logout

Logging out of the application and logging out of CAS are two different operations. The /logout link removes the local profile. To also end the SSO session on the CAS server, enable central logout in application.properties:

pac4j.logout.centralLogout=true
pac4j.logout.defaultUrl=/

pac4j then redirects the browser to the CAS /logout endpoint. The reverse direction works too: when the user logs out from CAS or from another application, the CAS server notifies every service that received a ticket during the session, and the CasClient processes these single logout requests to destroy the local session.

7) Run the application

Create src/main/java/org/example/SpringBootDemo.java:

package org.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringBootDemo {
    public static void main(final String[] args) {
        SpringApplication.run(SpringBootDemo.class, args);
    }
}

Start the application with mvn spring-boot:run.

Open http://localhost:8080/ and click on the Protected area link. You are redirected to the CAS login page, then sent back to the callback with a service ticket, and the protected page prints your profile.

If something goes wrong:

8) Beyond the login page

So far, we have used CAS to log in through a browser. The same pac4j-cas module also handles other situations:

9) Learn more

Discover more pac4j frameworks and more authentication mechanisms…