CAS SSO With Spring Security

1. Overview

In this article, we’re going to look at integrating the Central Authentication Service (CAS) with Spring Security. CAS is a Single Sign-On (SSO) service.

Let’s say we have applications requiring user authentication. The most common method is to implement a security mechanism for each application. However, it’d be better to implement user authentication for all the apps in one place.

This is precisely what the CAS SSO system does. This article gives more details on the architecture. The protocol diagram can be found here.

2. Project Setup and Installation

There’re at least two components involved in setting up a Central Authentication Service. One component is a Spring-based server – called cas-server. Other components are made up of one or more clients.

A client can be any web application that’s using the server for authentication.

2.1. CAS Server Setup

The server uses the Maven (Gradle) War Overlay style to facilitate easy setup and deployment. There’s a quick start template that can be cloned and used.

Let’s clone it:

git clone https://github.com/apereo/cas-overlay-template.git cas-server

This command clones the cas-overlay-template into the cas-server directory on the local machine.

Next, let’s add additional dependencies to the root pom.xml. These dependencies enable service registration via a JSON configuration.

Also, they facilitate connections to the database:

<dependency>
    <groupId>org.apereo.cas</groupId>
    <artifactId>cas-server-support-json-service-registry</artifactId>
    <version>${cas.version}</version>
</dependency>
<dependency>
    <groupId>org.apereo.cas</groupId>
    <artifactId>cas-server-support-jdbc</artifactId>
    <version>${cas.version}</version>
</dependency>
<dependency>
    <groupId>org.apereo.cas</groupId>
    <artifactId>cas-server-support-jdbc-drivers</artifactId>
    <version>${cas.version}</version>
</dependency>

The latest version of cas-server-support-json-service-registry, cas-server-support-jdbc and cas-server-support-jdbc-drivers dependencies can be found on Maven Central. Please note that the parent pom.xml automatically manages the artifact versions.

Next, let’s create the folder cas-server/src/main/resources and copy the folder cas-server/etc. into it. We’re also going to change the port of the application as well as the path of the SSL key store.

We configure these by editing the associated entries in cas-server/src/main/resources/application.properties:

server.port=6443
server.ssl.key-store=classpath:/etc/cas/thekeystore
standalone.config=classpath:/etc/cas/config

The config folder path was also set to classpath:/etc/cas/config. It points to the cas-server/src/main/resources/etc/cas/config.

The next step is to generate a local SSL key store. The key store is used for establishing HTTPS connections. This step is important and may not be skipped.

From the terminal, change directory to cas-server/src/main/resources/etc/cas. After that run the following command:

keytool -genkey -keyalg RSA -alias thekeystore -keystore thekeystore
-storepass changeit -validity 360 -keysize 2048

It’s important to use localhost when prompted for a first and last name, organization name and even organization unit. Failure to do this may lead an to error during SSL Handshake. Other fields such as city, state and country can be set as appropriate.

The above command generates a key store with the name thekeystore and password changeit. It’s stored in the current directory.

Next, the generated key store need to be exported to a .crt format for use by the client applications. So, still in the same directory, run the following command to export the generated thekeystore file to thekeystore.crt. The password remains unchanged:

keytool -export -alias thekeystore -file thekeystore.crt
-keystore thekeystore

Now, let’s import the exported thekeystore.crt into the Java cacerts key store. The terminal prompt should still be in the directory cas-server/src/main/resources/etc/cas directory.

From there, execute the command:

keytool -import -alias thekeystore -storepass changeit -file thekeystore.crt
 -keystore "C:\Program Files\Java\jdk1.8.0_152\jre\lib\security\cacerts"

Just to be double sure, we can also import the certificate into a JRE that is outside of the JDK installation:

keytool -import -alias thekeystore -storepass changeit -file thekeystore.crt
-keystore "C:\Program Files\Java\jre1.8.0_152\lib\security\cacerts"

Note that the -keystore flag points to the location of the Java key store on the local machine. This location may be different depending on the Java installation at hand.

Moreover, ensure that the JRE that is referenced as the location of the key store is the same as the one that is used for the client application.

