CAS SSO Mit Federsicherheit

1. Übersicht

In diesem Tutorial sehen wir uns den Apereo Central Authentication Service (CAS) an und sehen, wie ein Spring Boot-Dienst ihn zur Authentifizierung verwenden kann. CAS ist eine Enterprise Single Sign-On (SSO) -Lösung, die auch Open Source ist.

Was ist SSO? Wenn Sie sich mit denselben Anmeldeinformationen bei YouTube, Google Mail und Google Maps anmelden, ist dies Single Sign-On. Wir werden dies demonstrieren, indem wir einen CAS-Server und eine Spring Boot-App einrichten. Die Spring Boot-App verwendet CAS zur Authentifizierung.

2. CAS Server Setup

2.1. CAS-Installation und Abhängigkeiten

Der Server verwendet den Maven (Gradle) War Overlay-Stil, um die Einrichtung und Bereitstellung zu vereinfachen:

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

Dieser Befehl klont die cas-overlay-Vorlage in das cas-server- Verzeichnis.

Zu den Aspekten, die wir behandeln werden, gehören die Registrierung von JSON-Diensten und die JDBC-Datenbankverbindung. Also werden wir ihre Module zum Abhängigkeitsabschnitt der build.gradle- Datei hinzufügen :

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

Lassen Sie uns sicherstellen, dass Sie die neueste Version von casServer überprüfen.

2.2. CAS-Serverkonfiguration

Bevor wir den CAS-Server starten können, müssen wir einige grundlegende Konfigurationen hinzufügen. Beginnen wir mit der Erstellung eines Ordners cas-server / src / main / resources und in diesem Ordner. Darauf folgt die Erstellung von application.properties im Ordner:

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

Fahren wir mit der Erstellung der Schlüsselspeicherdatei fort, auf die in der obigen Konfiguration verwiesen wird. Zuerst müssen wir die Ordner / etc / cas und / etc / cas / config in cas-server / src / main / resources erstellen .

Dann müssen wir das Verzeichnis in cas-server / src / main / resources / etc / cas ändern und den Befehl ausführen , um den Keystore zu generieren :

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

Damit wir keinen SSL-Handshake-Fehler haben, sollten wir localhost als Wert für Vor- und Nachnamen verwenden. Wir sollten dasselbe auch für den Namen und die Einheit der Organisation verwenden. Außerdem müssen wir den Thekeystore in das JDK / JRE importieren, mit dem wir unsere Client-App ausführen:

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

Das Kennwort für den Quell- und Zielschlüsselspeicher lautet changeit . Auf Unix-Systemen müssen wir diesen Befehl möglicherweise mit dem Administratorrecht ( sudo ) ausführen . Nach dem Import sollten wir alle laufenden Java-Instanzen neu starten oder das System neu starten.

Wir verwenden JDK11, da es von CAS Version 6.1.x benötigt wird. Außerdem haben wir die Umgebungsvariable $ JAVA11_HOME definiert, die auf das Ausgangsverzeichnis verweist. Wir können jetzt den CAS-Server starten:

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

Wenn die Anwendung gestartet wird, wird auf dem Terminal "BEREIT" angezeigt, und der Server ist unter // localhost: 8443 verfügbar .

2.3. CAS Server Benutzerkonfiguration

Wir können uns noch nicht anmelden, da wir keinen Benutzer konfiguriert haben. CAS verfügt über verschiedene Methoden zum Verwalten der Konfiguration, einschließlich des Standalone-Modus. Erstellen wir einen Konfigurationsordner cas-server / src / main / resources / etc / cas / config, in dem wir eine Eigenschaftendatei cas.properties erstellen . Jetzt können wir einen statischen Benutzer in der Eigenschaftendatei definieren:

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

Wir müssen dem CAS-Server den Speicherort des Konfigurationsordners mitteilen, damit die Einstellungen wirksam werden. Lassen Sie uns task.gradle aktualisieren, damit wir den Speicherort als JVM-Argument über die Befehlszeile übergeben können:

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

Wir speichern dann die Datei und führen aus:

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

Bitte beachten Sie, dass der Wert von cas.standalone.configurationDirectory ein absoluter Pfad ist . Wir können jetzt zu // localhost: 8443 gehen und uns mit dem Benutzernamen casuser und dem Passwort Mellon anmelden .

