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
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.
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; }