Of course! Let's dive deep into the Java CAS (Central Authentication Service) client. This is a fundamental topic for any Java application that needs to integrate with a university, enterprise, or any other organization using a CAS server for Single Sign-On (SSO).
Here's a comprehensive guide covering:
- What is CAS and why use it?
- Core Concepts (Protocol Flow)
- Java CAS Client Library (The Recommended Approach)
- A Practical Walkthrough: Configuring a Spring Boot App
- Manual CAS Client Implementation (For Understanding)
- Key Configuration Parameters
- Best Practices & Troubleshooting
What is CAS and Why Use It?
CAS (Central Authentication Service) is a single sign-on protocol for the web. It allows a user to log in once and gain access to multiple, related, yet independent software systems.
- For Users: They only have to remember one set of credentials. No more juggling multiple usernames and passwords.
- For Developers: You offload the complex and security-critical task of authentication to a dedicated, robust system (the CAS server). Your application just needs to verify the user's identity with the CAS server.
Think of it like a master key for a building. You authenticate once at the main security desk (CAS Server) and are then given a keycard (a Ticket) that grants you access to various authorized rooms (your web applications).
Core Concepts (The CAS Protocol Flow)
Understanding the flow is crucial. The CAS client's job is to implement parts of this flow.
Service Initiation (Login Page)
- A user tries to access a protected resource in your Java application (the "Service").
- Your application (the CAS Client) detects the user is not authenticated.
- It redirects the user to the CAS Server's login URL, passing its own service URL as a parameter.
https://cas-server.example.com/login?service=https://my-java-app.com/home
User Authentication
- The CAS Server presents a login page to the user.
- The user enters their username and password.
- The CAS Server validates the credentials against its user store (e.g., LDAP, database).
Granting a Ticket
- If authentication is successful, the CAS Server generates a Service Ticket (ST).
- It redirects the user back to the original service URL, appending the ticket as a parameter.
https://my-java-app.com/home?ticket=ST-12345-abcdef...
Ticket Validation (The Client's Job)
- Your Java application receives the request with the
ticketparameter. - It takes the ticket and its own service URL and makes a back-channel HTTPS call to the CAS Server's validation endpoint.
https://cas-server.example.com/validate?service=https://my-java-app.com/home&ticket=ST-12345-abcdef...
- The CAS Server checks the ticket. If it's valid, it responds with
yesand the user's principal (username). - If the ticket is invalid or expired, it responds with
no.
Establishing a Session
- Upon receiving a
yesresponse, your application considers the user authenticated. - It typically creates an application-level session (e.g., a Spring Security session) and stores the user's details.
- It can now grant the user access to the protected resource.
- The
ticketparameter is now discarded for that session.
Java CAS Client Library (The Recommended Approach)
You should almost always use a well-established library to handle the CAS protocol. Manually implementing the HTTP calls, parsing responses, and managing security details is error-prone.
The most popular and widely used library is Jasig CAS Client for Java. It's mature, stable, and works with virtually any Java web framework.
Key Features of the CAS Client Library:
- Filters: Provides a
Cas20ProxyReceivingTicketValidationFilterthat automatically handles steps 4 and 5 of the protocol flow for you. - Configuration: Easily configured via
web.xmlor Spring Boot properties. - Integration: Seamlessly integrates with Spring Security, which is the de-facto standard for securing Spring applications.
A Practical Walkthrough: Configuring a Spring Boot App
This is the most common scenario. We'll use Spring Boot and Spring Security with the CAS Client.
Step 1: Add Dependencies
In your pom.xml, add the following dependencies:
<dependencies>
<!-- Spring Web for the web app -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Security for authentication and authorization -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- The official CAS Client for Java -->
<dependency>
<groupId>org.jasig.cas.client</groupId>
<artifactId>cas-client-core</artifactId>
<version>3.6.5</version> <!-- Use a recent stable version -->
</dependency>
</dependencies>
Step 2: Configure CAS in application.properties
Define the connection details to your CAS server.
# CAS Server Configuration
cas.server.url.prefix=https://cas-server.example.com
cas.login.url=${cas.server.url.prefix}/login
# Your Application's Configuration
server.port=8443
server.ssl.enabled=true
server.ssl.key-store=classpath:keystore.p12
server.ssl.key-store-password=changeit
server.ssl.keyStoreType=PKCS12
server.ssl.keyAlias=tomcat
# The URL of your application that will be protected
app.service.url=https://localhost:8443
# CAS Filter Configuration
cas.filter.url.pattern=/*
# CAS Server Validation URL (for CAS 2.0)
cas.validation.url=${cas.server.url.prefix}/p3/serviceValidate
Step 3: Configure Spring Security
This is where you wire up the CAS client filter into the Spring Security filter chain.
import org.jasig.cas.client.util.AssertionThreadLocalFilter;
import org.jasig.cas.client.validation.Cas20ServiceTicketValidator;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.cas.ServiceProperties;
import org.springframework.security.cas.web.CasAuthenticationFilter;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Value("${cas.server.url.prefix}")
private String casServerUrlPrefix;
@Value("${app.service.url}")
private String serviceUrl;
@Value("${cas.validation.url}")
private String casValidationUrl;
@Bean
public ServiceProperties serviceProperties() {
ServiceProperties serviceProperties = new ServiceProperties();
serviceProperties.setService(serviceUrl);
serviceProperties.setSendRenew(false);
return serviceProperties;
}
@Bean
public Cas20ServiceTicketValidator cas20ServiceTicketValidator() {
return new Cas20ServiceTicketValidator(casServerUrlPrefix);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// 1. Define public and protected resources
.authorizeRequests()
.antMatchers("/public/**").permitAll()
.anyRequest().authenticated()
.and()
// 2. Configure the CAS login flow
.addFilter(casAuthenticationFilter())
.exceptionHandling()
.authenticationEntryPoint(casAuthenticationEntryPoint())
.and()
// 3. Configure logout
.logout()
.addLogoutHandler(new SecurityContextLogoutHandler())
.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
.logoutSuccessUrl(casServerUrlPrefix + "/logout?service=" + serviceUrl)
.and()
// 4. Add the CAS Assertion Thread Local Filter (important for accessing user details)
.addFilterBefore(AssertionThreadLocalFilter.class, CasAuthenticationFilter.class);
}
@Bean
public CasAuthenticationFilter casAuthenticationFilter() throws Exception {
CasAuthenticationFilter filter = new CasAuthenticationFilter();
filter.setAuthenticationManager(authenticationManager());
filter.setServiceProperties(serviceProperties());
filter.setFilterProcessesUrl("/login/cas"); // The entry point for CAS login
return filter;
}
@Bean
public CasAuthenticationEntryPoint casAuthenticationEntryPoint() {
CasAuthenticationEntryPoint entryPoint = new CasAuthenticationEntryPoint();
entryPoint.setLoginUrl(casServerUrlPrefix + "/login");
entryPoint.setServiceProperties(serviceProperties());
return entryPoint;
}
}
Step 4: Create a Controller
Create a simple controller to show a protected page and access the authenticated user's details.
import org.jasig.cas.client.validation.Assertion;
import org.springframework.security.cas.authentication.CasAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import javax.servlet.http.HttpServletRequest;
import java.security.Principal;
@Controller
public class HomeController {
@GetMapping("/home")
public String home(Principal principal, HttpServletRequest request) {
// Get the user's name from the Spring Security Principal
System.out.println("User logged in: " + principal.getName());
// Get more details from the CAS Assertion
CasAuthenticationToken token = (CasAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
Assertion assertion = token.getAssertion();
System.out.println("CAS Assertion attributes: " + assertion.getAttributes());
// You can also get it from the request attribute
Assertion assertionFromRequest = (Assertion) request.getAttribute("_cas_assertion_");
System.out.println("CAS Assertion from request: " + assertionFromRequest.getAttributes());
return "home";
}
@GetMapping("/public")
public String publicPage() {
return "public";
}
}
Now, when you access https://localhost:8443/home, you will be automatically redirected to the CAS server to log in. After a successful login, you'll be sent back to your application and will be able to access the /home page.
Manual CAS Client Implementation (For Understanding)
While not recommended for production, understanding the manual calls helps you appreciate what the library does.
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
public class ManualCasClient {
private static final String CAS_SERVER_URL = "https://cas-server.example.com";
private static final String SERVICE_URL = "https://my-java-app.com/validate";
public void handleLoginRequest(HttpServletRequest request, HttpServletResponse response) throws IOException {
// Redirect user to CAS login
String loginUrl = CAS_SERVER_URL + "/login?service=" + SERVICE_URL;
response.sendRedirect(loginUrl);
}
public boolean validateTicket(String ticket) throws IOException {
String validationUrl = CAS_SERVER_URL + "/p3/serviceValidate?service=" + SERVICE_URL + "&ticket=" + ticket;
URL url = new URL(validationUrl);
HttpURLConnection con = (HttpURLConnection) url.openConnection();
con.setRequestMethod("GET");
int status = con.getResponseCode();
if (status == 200) {
BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));
String inputLine;
StringBuilder content = new StringBuilder();
while ((inputLine = in.readLine()) != null) {
content.append(inputLine);
}
in.close();
con.disconnect();
// Parse the XML response to check for <cas:authenticationSuccess>
// This is the tricky part! The library handles XML parsing for you.
return content.toString().contains("<cas:authenticationSuccess>");
} else {
return false;
}
}
}
As you can see, you have to handle redirects, make HTTP calls, and parse XML responses. This is why using a library is so much better.
Key Configuration Parameters
| Parameter | Description | Example |
|---|---|---|
cas.server.url.prefix |
The base URL of your CAS server. | https://sso.mycompany.com |
cas.login.url |
The full URL for the CAS login page. | ${cas.server.url.prefix}/login |
app.service.url |
The fully qualified URL of the protected page in your app. | https://app.mycompany.com/dashboard |
cas.filter.url.pattern |
Ant-style pattern for which URLs the CAS filter should protect. | /secure/** or |
cas.validation.url |
The CAS server's validation endpoint. | ${cas.server.url.prefix}/p3/serviceValidate (CAS 3.0) or /validate (CAS 2.0) |
cas.proxy.callback.url |
URL for handling proxy granting tickets (PGT), if your app needs to act as a proxy for other services. | https://app.mycompany.com/casProxy |
Best Practices & Troubleshooting
- HTTPS is Mandatory: Always use HTTPS for both your application and the CAS server to prevent ticket interception.
- Clock Synchronization: Ensure the clocks on your CAS client server and CAS server are synchronized (e.g., using NTP). Ticket expiration issues are often caused by time drift.
- Ticket Timeouts: Configure appropriate ticket timeouts on your CAS server. Shorter timeouts are more secure.
- Logging: Enable debug logging for the CAS client library to see the exact requests and responses being sent. This is invaluable for troubleshooting.
- In
logback.xml:<logger name="org.jasig.cas.client" level="DEBUG"/>
- In
- Common Error: "Ticket ST-XXX is invalid"
- Cause 1: The ticket has already been used or has expired. This is normal.
- Cause 2: The
serviceURL parameter in the validation request does not exactly match the one used in the initial login request. Check for trailing slashes, capitalization, or port mismatches. - Cause 3: The CAS server and client clocks are out of sync.
- Common Error: "Callback URL not allowed"
- Cause: The
app.service.urlyou are using is not whitelisted on the CAS server. CAS servers are configured to only allow tickets to be validated for specific, trusted service URLs. You must add your application's service URL to the CAS server's service registry.
- Cause: The