After successfully adding thekeystore.crt to the Java key store, we need to restart the system. Equivalently, we can kill every instance of the JVM running on the local machine.

Next, from the root project directory, cas-server, invoke the commands build package and build run from the terminal. Starting the server may take some time. When it’s ready, it prints READY in the console.

At this point, visiting https://localhost:6443/cas with a browser renders a login form. The default username is casuser and password is Mellon.

2.2. CAS Client Setup

Let’s use the Spring Initializr to generate the project with the following dependencies: Web, Security, Freemarker and optionally DevTools.

In addition to the dependencies generated by Spring Initializr, let’s add the dependency for the Spring Security CAS module:

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-cas</artifactId>
</dependency>

The latest version of the dependency can be found on Maven Central. Let’s also configure the server’s port to listen on port 9000 by adding the following entry in application.properties:

server.port=9000

3. Registering Services/Clients with CAS Server

The server doesn’t allow just any client to access it for authentication. The clients/services must be registered in the CAS server services registry.

There’re a couple of ways of registering a service with the server. These include YAML, JSON, Mongo, LDAP, and others.

Depending on the method, there are dependencies to be included in the pom.xml file. In this article, we use the JSON Service Registry method. The dependency was already included in the pom.xml file in the previous section.

Let’s create a JSON file that contains the definition of the client application. Inside the cas-server/src/main/resources folder, let’s create yet another folder – services. It’s this services folder that contains the JSON files.

Next, we create a JSON file named, casSecuredApp-19991.json in the cas-server/src/main/resources/services directory with the following content:

{
    "@class" : "org.apereo.cas.services.RegexRegisteredService",
    "serviceId" : "^http://localhost:9000/login/cas",
    "name" : "CAS Spring Secured App",
    "description": "This is a Spring App that usses the CAS Server for it's authentication",
    "id" : 19991,
    "evaluationOrder" : 1
}

The serviceId attribute defines a regex URL pattern for the client application that intends to use the server for authentication. In this case, the pattern matches an application running on localhost and listening on port 9000.

The id attribute should be unique to avoid conflicts and accidentally overriding configurations. The service configuration file name follows the convention serviceName-id.json. Other configurable attributes such as theme, proxyPolicy, logo, privacyUrl, and others can be found here.

For now, let’s just add two more configuration items to turn the JSON Service Registry on. One is to inform the server on the directory where the service configuration files are located. The other is to enable initialization of the service registry from JSON configuration files.

Both these configuration items are placed in another file, named cas.properties. We create this file in the cas-server/src/main/resources directory:

cas.serviceRegistry.initFromJson=true
cas.serviceRegistry.config.location=classpath:/services

Let’s execute the build run command again and take note of lines such as “Loaded [3] service(s) from [JsonServiceRegistryDao]” on the console.

4. Spring Security Configuration


==== 4.1. Configuring Single Sign-On

Now that the Spring Boot Application has been registered with the CAS Server as a service. Let’s configure Spring Security to work in concert with the server for user authentication. The full sequence of interactions between Spring Security and server can be found here.

Let’s first configure the beans that are related to the CAS module of Spring Security. This enables Spring Security to collaborate with the central authentication service.

To this extent, we need to add config beans to the CasSecuredAppApplication class the entry point to the Spring Boot application:

@Bean
public ServiceProperties serviceProperties() {
    ServiceProperties serviceProperties = new ServiceProperties();
    serviceProperties.setService("http://localhost:9000/login/cas");
    serviceProperties.setSendRenew(false);
    return serviceProperties;
}

@Bean
@Primary
public AuthenticationEntryPoint authenticationEntryPoint(
  ServiceProperties sP) {

    CasAuthenticationEntryPoint entryPoint
      = new CasAuthenticationEntryPoint();
    entryPoint.setLoginUrl("https://localhost:6443/cas/login");
    entryPoint.setServiceProperties(sP);
    return entryPoint;
}

@Bean
public TicketValidator ticketValidator() {
    return new Cas30ServiceTicketValidator(
      "https://localhost:6443/cas");
}

