Introduction
CAS (Central Authentication Service) is an enterprise-grade single sign-on (SSO) solution that provides centralized authentication for multiple web applications. A Java-based CAS server enables organizations to implement secure, scalable SSO across their application ecosystem. This guide explores how to build, configure, and deploy a CAS server using Spring Boot and the Apereo CAS framework.
Article: Implementing a Production-Ready CAS Server in Java
CAS server acts as a central authentication authority that issues tickets to authenticated users, allowing them to access multiple applications without re-entering credentials. The Java ecosystem provides robust tools for building enterprise-ready CAS servers.
1. CAS Architecture Overview
Key Components:
- CAS Server - Central authentication service
- CAS Clients - Applications protected by CAS
- Ticket Registry - Storage for tickets (TGT, ST)
- Service Registry - Registered applications and policies
- Authentication Handlers - Credential validation mechanisms
Authentication Flow:
1. User accesses protected service 2. Redirect to CAS login → 3. User provides credentials 4. CAS validates credentials → 5. Issue Ticket Granting Ticket (TGT) 6. Redirect back with Service Ticket (ST) → 7. Service validates ST 8. Grant access to service
2. Maven Dependencies and Project Setup
CAS Overlay Project Structure:
cas-overlay/ ├── pom.xml ├── src/ │ └── main/ │ ├── java/ │ │ └── com/myapp/cas/ │ └── resources/ │ ├── application.yml │ └── services/ └── etc/ └── cas/
pom.xml for CAS Overlay:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>com.myapp</groupId>
<artifactId>cas-overlay</artifactId>
<version>1.0.0</version>
<packaging>war</packaging>
<properties>
<cas.version>6.6.14</cas.version>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.3.2</version>
<configuration>
<warName>cas</warName>
<overlays>
<overlay>
<groupId>org.apereo.cas</groupId>
<artifactId>cas-server-webapp</artifactId>
</overlay>
</overlays>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<!-- CAS Server Web Application -->
<dependency>
<groupId>org.apereo.cas</groupId>
<artifactId>cas-server-webapp</artifactId>
<version>${cas.version}</version>
<type>war</type>
<scope>runtime</scope>
</dependency>
<!-- CAS Configuration -->
<dependency>
<groupId>org.apereo.cas</groupId>
<artifactId>cas-server-core-configuration</artifactId>
<version>${cas.version}</version>
</dependency>
<!-- LDAP Authentication -->
<dependency>
<groupId>org.apereo.cas</groupId>
<artifactId>cas-server-support-ldap</artifactId>
<version>${cas.version}</version>
</dependency>
<!-- JDBC Authentication -->
<dependency>
<groupId>org.apereo.cas</groupId>
<artifactId>cas-server-support-jdbc</artifactId>
<version>${cas.version}</version>
</dependency>
<!-- REST API Support -->
<dependency>
<groupId>org.apereo.cas</groupId>
<artifactId>cas-server-support-rest</artifactId>
<version>${cas.version}</version>
</dependency>
<!-- SAML Support -->
<dependency>
<groupId>org.apereo.cas</groupId>
<artifactId>cas-server-support-saml</artifactId>
<version>${cas.version}</version>
</dependency>
<!-- OAuth2 Support -->
<dependency>
<groupId>org.apereo.cas</groupId>
<artifactId>cas-server-support-oauth</artifactId>
<version>${cas.version}</version>
</dependency>
<!-- Monitoring and Management -->
<dependency>
<groupId>org.apereo.cas</groupId>
<artifactId>cas-server-support-monitor</artifactId>
<version>${cas.version}</version>
</dependency>
</dependencies>
</project>
3. CAS Server Configuration
application.yml:
# CAS Server Configuration
server:
port: 8443
ssl:
enabled: true
key-store: file:/etc/cas/jetty/thekeystore
key-store-password: changeit
key-password: changeit
cas:
server:
name: https://cas.mycompany.com:8443
prefix: ${cas.server.name}/cas
# Service Registry Configuration
service-registry:
core:
init-from-json: true
json:
location: file:/etc/cas/services
# Authentication Configuration
authn:
# LDAP Authentication
ldap:
- type: AUTHENTICATED
ldap-url: ldap://ldap.mycompany.com:389
base-dn: dc=mycompany,dc=com
user-filter: cn={user}
bind-dn: cn=admin,dc=mycompany,dc=com
bind-credential: password
search-filter: (cn={user})
# JDBC Authentication
jdbc:
query:
- url: jdbc:postgresql://localhost:5432/cas
user: cas_user
password: cas_password
driver-class: org.postgresql.Driver
field-password: password
table-users: users
field-expired: expired
field-disabled: disabled
sql: SELECT * FROM users WHERE username = ?
# Accept Users (for testing)
accept:
users: "user1::password1,user2::password2"
# REST Authentication
rest:
uri: http://localhost:8080/api/authenticate
# Ticket Registry
ticket:
registry:
jpa:
dialect: org.hibernate.dialect.PostgreSQLDialect
ddl-auto: update
url: jdbc:postgresql://localhost:5432/cas
user: cas_user
password: cas_password
driver-class: org.postgresql.Driver
# Logout Configuration
logout:
follow-service-redirects: true
# Monitoring
monitor:
endpoints:
enabled: true
sensitive: false
web:
exposure:
include: health,info,metrics,loggers
# Theme Configuration
theme:
default-theme-name: mytheme
# Custom Properties
custom:
app-name: "MyCompany CAS"
support-email: "[email protected]"
4. Custom CAS Configuration Classes
CAS Configuration Properties:
package com.myapp.cas.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "cas.custom")
public class CasCustomProperties {
private String appName;
private String supportEmail;
private int sessionTimeout = 3600;
private boolean multiFactorEnabled = false;
// Getters and setters
public String getAppName() { return appName; }
public void setAppName(String appName) { this.appName = appName; }
public String getSupportEmail() { return supportEmail; }
public void setSupportEmail(String supportEmail) { this.supportEmail = supportEmail; }
public int getSessionTimeout() { return sessionTimeout; }
public void setSessionTimeout(int sessionTimeout) { this.sessionTimeout = sessionTimeout; }
public boolean isMultiFactorEnabled() { return multiFactorEnabled; }
public void setMultiFactorEnabled(boolean multiFactorEnabled) {
this.multiFactorEnabled = multiFactorEnabled;
}
}
CAS Configuration Class:
package com.myapp.cas.config;
import org.apereo.cas.configuration.CasConfigurationProperties;
import org.apereo.cas.web.flow.configurer.DefaultLoginWebflowConfigurer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.webflow.definition.registry.FlowDefinitionRegistry;
import org.springframework.webflow.engine.builder.support.FlowBuilderServices;
@Configuration
public class CasWebflowConfiguration {
@Autowired
private CasConfigurationProperties casProperties;
@Autowired
private ApplicationContext applicationContext;
@Autowired
private FlowBuilderServices flowBuilderServices;
@Bean
public DefaultLoginWebflowConfigurer customLoginWebflowConfigurer(
FlowDefinitionRegistry loginFlowDefinitionRegistry) {
return new CustomLoginWebflowConfigurer(
flowBuilderServices,
loginFlowDefinitionRegistry,
applicationContext,
casProperties
);
}
}
5. Custom Webflow Configuration
Custom Login Webflow Configurer:
package com.myapp.cas.webflow;
import org.apereo.cas.configuration.CasConfigurationProperties;
import org.apereo.cas.web.flow.configurer.DefaultLoginWebflowConfigurer;
import org.springframework.context.ApplicationContext;
import org.springframework.webflow.definition.registry.FlowDefinitionRegistry;
import org.springframework.webflow.engine.Flow;
import org.springframework.webflow.engine.builder.support.FlowBuilderServices;
public class CustomLoginWebflowConfigurer extends DefaultLoginWebflowConfigurer {
public CustomLoginWebflowConfigurer(FlowBuilderServices flowBuilderServices,
FlowDefinitionRegistry loginFlowDefinitionRegistry,
ApplicationContext applicationContext,
CasConfigurationProperties casProperties) {
super(flowBuilderServices, loginFlowDefinitionRegistry, applicationContext, casProperties);
}
@Override
protected void createRememberMeAuthnWebflowConfig(Flow flow) {
// Customize remember-me functionality
if (casProperties.getTicket().getTgt().getRememberMe().isEnabled()) {
createFlowVariable(flow, "credential", rememberMeUsernamePasswordCredential.class);
final ViewState state = getState(flow, "viewLoginForm", ViewState.class);
createRememberMeAuthnWebflowConfig(state);
}
}
@Override
protected void createInitialAuthenticationActions(Flow flow) {
// Add custom pre-authentication actions
super.createInitialAuthenticationActions(flow);
// Add custom action before authentication
final ActionState actionState = getState(flow, "initializeLoginForm", ActionState.class);
actionState.getEntryActionList().add(createEvaluateAction("customPreAuthenticationAction"));
}
}
6. Custom Authentication Handlers
Custom Authentication Handler:
package com.myapp.cas.auth;
import org.apereo.cas.authentication.AuthenticationHandlerExecutionResult;
import org.apereo.cas.authentication.Credential;
import org.apereo.cas.authentication.PreventedException;
import org.apereo.cas.authentication.handler.support.AbstractPreAndPostProcessingAuthenticationHandler;
import org.apereo.cas.authentication.principal.PrincipalFactory;
import org.apereo.cas.services.ServicesManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.security.auth.login.FailedLoginException;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.HashMap;
public class CustomAuthenticationHandler extends AbstractPreAndPostProcessingAuthenticationHandler {
private static final Logger logger = LoggerFactory.getLogger(CustomAuthenticationHandler.class);
public CustomAuthenticationHandler(String name, ServicesManager servicesManager,
PrincipalFactory principalFactory, Integer order) {
super(name, servicesManager, principalFactory, order);
}
@Override
protected AuthenticationHandlerExecutionResult doAuthentication(Credential credential)
throws GeneralSecurityException, PreventedException {
try {
// Custom authentication logic
UsernamePasswordCredential upc = (UsernamePasswordCredential) credential;
String username = upc.getUsername();
String password = upc.getPassword();
logger.info("Attempting authentication for user: {}", username);
// Validate credentials against your custom source
if (validateCredentials(username, password)) {
// Create principal with attributes
Map<String, Object> attributes = new HashMap<>();
attributes.put("email", username + "@mycompany.com");
attributes.put("displayName", getDisplayName(username));
attributes.put("roles", getUserRoles(username));
final Principal principal = this.principalFactory.createPrincipal(username, attributes);
return createHandlerResult(upc, principal, new ArrayList<>());
}
throw new FailedLoginException("Invalid credentials for user: " + username);
} catch (Exception e) {
logger.error("Authentication error", e);
throw new FailedLoginException("Authentication failed: " + e.getMessage());
}
}
@Override
public boolean supports(Class<? extends Credential> clazz) {
return UsernamePasswordCredential.class.isAssignableFrom(clazz);
}
@Override
public boolean supports(Credential credential) {
return credential instanceof UsernamePasswordCredential;
}
private boolean validateCredentials(String username, String password) {
// Implement your custom validation logic
// This could be against a database, external API, etc.
return "validPassword".equals(password); // Replace with actual validation
}
private String getDisplayName(String username) {
// Retrieve display name from your user store
return username.toUpperCase();
}
private List<String> getUserRoles(String username) {
// Retrieve roles from your user store
return List.of("ROLE_USER", "ROLE_APP_USER");
}
}
Authentication Handler Configuration:
package com.myapp.cas.config;
import com.myapp.cas.auth.CustomAuthenticationHandler;
import org.apereo.cas.authentication.AuthenticationEventExecutionPlan;
import org.apereo.cas.authentication.AuthenticationEventExecutionPlanConfigurer;
import org.apereo.cas.authentication.AuthenticationHandler;
import org.apereo.cas.authentication.principal.PrincipalFactory;
import org.apereo.cas.configuration.CasConfigurationProperties;
import org.apereo.cas.services.ServicesManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableConfigurationProperties(CasConfigurationProperties.class)
public class CustomAuthenticationConfiguration implements AuthenticationEventExecutionPlanConfigurer {
@Autowired
private CasConfigurationProperties casProperties;
@Autowired
@Qualifier("servicesManager")
private ServicesManager servicesManager;
@Autowired
@Qualifier("principalFactory")
private PrincipalFactory principalFactory;
@Bean
public AuthenticationHandler customAuthenticationHandler() {
return new CustomAuthenticationHandler(
"CustomAuthenticationHandler",
servicesManager,
principalFactory,
1 // Order
);
}
@Override
public void configureAuthenticationExecutionPlan(AuthenticationEventExecutionPlan plan) {
plan.registerAuthenticationHandler(customAuthenticationHandler());
}
}
7. REST API Controllers
CAS Management REST API:
package com.myapp.cas.api;
import org.apereo.cas.services.RegisteredService;
import org.apereo.cas.services.ServicesManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/api/admin")
public class CasManagementController {
private static final Logger logger = LoggerFactory.getLogger(CasManagementController.class);
@Autowired
private ServicesManager servicesManager;
@GetMapping("/services")
public ResponseEntity<Collection<RegisteredService>> getAllServices() {
try {
Collection<RegisteredService> services = servicesManager.getAllServices();
return ResponseEntity.ok(services);
} catch (Exception e) {
logger.error("Failed to retrieve services", e);
return ResponseEntity.internalServerError().build();
}
}
@GetMapping("/services/{id}")
public ResponseEntity<RegisteredService> getService(@PathVariable Long id) {
try {
RegisteredService service = servicesManager.findServiceBy(id);
if (service == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(service);
} catch (Exception e) {
logger.error("Failed to retrieve service: {}", id, e);
return ResponseEntity.internalServerError().build();
}
}
@PostMapping("/services")
public ResponseEntity<Map<String, Object>> createService(@RequestBody RegisteredService service) {
try {
servicesManager.save(service);
servicesManager.load();
Map<String, Object> response = new HashMap<>();
response.put("message", "Service created successfully");
response.put("serviceId", service.getId());
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("Failed to create service", e);
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
}
}
@DeleteMapping("/services/{id}")
public ResponseEntity<Map<String, String>> deleteService(@PathVariable Long id) {
try {
RegisteredService service = servicesManager.findServiceBy(id);
if (service == null) {
return ResponseEntity.notFound().build();
}
servicesManager.delete(service);
servicesManager.load();
return ResponseEntity.ok(Map.of("message", "Service deleted successfully"));
} catch (Exception e) {
logger.error("Failed to delete service: {}", id, e);
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
}
}
@GetMapping("/metrics")
public ResponseEntity<Map<String, Object>> getMetrics() {
try {
Map<String, Object> metrics = new HashMap<>();
metrics.put("totalServices", servicesManager.count());
metrics.put("activeSessions", getActiveSessionsCount());
metrics.put("serverUptime", getServerUptime());
return ResponseEntity.ok(metrics);
} catch (Exception e) {
logger.error("Failed to retrieve metrics", e);
return ResponseEntity.internalServerError().build();
}
}
private int getActiveSessionsCount() {
// Implement active sessions count logic
return 0;
}
private String getServerUptime() {
// Implement server uptime calculation
return "0 days, 0 hours, 0 minutes";
}
}
8. Service Registry Configuration
JSON Service Registry Files:
// /etc/cas/services/MyWebApp-10000001.json
{
"@class": "org.apereo.cas.services.RegexRegisteredService",
"serviceId": "^(https|http)://app1.mycompany.com(:443)?/.*",
"name": "MyWebApp",
"id": 10000001,
"description": "My Company Web Application",
"evaluationOrder": 1,
"logoutType": "BACK_CHANNEL",
"attributeReleasePolicy": {
"@class": "org.apereo.cas.services.ReturnAllowedAttributeReleasePolicy",
"allowedAttributes": ["email", "displayName", "roles"]
},
"accessStrategy": {
"@class": "org.apereo.cas.services.DefaultRegisteredServiceAccessStrategy",
"enabled": true,
"ssoEnabled": true
}
}
// /etc/cas/services/MyAPI-10000002.json
{
"@class": "org.apereo.cas.services.RegexRegisteredService",
"serviceId": "^(https|http)://api.mycompany.com(:443)?/.*",
"name": "MyAPI",
"id": 10000002,
"description": "My Company REST API",
"evaluationOrder": 2,
"logoutType": "BACK_CHANNEL",
"attributeReleasePolicy": {
"@class": "org.apereo.cas.services.ReturnAllowedAttributeReleasePolicy",
"allowedAttributes": ["email", "roles"]
},
"accessStrategy": {
"@class": "org.apereo.cas.services.DefaultRegisteredServiceAccessStrategy",
"enabled": true,
"ssoEnabled": true
}
}
9. Custom Thymeleaf Views
Custom Login View (login.html):
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title th:text="#{screen.login.title}">MyCompany CAS - Login</title>
<link rel="stylesheet" th:href="@{/css/cas.css}">
<style>
.custom-login-container {
max-width: 400px;
margin: 50px auto;
padding: 20px;
border: 1px solid #ddd;
border-radius: 5px;
background: #fff;
}
.company-logo {
text-align: center;
margin-bottom: 20px;
}
.alert-custom {
padding: 10px;
margin-bottom: 15px;
border-radius: 3px;
}
.alert-error {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
</style>
</head>
<body>
<div class="custom-login-container">
<div class="company-logo">
<h2>MyCompany CAS</h2>
<p>Central Authentication Service</p>
</div>
<div th:if="${param.error != null}" class="alert-custom alert-error">
<strong>Login Failed!</strong> Invalid username or password.
</div>
<div th:if="${param.logout != null}" class="alert-custom alert-success">
You have been logged out successfully.
</div>
<form th:action="@{/login}" method="post">
<div class="form-group">
<label for="username">Username:</label>
<input type="text" id="username" name="username"
class="form-control" required autofocus>
</div>
<div class="form-group">
<label for="password">Password:</label>
<input type="password" id="password" name="password"
class="form-control" required>
</div>
<div class="form-group form-check">
<input type="checkbox" id="rememberMe" name="rememberMe"
class="form-check-input">
<label for="rememberMe" class="form-check-label">Remember me</label>
</div>
<input type="hidden" th:name="${_csrf.parameterName}"
th:value="${_csrf.token}">
<button type="submit" class="btn btn-primary btn-block">Sign In</button>
</form>
<div class="mt-3 text-center">
<a th:href="@{/forgot-password}">Forgot Password?</a>
</div>
<div class="mt-3 text-center">
<small>© 2024 MyCompany. All rights reserved.</small>
</div>
</div>
</body>
</html>
10. Database Schema for Ticket Registry
PostgreSQL Schema:
-- CAS Ticket Registry Tables CREATE TABLE casserviceticket ( id CHARACTER VARYING(255) NOT NULL, number_of_times_used INTEGER, creation_time TIMESTAMP WITHOUT TIME ZONE, expiration_policy BYTEA, last_time_used TIMESTAMP WITHOUT TIME ZONE, previous_last_time_used TIMESTAMP WITHOUT TIME ZONE, service CHARACTER VARYING(255), ticket_already_used BOOLEAN, type CHARACTER VARYING(255), ticket_granting_ticket_id CHARACTER VARYING(255), PRIMARY KEY (id) ); CREATE TABLE casticketgrantingticket ( id CHARACTER VARYING(255) NOT NULL, expiration_policy BYTEA, creation_time TIMESTAMP WITHOUT TIME ZONE, authentication BYTEA, number_of_times_used INTEGER, PRIMARY KEY (id) ); CREATE TABLE casticketgrantingticket_proxied_by ( ticket_granting_ticket_id CHARACTER VARYING(255) NOT NULL, proxied_by_id CHARACTER VARYING(255) NOT NULL ); CREATE TABLE casticketgrantingticket_services ( ticket_granting_ticket_id CHARACTER VARYING(255) NOT NULL, service CHARACTER VARYING(255) ); -- Indexes for performance CREATE INDEX idx_service_ticket_service ON casserviceticket (service); CREATE INDEX idx_service_ticket_tgt_id ON casserviceticket (ticket_granting_ticket_id); CREATE INDEX idx_tgt_expiration ON casticketgrantingticket (creation_time);
11. Spring Boot Application Class
CAS Application:
package com.myapp.cas;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.ComponentScan;
@SpringBootApplication
@EnableConfigurationProperties
@ComponentScan(basePackages = {"com.myapp.cas", "org.apereo.cas"})
public class CasServerApplication {
public static void main(String[] args) {
SpringApplication.run(CasServerApplication.class, args);
}
}
12. Docker Deployment
Dockerfile:
FROM eclipse-temurin:11-jre # Install dependencies RUN apt-get update && apt-get install -y \ curl \ && rm -rf /var/lib/apt/lists/* # Create CAS user and directories RUN adduser --disabled-password --home /opt/cas --gecos '' cas USER cas WORKDIR /opt/cas # Copy CAS WAR file COPY --chown=cas:cas target/cas.war /opt/cas/ # Create configuration directory RUN mkdir -p /opt/cas/etc/cas /opt/cas/etc/cas/services /opt/cas/etc/cas/config # Copy configuration files COPY --chown=cas:cas src/main/resources/ /opt/cas/etc/cas/config/ COPY --chown=cas:cas etc/cas/ /opt/cas/etc/cas/ # Expose CAS port EXPOSE 8443 # Health check HEALTHCHECK --interval=30s --timeout=3s --start-period=60s \ CMD curl -f https://localhost:8443/cas/actuator/health || exit 1 # Start CAS server CMD ["java", "-jar", "cas.war", \ "--server.ssl.key-store=/opt/cas/etc/cas/thekeystore", \ "--server.ssl.key-store-password=changeit", \ "--cas.service-registry.json.location=file:/opt/cas/etc/cas/services", \ "--spring.config.location=file:/opt/cas/etc/cas/config/application.yml"]
docker-compose.yml:
version: '3.8' services: cas-server: build: . ports: - "8443:8443" environment: - SPRING_PROFILES_ACTIVE=prod - CAS_SERVER_NAME=https://cas.mycompany.com:8443 volumes: - ./logs:/opt/cas/logs - ./keystore:/opt/cas/etc/cas networks: - cas-network postgres: image: postgres:13 environment: - POSTGRES_DB=cas - POSTGRES_USER=cas_user - POSTGRES_PASSWORD=cas_password volumes: - postgres-data:/var/lib/postgresql/data networks: - cas-network ldap: image: osixia/openldap:1.5.0 environment: - LDAP_DOMAIN=mycompany.com - LDAP_ADMIN_PASSWORD=admin volumes: - ldap-data:/var/lib/ldap networks: - cas-network volumes: postgres-data: ldap-data: networks: cas-network: driver: bridge
13. Monitoring and Management
CAS Actuator Endpoints:
package com.myapp.cas.monitoring;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;
@Component
public class CasHealthIndicator implements HealthIndicator {
private final ServicesManager servicesManager;
public CasHealthIndicator(ServicesManager servicesManager) {
this.servicesManager = servicesManager;
}
@Override
public Health health() {
try {
int serviceCount = servicesManager.count();
boolean healthy = serviceCount >= 0; // Basic health check
if (healthy) {
return Health.up()
.withDetail("servicesRegistered", serviceCount)
.withDetail("status", "CAS Server is healthy")
.build();
} else {
return Health.down()
.withDetail("error", "No services registered")
.build();
}
} catch (Exception e) {
return Health.down()
.withDetail("error", e.getMessage())
.build();
}
}
}
14. Security Configuration
Security Configuration:
package com.myapp.cas.security;
import org.apereo.cas.configuration.CasConfigurationProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private CasConfigurationProperties casProperties;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.ignoringRequestMatchers(
"/logout", "/validate", "/serviceValidate", "/proxyValidate", "/samlValidate"
))
.authorizeHttpRequests(authz -> authz
.requestMatchers("/", "/login", "/logout", "/error").permitAll()
.requestMatchers("/actuator/health", "/actuator/info").permitAll()
.requestMatchers("/actuator/**").hasRole("ADMIN")
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.sessionManagement(session -> session
.sessionFixation().migrateSession()
);
return http.build();
}
}
Benefits of Java CAS Server
- Enterprise-Grade SSO - Production-ready single sign-on
- Protocol Support - CAS, SAML, OAuth2, OpenID Connect
- Extensible Architecture - Custom authentication handlers and flows
- High Availability - Clustered ticket registry support
- Security - Built-in protection against common attacks
- Monitoring - Comprehensive metrics and health checks
Conclusion
Building a CAS server in Java using the Apereo CAS framework provides a robust, scalable solution for enterprise single sign-on. The framework's extensible architecture allows for custom authentication mechanisms, service registration, and integration with various identity providers.
The key to successful CAS implementation is:
- Proper service registry management for application registration
- Secure ticket storage with appropriate expiration policies
- Comprehensive monitoring and health checks
- Regular security updates and maintenance
- Thorough testing of authentication flows
Start with a basic CAS setup and gradually add more advanced features like multi-factor authentication, delegated authentication, and protocol bridging as your requirements evolve.
Call to Action: Begin by setting up a development CAS server with file-based service registry and test authentication flows. Once validated, move to production with database-backed ticket registry and proper SSL configuration. Monitor the CAS server performance and security regularly to ensure reliable SSO services.