November 22, 2024

CAS 5.2 SSO and Spring Security

This article provides an example on Apereo CAS web Single Sign-On (SSO) integrated with a single or multiple web applications based on Spring Security. As centralized access control system, CAS is responsible for authenticating users and granting access to the “CASified” protected webapps (also called CAS clients or CAS services).
The client-server communication covered in this post is ticket-based and takes place by using the CAS 3.0 protocol (other authentication protocols supported by the CAS server are SAML, OpenID, OAuth). Let we take a look at the architecture.

Stack

  • Apereo CAS 5.2.3
  • Spring Security 5.0.3.RELEASE
  • Spring MVC 5.0.3.RELEASE
  • LDAP Active Directory 2012
  • JDK 1.8.0_112
  • Maven 3.5.0

SOURCE CODE (/giuseu/spring-mvc)

GIT
git clone https://gitlab.com/giuseppeurso-eu/spring-mvc

NOTE: Code examples covered in this article, are located under the project mvc-security-cas

CAS Server Setup

The first thing to do is to download the pre-built CAS web application server from the CAS git repository and enable an authentication handler. There are a variety of authentication handlers and schemes supported by CAS. In this example, I enable LDAP support by including the dependency in the pom.xml.

$ git clone https://github.com/apereo/cas-overlay-template.git
<dependency>
	<groupId>org.apereo.cas</groupId>
	<artifactId>cas-server-support-ldap</artifactId>
	<version>${cas.version}</version>
</dependency>

Protected webapps (services) must be registered in the CAS server services registry. The following dependency enables service registration via a JSON configuration.

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

Two JSON file that contains the definition of the client applications. The convention for the file name must be <serviceName>-<serviceNumericId>.json

## ./cas-server/src/main/cas-server-config/client-services/appA-100.json

{
  "@class" : "org.apereo.cas.services.RegexRegisteredService",
  "serviceId" : "http://localhost:9090/appA.*",
  "name" : "appA",
  "id" : 100,
  "evaluationOrder" : 1
}
## ./cas-server/src/main/cas-server-config/client-services/appB-200.json

{
  "@class" : "org.apereo.cas.services.RegexRegisteredService",
  "serviceId" : "http://localhost:9090/appB.*",
  "name" : "appB",
  "id" : 200,
  "evaluationOrder" : 1
}

I have to configure CAS to connect to the LDAP server for authentication handling. Also make sure CAS runs on HTTPS otherwise the web SSO functionality will not work.

Single Sign On NOT WORKS if you access CAS server over non-secure connection HTTP.
In order to have SSO on work, you must log in over HTTPS.
## src/main/cas-server-config/cas.properties

## CAS on HTTPS 
##
server.context-path=/cas
cas.server.name=https://localhost:8443
cas.server.prefix=https://localhost:8443/cas
# These properties have impacts only on the embedded Tomcat server
server.ssl.enabled=true
server.port=8443
server.ssl.key-store=file:/home/user/cas-certs/cas.keystore
server.ssl.key-store-password=store12345
server.ssl.key-password=key12345

## SERVICES REGISTRY
##
cas.serviceRegistry.initFromJson=false
cas.serviceRegistry.json.location=file:/home/user/project/src/main/cas-server-config/client-services

## LDAP AUTHENTICATION
## 
cas.authn.ldap[0].type=AD
cas.authn.ldap[0].ldapUrl=ldap://192.168.56.120:389
cas.authn.ldap[0].useSsl=false
cas.authn.ldap[0].baseDn=OU=test-foo,DC=example,DC=com
cas.authn.ldap[0].bindDn=CN=Administrator,CN=Users,DC=example,DC=com
cas.authn.ldap[0].bindCredential=12345
cas.authn.ldap[0].userFilter=sAMAccountName={user}
cas.authn.ldap[0].dnFormat=%s@example.com
cas.authn.ldap[0].principalAttributeId=sAMAccountName

CAS Client Setup

The code covered in this article uses Spring MVC and Spring Security frameworks for the web application project (CAS client). In order to integrate a webapp with the CAS server, first of all I have to add the dependency for the Spring Security CAS module.

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

To initialize the web application context, I use the annotation-based approach as opposed to the traditional web.xml-based approach.
I configure the ServletContext with any servlets, filters, listeners, context-params and so on, in the WebappInitializer. This class implements Spring WebApplicationInitializer.

// Creating the servlet application context by using annotation based context
AnnotationConfigWebApplicationContext webApplicationContext = new AnnotationConfigWebApplicationContext();
webApplicationContext.register(MvcConfigurer.class);
webApplicationContext.register(CasConfigurer.class);
webApplicationContext.register(SecurityConfigurer.class);
webApplicationContext.setServletContext(servletContext);

