CAS SSO con Spring Security

1. Panoramica

In questo tutorial, esamineremo il servizio CAS (Central Authentication Service) di Apereo e vedremo come un servizio Spring Boot può utilizzarlo per l'autenticazione. CAS è una soluzione Single Sign-On (SSO) aziendale che è anche open source.

Cos'è SSO? Quando accedi a YouTube, Gmail e Maps con le stesse credenziali, questo è Single Sign-On. Lo dimostreremo configurando un server CAS e un'app Spring Boot. L'app Spring Boot utilizzerà CAS per l'autenticazione.

2. Configurazione server CAS

2.1. Installazione e dipendenze CAS

Il server utilizza lo stile Overlay di guerra Maven (Gradle) per semplificare l'installazione e la distribuzione:

git clone //github.com/apereo/cas-overlay-template.git cas-server

Questo comando clonerà il cas-overlay-template nella directory cas-server .

Alcuni degli aspetti che tratteremo includono la registrazione del servizio JSON e la connessione al database JDBC. Quindi, aggiungeremo i loro moduli alla sezione delle dipendenze del file build.gradle :

compile "org.apereo.cas:cas-server-support-json-service-registry:${casServerVersion}" compile "org.apereo.cas:cas-server-support-jdbc:${casServerVersion}"

Assicuriamoci di controllare l'ultima versione di casServer.

2.2. Configurazione server CAS

Prima di poter avviare il server CAS, è necessario aggiungere alcune configurazioni di base. Iniziamo creando una cartella cas-server / src / main / resources e in questa cartella. Seguirà anche la creazione di application.properties nella cartella:

server.port=8443 spring.main.allow-bean-definition-overriding=true server.ssl.key-store=classpath:/etc/cas/thekeystore server.ssl.key-store-password=changeit

Procediamo con la creazione del file key-store referenziato nella configurazione sopra. Per prima cosa, dobbiamo creare le cartelle / etc / cas e / etc / cas / config in cas-server / src / main / resources .

Quindi, dobbiamo cambiare la directory in cas-server / src / main / resources / etc / cas ed eseguire il comando per generare il keystore :

keytool -genkey -keyalg RSA -alias thekeystore -keystore thekeystore -storepass changeit -validity 360 -keysize 2048

Per non avere un errore di handshake SSL, dovremmo usare localhost come valore di nome e cognome. Dovremmo usare lo stesso anche per il nome e l'unità dell'organizzazione. Inoltre, dobbiamo importare il thekeystore nel JDK / JRE che utilizzeremo per eseguire la nostra app client:

keytool -importkeystore -srckeystore thekeystore -destkeystore $JAVA11_HOME/jre/lib/security/cacerts

La password per il keystore di origine e di destinazione è changeit . Su sistemi Unix, potremmo dover eseguire questo comando con privilegi di amministratore ( sudo ). Dopo l'importazione, dovremmo riavviare tutte le istanze di Java in esecuzione o riavviare il sistema.

Stiamo usando JDK11 perché è richiesto dalla versione CAS 6.1.x. Inoltre, abbiamo definito la variabile d'ambiente $ JAVA11_HOME che punta alla sua directory home. Ora possiamo avviare il server CAS:

./gradlew run -Dorg.gradle.java.home=$JAVA11_HOME

Quando l'applicazione si avvia, vedremo "READY" stampato sul terminale e il server sarà disponibile su // localhost: 8443 .

2.3. Configurazione utente server CAS

Non possiamo ancora accedere perché non abbiamo configurato alcun utente. CAS ha diversi metodi di gestione della configurazione, inclusa la modalità standalone. Creiamo una cartella di configurazione cas-server / src / main / resources / etc / cas / config in cui creeremo un file delle proprietà cas.properties . Ora possiamo definire un utente statico nel file delle proprietà:

cas.authn.accept.users=casuser::Mellon

Dobbiamo comunicare la posizione della cartella di configurazione al server CAS affinché le impostazioni abbiano effetto. Aggiorniamo tasks.gradle in modo da poter passare la posizione come argomento JVM dalla riga di comando:

task run(group: "build", description: "Run the CAS web application in embedded container mode") { dependsOn 'build' doLast { def casRunArgs = new ArrayList(Arrays.asList( "-server -noverify -Xmx2048M -XX:+TieredCompilation -XX:TieredStopAtLevel=1".split(" "))) if (project.hasProperty('args')) { casRunArgs.addAll(project.args.split('\\s+')) } javaexec { main = "-jar" jvmArgs = casRunArgs args = ["build/libs/${casWebApplicationBinaryName}"] logger.info "Started ${commandLine}" } } }

Quindi salviamo il file ed eseguiamo:

./gradlew run -Dorg.gradle.java.home=$JAVA11_HOME -Pargs="-Dcas.standalone.configurationDirectory=/cas-server/src/main/resources/etc/cas/config"

Tieni presente che il valore di cas.standalone.configurationDirectory è un percorso assoluto . Ora possiamo andare su // localhost: 8443 e accedere con il nome utente casuser e la password Mellon .