@Bean
public CasAuthenticationProvider casAuthenticationProvider() {

    CasAuthenticationProvider provider = new CasAuthenticationProvider();
    provider.setServiceProperties(serviceProperties());
    provider.setTicketValidator(ticketValidator());
    provider.setUserDetailsService(
      s -> new User("casuser", "Mellon", true, true, true, true,
        AuthorityUtils.createAuthorityList("ROLE_ADMIN")));
    provider.setKey("CAS_PROVIDER_LOCALHOST_9000");
    return provider;
}

We configure the ServiceProperties bean with the default service login URL that the CasAuthenticationFilter will be internally mapped to. The sendRenew property of ServiceProperties is set to false. As a consequence, a user only needs to present login credentials to the server once.

Subsequent authentication will be done automatically, i.e., without asking the user for username and password again. This behavior means that a single user that has access to multiple services that use the same server for authentication.

As we’ll see later, if a user logs out from the server completely, his ticket is invalidated. As a consequence, the user is logged out of all applications connected to the server at the same time. This is called the Single Logout.

We configure the AuthenticationEntryPoint bean with the default login URL of the server. Note that this URL is different from the Service login URL. This server login URL is the location where the user will be redirected to for authentication.

The TicketValidator is the bean that the service app uses to validate a service ticket granted to a user upon successful authentication with the server.

The flow is:

  1. A user attempts to access a secured page

  2. The AuthenticationEntryPoint is triggered and takes the user to the server. The login address of the server has been specified in the AuthenticationEntryPoint

  3. On a successful authentication with the server, it redirects the request back to the service URL that has been specified, with the service ticket appended as a query parameter

  4. CasAuthenticationFilter is mapped to a URL that matches the pattern and in turn, triggers the ticket validation internally.

  5. If the ticket is valid, a user will be redirected to the originally requested URL

Now, we need to configure Spring Security to protect some routes and use the CasAuthenticationEntryPoint bean.

Let’s create SecurityConfig.java that extends WebSecurityConfigurerAdapter and override the config():

@EnableWebSecurity
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
      http
        .authorizeRequests()
        .regexMatchers("/secured.*", "/login")
        .authenticated()
        .and()
        .authorizeRequests()
        .regexMatchers("/")
        .permitAll()
        .and()
        .httpBasic()
        .authenticationEntryPoint(authenticationEntryPoint);
    }
    // ...
}

Also, in the SecurityConfig class, we override the following methods and create the CasAuthenticationFilter bean at the same time:

@EnableWebSecurity
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private AuthenticationProvider authenticationProvider;
    private AuthenticationEntryPoint authenticationEntryPoint;
    private SingleSignOutFilter singleSignOutFilter;
    private LogoutFilter logoutFilter;

    @Autowired
    public SecurityConfig(CasAuthenticationProvider casAuthenticationProvider, AuthenticationEntryPoint eP,
                          LogoutFilter lF
                          , SingleSignOutFilter ssF
    ) {
        this.authenticationProvider = casAuthenticationProvider;
        this.authenticationEntryPoint = eP;

        this.logoutFilter = lF;
        this.singleSignOutFilter = ssF;

    }

    // ...

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
      auth.authenticationProvider(authenticationProvider);
    }

    @Override
    protected AuthenticationManager authenticationManager() throws Exception {
      return new ProviderManager(Arrays.asList(authenticationProvider));
    }

    @Bean
    public CasAuthenticationFilter casAuthenticationFilter(ServiceProperties sP) throws Exception {
      CasAuthenticationFilter filter = new CasAuthenticationFilter();
      filter.setServiceProperties(sP);
      filter.setAuthenticationManager(authenticationManager());
      return filter;
    }
}

Let’s create controllers that handle requests directed to /secured, /login and the home page as well.

The homepage is mapped to an IndexController that has a method index(). This method merely returns the index view:

@GetMapping("/")
public String index() {
    return "index";
}

The /login path is mapped to the login() method from the AuthController class. It just redirects to the default login successful page.