// Spring DelegatingFilterProxy which allows you to enable Spring Security and use your custom Filters
FilterRegistration.Dynamic filterRegistration = servletContext.addFilter("giusWebappFilterDelegator", new DelegatingFilterProxy("springSecurityFilterChain"));
filterRegistration.addMappingForUrlPatterns(null, false, "/*");

Annotated definition of Spring beans representing CAS authentication service. Goal of CasConfigurer class is to centralize all the configuration properties of the CAS Server.

/**
 * CAS global properties.
 * @return
 */
@Bean
public ServiceProperties serviceProperties() {
 String appLogin = "http://localhost:18080/mvc-casclient/login-cas";
 ServiceProperties serviceProperties = new ServiceProperties();
 serviceProperties.setService(appLogin);
 serviceProperties.setAuthenticateAllArtifacts(true);
 return serviceProperties;
}

/**
 * The entry point of Spring Security authentication process (based on CAS).
 * The user's browser will be redirected to the CAS login page.
 * @return
 */
@Bean
public AuthenticationEntryPoint casAuthenticationEntryPoint() {
 String casLogin = "https://localhost:8443/cas/login";
 CasAuthenticationEntryPoint entryPoint = new CasAuthenticationEntryPoint();
 entryPoint.setLoginUrl(casLogin);
 entryPoint.setServiceProperties(serviceProperties());
 return entryPoint;
}

/**
 * CAS ticket validator, if you plan to use CAS 3.0 protocol
 * @return
 */
@Bean
public Cas30ServiceTicketValidator ticketValidatorCas30() {
 Cas30ServiceTicketValidator ticketValidator = new Cas30ServiceTicketValidator("http://localhost:8080/cas");
 return ticketValidator;
}

/**
 * The authentication provider that integrates with CAS.
 * This implementation uses CAS 3.0 protocol for ticket validation. 
 * 
 */
@Bean
public CasAuthenticationProvider casAuthenticationProvider() {
 CasAuthenticationProvider provider = new CasAuthenticationProvider();
 provider.setServiceProperties(serviceProperties());
 provider.setTicketValidator(ticketValidatorCas30());
		
 // Loads only a default set of authorities for any authenticated users (username and password are)
 provider.setUserDetailsService((UserDetailsService) fakeUserDetailsService());
	
 provider.setKey("CAS_PROVIDER_KEY_LOCALHOST");
	
 return provider;
}

/**
 * CAS Authentication Provider does not use credentials specified here for authentication. It only loads
 * the authorities for a user, once they have been authenticated by CAS.
 * 
 */
@Bean
public User fakeUserDetailsService(){
 return new User("fakeUser", "fakePass", true, true, true, true, AuthorityUtils.createAuthorityList("ROLE_USER"));
}

SecurityConfigurer class which extends Spring WebSecurityConfigurerAdapter. This class initializes Spring Security and allows customization of the web based security.

// Let Spring resolves and injects collaborating beans into this class by @Autowired annotations...
@Autowired
private AuthenticationProvider casAuthenticationProvider;
@Autowired
private AuthenticationEntryPoint casAuthenticationEntryPoint;
@Autowired
private ServiceProperties casServiceProperties;

/**
 * Configures web based security for specific http requests.
 */
@Override
   protected void configure(HttpSecurity http) throws Exception {
    	http.authorizeRequests()
    		.antMatchers("/api/**").permitAll()
    		.anyRequest().authenticated();
    	
    	http.httpBasic()
    		.authenticationEntryPoint(casAuthenticationEntryPoint);
    	
    	http.addFilter(casAuthenticationFilter());
    }

/**
 * Configures multiple Authentication providers. 
 * AuthenticationManagerBuilder allows for easily building multiple authentication mechanisms in the order they're declared.
 * CasAuthenticationProvider is used here.
 */
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
 auth.authenticationProvider(casAuthenticationProvider);				
}

/**
 * Cas authentication filter responsible processing a CAS service ticket.
 * Here, I was unable to declare this bean in the Cas configurator class( https://tinyurl.com/y9fzgma9 )
 * @return
 * @throws Exception
 */
@Bean
public CasAuthenticationFilter casAuthenticationFilter() throws Exception {
 CasAuthenticationFilter filter = new CasAuthenticationFilter();
 filter.setServiceProperties(casServiceProperties);
 filter.setAuthenticationManager(authenticationManager());
 return filter;
}

Finally the controller that handles requests directed to “/sso-login” and root web context “/”

@GetMapping("/sso-login")
 public String login(HttpServletRequest request) {
	    return "redirect:/";
	}

@RequestMapping(value = {"/"}, method = RequestMethod.GET)
public ModelAndView defaultView (HttpServletRequest request, HttpServletResponse response) {
 String pageName = "index.html";
 ModelAndView view = new ModelAndView(pageName);
 return view;
}

 

Related posts

Leave a Reply

Your email address will not be published.