3. Configurazione client CAS

Useremo Spring Initializr per generare un'app client Spring Boot. Avrà Web , sicurezza , FreeMarker e DevTools dipendenze. Inoltre, aggiungeremo anche la dipendenza per il modulo Spring Security CAS al suo pom.xml :

 org.springframework.security spring-security-cas 5.3.0.RELEASE 

Infine, aggiungiamo le seguenti proprietà Spring Boot per configurare l'app:

server.port=8900 spring.freemarker.suffix=.ftl

4. Registrazione del servizio server CAS

Le applicazioni client devono registrarsi con il server CAS prima di qualsiasi autenticazione . Il server CAS supporta l'uso dei registri client YAML, JSON, MongoDB e LDAP.

In questo tutorial, useremo il metodo JSON Service Registry. Creiamo ancora un'altra cartella cas-server / src / main / resources / etc / cas / services . È questa cartella che ospiterà i file JSON del registro di servizio.

Creeremo un file JSON che contiene la definizione della nostra applicazione client. Il nome del file, casSecuredApp-8900.json, segue il modello s erviceName-Id.json :

{ "@class" : "org.apereo.cas.services.RegexRegisteredService", "serviceId" : "//localhost:8900/login/cas", "name" : "casSecuredApp", "id" : 8900, "logoutType" : "BACK_CHANNEL", "logoutUrl" : "//localhost:8900/exit/cas" }

L' attributo serviceId definisce un pattern URL regex per l'applicazione client. Il pattern deve corrispondere all'URL dell'applicazione client.

L' attributo id dovrebbe essere univoco. In altre parole, non dovrebbero esserci due o più servizi con lo stesso ID registrati nello stesso server CAS. Avere un ID duplicato porterà a conflitti e alla sovrascrittura delle configurazioni.

Configuriamo anche il tipo di logout come BACK_CHANNEL e l'URL come // localhost: 8900 / exit / cas in modo da poter eseguire il logout singolo in seguito. Prima che il server CAS possa utilizzare il nostro file di configurazione JSON, dobbiamo abilitare il registro JSON nel nostro cas.properties :
cas.serviceRegistry.initFromJson=true cas.serviceRegistry.json.location=classpath:/etc/cas/services

5. Configurazione Single Sign-On del client CAS

Il prossimo passo per noi è configurare Spring Security per lavorare con il server CAS. Dovremmo anche controllare l'intero flusso di interazioni, chiamato sequenza CAS.

Let's add the following bean configurations to the CasSecuredApplication class of our Spring Boot app:

@Bean public CasAuthenticationFilter casAuthenticationFilter( AuthenticationManager authenticationManager, ServiceProperties serviceProperties) throws Exception { CasAuthenticationFilter filter = new CasAuthenticationFilter(); filter.setAuthenticationManager(authenticationManager); filter.setServiceProperties(serviceProperties); return filter; } @Bean public ServiceProperties serviceProperties() { logger.info("service properties"); ServiceProperties serviceProperties = new ServiceProperties(); serviceProperties.setService("//cas-client:8900/login/cas"); serviceProperties.setSendRenew(false); return serviceProperties; } @Bean public TicketValidator ticketValidator() { return new Cas30ServiceTicketValidator("//localhost:8443"); } @Bean public CasAuthenticationProvider casAuthenticationProvider( TicketValidator ticketValidator, ServiceProperties serviceProperties) { CasAuthenticationProvider provider = new CasAuthenticationProvider(); provider.setServiceProperties(serviceProperties); provider.setTicketValidator(ticketValidator); provider.setUserDetailsService( s -> new User("[email protected]", "Mellon", true, true, true, true, AuthorityUtils.createAuthorityList("ROLE_ADMIN"))); provider.setKey("CAS_PROVIDER_LOCALHOST_8900"); return provider; }

The ServiceProperties bean has the same URL as the serviceId in casSecuredApp-8900.json. This is important because it identifies this client to the CAS server.

The sendRenew property of ServiceProperties is set to false. This means a user only needs to present login credentials to the server once.

The AuthenticationEntryPoint bean will handle authentication exceptions. Thus, it'll redirect the user to the login URL of the CAS server for authentication.

In summary, the authentication flow goes:

  1. A user attempts to access a secure page, which triggers an authentication exception
  2. The exception triggers AuthenticationEntryPoint. In response, the AuthenticationEntryPoint will take the user to the CAS server login page – //localhost:8443/login
  3. On successful authentication, the server redirects back to the client with a ticket
  4. CasAuthenticationFilter will pick up the redirect and call CasAuthenticationProvider
  5. CasAuthenticationProvider will use TicketValidator to confirm the presented ticket on CAS server
  6. If the ticket is valid, the user will get a redirection to the requested secure URL

Finally, let's configure HttpSecurity to secure some routes in WebSecurityConfig. In the process, we'll also add the authentication entry point for exception handling:

@Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests().antMatchers( "/secured", "/login") .authenticated() .and().exceptionHandling() .authenticationEntryPoint(authenticationEntryPoint()); }