Notice that while configuring the HttpSecurity above, we configured the /login path so that it requires authentication. This way, we redirect the user to the CAS server for authentication.

This mechanism is a bit different from the normal configuration where the /login path is not a protected route and returns a login form:

@GetMapping("/login")
public String login() {
    return "redirect:/secured";
}

The /secured path is mapped to the index() method from the SecuredPageController class. It gets the username of the authenticated user and displays it as part of the welcome message:

@GetMapping
public String index(ModelMap modelMap) {
  Authentication auth = SecurityContextHolder.getContext()
    .getAuthentication();
  if(auth != null
    && auth.getPrincipal() != null
    && auth.getPrincipal() instanceof UserDetails) {
      modelMap.put("username", ((UserDetails) auth.getPrincipal()).getUsername());
  }
  return "secure/index";
}

Note that all the views are available in the resources folder of the cas-secured-app. At this point, the cas-secured-app should be able to use the server for authentication.

Finally, we execute build run from the terminal and simultaneously start the Spring boot app as well. Note that SSL is key in this whole process, so the SSL generation step above should not be skipped!

4.2. Configuring Single Logout

Let’s proceed with the authentication process by logging out a user from the system. There are two places a user can be logged out from: the client app and the server.

Logging a user out of the client app/service is the first thing to do. This does not affect the authentication state of the user in other applications connected to the same server. Of course, logging a user out from the server also logs the user out from all other registered services/clients.

Let’s start by defining some bean configurations in CasSecuredAppApplicaiton class:

@Bean
public SecurityContextLogoutHandler securityContextLogoutHandler() {
    return new SecurityContextLogoutHandler();
}

@Bean
public LogoutFilter logoutFilter() {
    LogoutFilter logoutFilter = new LogoutFilter(
      "https://localhost:6443/cas/logout",
      securityContextLogoutHandler());
    logoutFilter.setFilterProcessesUrl("/logout/cas");
    return logoutFilter;
}

@Bean
public SingleSignOutFilter singleSignOutFilter() {
    SingleSignOutFilter singleSignOutFilter = new SingleSignOutFilter();
    singleSignOutFilter.setCasServerUrlPrefix("https://localhost:6443/cas");
    singleSignOutFilter.setIgnoreInitConfiguration(true);
    return singleSignOutFilter;
}

@EventListener
public SingleSignOutHttpSessionListener singleSignOutHttpSessionListener(
  HttpSessionEvent event) {
    return new SingleSignOutHttpSessionListener();
}

We configure the logoutFilter to intercept the URL pattern /logout/cas and to redirect the application to the server for a system-wide log-out. The server sends a single logout request to all services concerned. Such a request is handled by the SingleSignOutFilter, which invalidates the HTTP session.

Let’s modify the HttpSecurity configuration in the config() of SecurityConfig class. The CasAuthenticationFilter and LogoutFilter that were configured earlier are now added to the chain as well:

http
  .authorizeRequests()
  .regexMatchers("/secured.*", "/login")
  .authenticated()
  .and()
  .authorizeRequests()
  .regexMatchers("/")
  .permitAll()
  .and()
  .httpBasic()
  .authenticationEntryPoint(authenticationEntryPoint)
  .and()
  .logout().logoutSuccessUrl("/logout")
  .and()
  .addFilterBefore(singleSignOutFilter, CasAuthenticationFilter.class)
  .addFilterBefore(logoutFilter, LogoutFilter.class);

For the logout to work correctly, we should implement a logout() method that first logs a user out of the system locally and shows a page with a link to optionally log the user out from all other services connected to the server.

The link is the same as the one set as the filter process URL of the LogoutFilter we configured above:

@GetMapping("/logout")
public String logout(
  HttpServletRequest request,
  HttpServletResponse response,
  SecurityContextLogoutHandler logoutHandler) {
    Authentication auth = SecurityContextHolder
      .getContext().getAuthentication();
    logoutHandler.logout(request, response, auth );
    new CookieClearingLogoutHandler(
      AbstractRememberMeServices.SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY)
      .logout(request, response, auth);
    return "auth/logout";
}

The logout view:

