Aprile 24, 2024

CAS 5.2 SSO e Spring Security

Ecco un pratico esempio di come integrare Apereo CAS con una o più applicazioni web scritte in Java che utilizzano Spring Security. Il risultato è un web Single-Sign On centralizzato che vede CAS come unico sistema di controllo degli accessi e che consente perciò agli utenti di poter fruire di più risorse protette, effettuando un’unica autenticazione. In questo esempio, la comunicazione tra il CAS server e le applicazioni web protette, anche chiamate CAS client o CAS service, è ticket-based ed è implementata su protocollo CAS 3.0 (altri protocolli supportati da CAS sono SAML, OpenID, OAuth). L’immagine seguente mostra una visione ad alto livello dell’architettura del sistema.

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: Il codice sorgente trattato in questo articolo si trova nella directory mvc-security-cas

CAS Server Setup

Scaricare il sorgente di CAS server dal repository git e abilitare un handler di autenticazione. In questo esempio ho integrato CAS a un server Active Directory con degli utenti di test, per cui è sufficiente aggiungere la dipendenza al supporto LDAP nel pom.xml del sorgente.

$ 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>

Per definire le webapp che sono protette da CAS utilizzerò un file JSON ma è necessario anche in questo caso aggiungere nel pom.xml la dipendenza a json-service-registry per la definizione dei servizi in formato json

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

Il nome del file json che definisce i servizi deve essere creato secondo la convenzione segueente:

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

Assicurarsi che CAS sia in esecuzione su HTTPS altrimenti la funzionalità di SSO non funzionerà. La pagina di login di CAS mostra un warning di questo tipo:

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

La webapp di questo esempio utilizza Spring MVC e Spring Security che fornisce nativamente supporto all’integrazione con CAS. Basta aggiungere la dipendenza dal modulo nel pom.xml.

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

Per inizializzare l’application context, utilizzo un’implementazione di Spring WebApplicationInitializer. Questo mi consente di istanziare programmaticamente filtri, servlet, e listener direttamente in Java senza utilizzare il tradizionale approccio via web.xml.

// 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, "/*");

La definizione di tutti gli oggetti legati a CAS viene fatta via annotation. Lo scopo della classe CasConfigurer è proprio quello di centralizzare tutte le configurazioni di CAS in un’unico oggetto.

/**
 * 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 estende Spring WebSecurityConfigurerAdapter e inizializza Spring 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;
}

Il controller gestisce le url per le richieste verso la pagina di login dell’applicazione “/sso-login” e il redirect alla 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.