3. CAS-Client-Setup

Wir werden Spring Initializr verwenden, um eine Spring Boot-Client-App zu generieren. Es wird Web- , Sicherheits- , Freemarker- und DevTools- Abhängigkeiten geben. Außerdem fügen wir der pom.xml die Abhängigkeit für das Spring Security CAS-Modul hinzu :

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

Fügen Sie abschließend die folgenden Spring Boot-Eigenschaften hinzu, um die App zu konfigurieren:

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

4. CAS Server Service-Registrierung

Clientanwendungen müssen sich vor jeder Authentifizierung beim CAS-Server registrieren . Der CAS-Server unterstützt die Verwendung von YAML-, JSON-, MongoDB- und LDAP-Client-Registern.

In diesem Lernprogramm verwenden wir die JSON Service Registry-Methode. Erstellen wir noch einen Ordner cas-server / src / main / resources / etc / cas / services . In diesem Ordner werden die JSON-Dateien der Dienstregistrierung gespeichert.

Wir erstellen eine JSON-Datei, die die Definition unserer Client-Anwendung enthält. Der Name der Datei, casSecuredApp-8900.json, folgt dem Muster 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" }

Das Attribut serviceId definiert ein Regex-URL-Muster für die Clientanwendung. Das Muster sollte mit der URL der Clientanwendung übereinstimmen.

Das ID- Attribut sollte eindeutig sein. Mit anderen Worten, es sollten nicht zwei oder mehr Dienste mit derselben ID auf demselben CAS-Server registriert sein. Eine doppelte ID führt zu Konflikten und zum Überschreiben von Konfigurationen.

Wir konfigurieren den Abmeldetyp auch als BACK_CHANNEL und die URL als // localhost: 8900 / exit / cas, damit wir später eine einzelne Abmeldung durchführen können. Bevor der CAS-Server unsere JSON-Konfigurationsdatei verwenden kann, müssen wir die JSON-Registrierung in unseren cas.properties aktivieren :
cas.serviceRegistry.initFromJson=true cas.serviceRegistry.json.location=classpath:/etc/cas/services

5. CAS Client Single Sign-On-Konfiguration

Der nächste Schritt für uns ist die Konfiguration von Spring Security für die Verwendung mit dem CAS-Server. Wir sollten auch den vollständigen Fluss der Interaktionen überprüfen, der als CAS-Sequenz bezeichnet wird.

Fügen wir der CasSecuredApplication- Klasse unserer Spring Boot-App die folgenden Bean-Konfigurationen hinzu :

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

Die ServiceProperties- Bean hat dieselbe URL wie die serviceId in casSecuredApp-8900.json . Dies ist wichtig, da dieser Client gegenüber dem CAS-Server identifiziert wird.

Die sendRenew- Eigenschaft von ServiceProperties wird auf false gesetzt . Dies bedeutet, dass ein Benutzer dem Server nur einmal Anmeldeinformationen vorlegen muss.

Die AuthenticationEntryPoint- Bean behandelt Authentifizierungsausnahmen. Daher wird der Benutzer zur Authentifizierung zur Anmelde-URL des CAS-Servers umgeleitet.

Zusammenfassend lautet der Authentifizierungsablauf:

  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 verwendet das Kennwort nicht zur Authentifizierung. Der Benutzername muss jedoch mit dem des CAS-Servers übereinstimmen, damit die Authentifizierung erfolgreich ist. Für den CAS-Server muss ein MySQL-Server auf localhost an Port 3306 ausgeführt werden . Der Benutzername und das Passwort sollten root sein .

Starten Sie den CAS-Server und die Spring Boot-App erneut. Verwenden Sie dann die neuen Anmeldeinformationen zur Authentifizierung.

8. Fazit

Wir haben uns angesehen, wie CAS SSO mit Spring Security und vielen der beteiligten Konfigurationsdateien verwendet wird. Es gibt viele andere Aspekte von CAS SSO, die konfigurierbar sind. Angefangen von Themen und Protokolltypen bis hin zu Authentifizierungsrichtlinien.

Diese und andere sind in den Dokumenten enthalten. Der Quellcode für den CAS-Server und die Spring Boot-App ist auf GitHub verfügbar.