6. CAS Client Single Logout Configuration

So far, we've dealt with single sign-on; let's now consider CAS single logout (SLO).

Applications that use CAS for managing user authentication can log out a user from two places:

  • The client application can logout a user from itself locally – this will not affect the user's login status in other applications using the same CAS server
  • The client application can also log out the user from the CAS server – this will cause the user to be logged out from all other client apps connected to the same CAS server.

We'll first put in place logout on the client application and then extend it to single logout on the CAS server.

In order to make obvious what goes on behind the scene, we'll create a logout() method to handle the local logout. On success, it'll redirect us to a page with a link for single logout:

@GetMapping("/logout") public String logout( HttpServletRequest request, HttpServletResponse response, SecurityContextLogoutHandler logoutHandler) { Authentication auth = SecurityContextHolder .getContext().getAuthentication(); logoutHandler.logout(request, response, auth ); new CookieClearingLogoutHandler( AbstractRememberMeServices.SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY) .logout(request, response, auth); return "auth/logout"; }

In the single logout process, the CAS server will first expire the user's ticket and then send an async request to all registered client apps. Each client app that receives this signal will perform a local logout. Thereby accomplishing the goal of logout once, it will cause a log out everywhere.

Having said that, let's add some bean configurations to our client app. Specifically, in the CasSecuredApplicaiton:

@Bean public SecurityContextLogoutHandler securityContextLogoutHandler() { return new SecurityContextLogoutHandler(); } @Bean public LogoutFilter logoutFilter() { LogoutFilter logoutFilter = new LogoutFilter("//localhost:8443/logout", securityContextLogoutHandler()); logoutFilter.setFilterProcessesUrl("/logout/cas"); return logoutFilter; } @Bean public SingleSignOutFilter singleSignOutFilter() { SingleSignOutFilter singleSignOutFilter = new SingleSignOutFilter(); singleSignOutFilter.setCasServerUrlPrefix("//localhost:8443"); singleSignOutFilter.setLogoutCallbackPath("/exit/cas"); singleSignOutFilter.setIgnoreInitConfiguration(true); return singleSignOutFilter; }

The logoutFilter will intercept requests to /logout/cas and redirect the application to the CAS server. The SingleSignOutFilter will intercept requests coming from the CAS server and perform the local logout.

7. Connecting the CAS Server to a Database

We can configure the CAS server to read credentials from a MySQL database. We'll use the test database of a MySQL server that's running in a local machine. Let's update cas-server/src/main/resources/etc/cas/config/cas.properties:

cas.authn.accept.users= cas.authn.jdbc.query[0].sql=SELECT * FROM users WHERE email = ? cas.authn.jdbc.query[0].url= jdbc:mysql://127.0.0.1:3306/test? useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC cas.authn.jdbc.query[0].dialect=org.hibernate.dialect.MySQLDialect cas.authn.jdbc.query[0].user=root cas.authn.jdbc.query[0].password=root cas.authn.jdbc.query[0].ddlAuto=none cas.authn.jdbc.query[0].driverClass=com.mysql.cj.jdbc.Driver cas.authn.jdbc.query[0].fieldPassword=password cas.authn.jdbc.query[0].passwordEncoder.type=NONE

We set the cas.authn.accept.users to blank. This will deactivate the use of static user repositories by the CAS server.

According to the SQL above, users' credentials are stored in the users table. The email column is what represents the users' principal (username).

Please make sure to check the list of supported databases, available drivers and dialects. We also set the password encoder type to NONE. Other encryption mechanisms and their peculiar properties are also available.

Note that the principal in the database of the CAS server must be the same as that of the client application.

Let's update CasAuthenticationProvider to have the same username as the CAS server:

@Bean public CasAuthenticationProvider casAuthenticationProvider() { CasAuthenticationProvider provider = new CasAuthenticationProvider(); provider.setServiceProperties(serviceProperties()); provider.setTicketValidator(ticketValidator()); provider.setUserDetailsService( s -> new User("[email protected]", "Mellon", true, true, true, true, AuthorityUtils.createAuthorityList("ROLE_ADMIN"))); provider.setKey("CAS_PROVIDER_LOCALHOST_8900"); return provider; }

CasAuthenticationProvider non utilizza la password per l'autenticazione. Tuttavia, il suo nome utente deve corrispondere a quello del server CAS affinché l'autenticazione abbia successo. Il server CAS richiede che un server MySQL sia in esecuzione su localhost alla porta 3306 . Il nome utente e la password dovrebbero essere root .

Riavviare nuovamente il server CAS e l'app Spring Boot. Quindi utilizzare le nuove credenziali per l'autenticazione.

8. Conclusione

Abbiamo esaminato come utilizzare CAS SSO con Spring Security e molti dei file di configurazione coinvolti. Ci sono molti altri aspetti di CAS SSO configurabili. Che vanno da temi e tipi di protocollo a criteri di autenticazione.

Questi e altri sono nei documenti. Il codice sorgente per il server CAS e l'app Spring Boot è disponibile su GitHub.