<html>
<head>
    <title>Cas Secured App - Logout</title>
</head>
<body>
<h1>You have logged out of Cas Secured Spring Boot App Successfully</h1>
<br>
<a href="/logout/cas">Log out of all other Services</a>
</body>
</html>

5. Connecting the CAS Server to a Database

We’ve been using static user credentials for authentication. However, in production environments, user credentials are stored in a database most of the time. So, next, we show how to connect our server to a MySQL database (database name: test) running locally.

We do this by appending the following data to the application.properties file in the cas-server/src/main/resources directory:

cas.authn.accept.users=
cas.authn.accept.name=

cas.authn.jdbc.query[0].sql=SELECT * FROM users WHERE email = ?
cas.authn.jdbc.query[0].url=jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC
cas.authn.jdbc.query[0].dialect=org.hibernate.dialect.MySQLDialect
cas.authn.jdbc.query[0].user=root
cas.authn.jdbc.query[0].password=root
cas.authn.jdbc.query[0].ddlAuto=none
cas.authn.jdbc.query[0].driverClass=com.mysql.cj.jdbc.Driver
cas.authn.jdbc.query[0].fieldPassword=password
cas.authn.jdbc.query[0].passwordEncoder.type=NONE

Remember that the complete content of application.properties can be found in the source code. Leaving the value of cas.authn.accept.users blank deactivates the use of static user repositories by the server.

Furthermore, we define the SQL statement that gets the users from the database. The ability to configure the SQL itself makes the storage of users in the database very flexible.

According to the SQL above, a users’ record is stored in the users table. The email column is what represents the users’ principal (username). Further down the configuration, we set the name of the password field, cas.authn.jdbc.query[0].fieldPassword. We set it to the value password to increase the flexibility further.

Other attributes that we configured are the database user (root) and password (blank), dialect and the JDBC connection String. The list of supported databases, available drivers and dialects can be found here.

Another essential attribute is the encryption type used for storing the password. In this case, it is set to NONE.

However, the server supports more encryption mechanisms, such as Bcrypt. These encryption mechanisms that can be found here, together with other configurable properties.

Running the server (build run) now enables the authentication of users with credentials that are present in the configured database. Note again that the principal in the database that the server uses must be the same as that of the client applications.

In this case, the Spring Boot app should have the same value ([email protected]) for the principal (username) as that of the database connected to the server.

Let’s then modify the UserDetails connected to the CasAuthenticationProvider bean configured in the CasSecuredAppApplication class of the Spring Boot application:

@Bean
public CasAuthenticationProvider casAuthenticationProvider() {
    CasAuthenticationProvider provider = new CasAuthenticationProvider();
    provider.setServiceProperties(serviceProperties());
    provider.setTicketValidator(ticketValidator());
    provider.setUserDetailsService((s) -> new User(
      "[email protected]", "testU",
      true, true, true, true,
    AuthorityUtils.createAuthorityList("ROLE_ADMIN")));
    provider.setKey("CAS_PROVIDER_LOCALHOST_9000");
    return provider;
}

Another thing to take note of is that although the UserDetails is given a password, it’s not used. However, if the username differs from that of the server, authentication will fail.

For the application to authenticate successfully with the credentials stored in the database, start a MySQL server running on 127.0.0.1 and port 3306 with username root and password root.

Then use the SQL file, cas-server\src\main\resources\create_test_db_and_users_tbl.sql, which is part of the source code, to create the table users in database test.

By default, it contains the email [email protected] and password Mellon. Remember, we can always modify the database connection settings in application.properties.

Start the CAS Server once again with build run, go to https://localhost:6443/cas and use those credentials for authentication. The same credentials will also work for the cas-secured Spring Boot App.

6. Conclusion

We’ve looked extensively at how to use CAS Server SSO with Spring Security and many of the configuration files involved.

There are many other aspects of a server that can be configured ranging from themes and protocol types to authentication policies. These can all be found here in the docs.

The source code for the server in this article and it’s configuration files can be found here, and that of the Spring Boot application can be found here.

Leave a Reply

Your email address will not be published.