OpenID Connect mit Spring Boot 3
Authentifizierung mit OpenID Connect geht einfach dank Spring Boot. Wir bauen den Login Deiner Web-Anwendung mit dem Autorisierungsserver Deiner Firma. Wie wir dazu OpenID Connect mit Spring Boot 3 konfigurieren, zeige ich in diesem Blog-Artikel.
OpenID Connect - Authorization Code Prozess
OpenID Connect ist ein Single Sign-On Login-Verfahren. Zur Umsetzung komplexer Anwendungsfälle verwenden größere Firmen meist verteilte Systeme. Damit der Benutzer z. B. beim Online-Shopping den Systemwechsel von Produktseiten zum Einkaufswagen und zur Kasse nicht wahrnimmt, loggt er sich mittels Single Sign-On nur einmal ein. Die beteiligten Systeme authentifizieren den eingeloggten Benutzer anhand seiner Single Sign-On Session.
Detaillierte Informationen über OpenID Connect und Single Sign-On findet ihr hier. In diesem Artikel fokussiere ich mich auf das Anwendungs-System, welches zur Benutzer-Authentifizierung den firmeneigenen OpenID Identity Provider verwendet. Bevor wir uns die Implementierung mit Spring Boot und Spring Security anschauen, betrachten wir im folgenden Schaubild den vereinfachen OpenID Connect Ablauf.
- Der Benutzer ruft eine URL unseres Spring Systems auf, die nur von eingeloggten Benutzern verwendet werden darf.
- Der anonyme Benutzer wird von unserem System zum Single Sign-On an den OpenID Provider unserer Firma weitergeleitet.
- Nach erfolgreichem Login z. B. mittels Benutzername und Passwort leitet der OpenID Provider den Benutzer an unserem System zurück. Als Beweis für den erfolgreichen Login enthält der Weiterleitungs-Request einen Authorization Code.
- Unser System ruft nun den OAuth2 Token Endpunkt des OpenID Providers auf, um den Authorization Code gegen ein OpenID Token einzutauschen. Das OpenID Token ist im JWT Format und enthält Benutzer-Informationen und eine Signatur.
- Nach erfolgreicher Validierung des OpenID Tokens gilt der Benutzer als authentifiziert und ist für die ursprünglich aufgerufene URL berechtigt.
Achtung Spoiler: Zur Umsetzung dieses Prozesses konfigurieren wir dank Spring Boot nur URLs und Credentials.
Englisches Video zum Artikel: Single Sign-On bei der Deutschen Telekom
Gradle Dependencies für OpenID Connect
Die Build-Dependencies managt Spring Boot für uns. Wir fügen nur die richtige Spring Boot Starter Dependency hinzu, um in unserem Projekt OpenID Connect zu nutzen. Mein Demo-Projekt ist eine Thymeleaf Web-Anwendung. Die Authentifizierung mit OpenID Connect benötigt nur die fett markierte Dependency - der Rest ist nur für meine Demo.
dependencies {
implementation 'org.springframework.boot:
spring-boot-starter-oauth2-client'
spring-boot-starter-oauth2-client'
implementation 'org.springframework.boot:
spring-boot-starter-web'
spring-boot-starter-web'
implementation 'org.springframework.boot:
spring-boot-starter-thymeleaf'
spring-boot-starter-thymeleaf'
...
Die benötigte Spring Security Dependency kommt automatisch mit der spring-boot-starter-oauth2-client Dependency.
Single Sign-On beim firmeneigenen OpenID Provider
Den Single Sign-On mit OpenID konfigurieren wir mit Spring Boot:
- in der application.properties oder application.yml Datei in Form von Key-Value-Properties
- und im Java Code durch die Definition einer SecurityFilterChain Bean.
Konfiguration in Spring Properties
Meine Firma, die Deutsche Telekom AG, hat ein eigenes Identity & Access Management System. (Fast) alle Systeme verwenden dieses IAM-System für Login bzw. Single Sign-On. Dazu agiert dieses IAM-System als OpenID Provider, den ich in der application.yml Datei als OpenID Connect Schnittstelle konfiguriere.
spring.security.oauth2.client:
provider:
sam:
issuer-uri: 'https://id.provider.de'
registration:
sam:
authorization-grant-type: authorization_code
client-id: id
client-secret: password
redirect-uri: 'http://localhost:8080/process-login'
scope: openid
- sam ist der Name des Deutsche Telekom IAM-Systems. Ihr könnt einen beliebige andere Namen verwenden. Wenn ihr github, google oder facebook als IAM-System verwendet, braucht ihr nur client-id und client-secret zu setzen, da alles andere für diese wohlbekannten Identity-Provider in Spring Boot vorkonfiguriert ist.
- client-id & client-secret sind die Credentials meines Systems beim OpenID Provider.
- issuer-uri zeigt auf den OpenID Connect Discovery Endpunkt des OpenID Providers. Der Discovery Endpunkt zeigt dann z. B. alle weiteren URLs die beim OpenID Connect Verfahren verwendet werden. Diese müssen wir aufgrund der Öffentlichkeit des Discovery Endpunkts aber nicht extra in der Spring Konfiguration hinterlegen. Bei Google als OpenID Provider ist das der Discovery Endpunkt https://accounts.google.com/.well-known/openid-configuration - im JSON dieses Endpunktes steht die für Google zu verwendende issuer-uri "https://accounts.google.com".
- scope und authorization-grant-type setze ich auf die oben gezeigte Werte, um OpenID Connect mit dem Authorization Code Prozess als Single Sign-On Verfahren festzulegen.
- Nach erfolgreichem Login am OpenID Provider wird der Benutzer auf die in redirect-uri konfigurierte URL weitergeleitet. Diese URL muss auf unser System zeigen und mit der Login-Processing-URL übereinstimmen, die wir im nächsten Abschnitt in der SecurityFilterChain Bean setzen.
Authentifizierung mit SecurityFilterChain Bean
Die gesicherten URL-Pfade und den Login definieren wir im Java Code der SecurityFilterChain Bean.
@EnableWebSecurity
@Configuration
public class SecurityConfiguration {
@Bean
SecurityFilterChain securityFilterChain(
HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
.requestMatchers("/secured").authenticated()
.anyRequest().permitAll();
http.oauth2Login(c -> {
c.loginProcessingUrl("/process-login");
c.loginPage("/oauth2/authorization/sam");
});
return http.build();
}
}
- @EnableWebSecurity, SecurityFilterChain, HttpSecurity, authorizeHttpRequests, requestMatchers, authenticated, anyRequest und permitAll sind in meinem Artikel zu Spring Security erklärt. Zusammengefasst definiert der nicht fett markierte Java Code, dass die URL http://localhost:8080/secured einen eingeloggten Benutzer verlangt. Alle andern URLs können auch von anonymen Benutzern aufgerufen werden.
Mit Spring Boot 3 muss @Configuration gesetzt werden, damit die SecurityFilterChain Bean erzeugt wird. - Das OpenID Token zur Benutzer-Authentifikation wird mittels OAuth2 vom OpenID Provider geholt. Daher definieren wir mit der Methode oauth2Login einen OAuth2 basierten Login. Der Lambda Ausdruck als Parameter von oauth2Login legt weitere Details zum Login fest.
- Die Login Seite befindet sich beim OpenID Provider. Sie wird von Spring über den OpenID Connect Discover Endpunkt ermittelt. loginPage legt die URL fest von der unser Spring Web-Application an den OpenID Provider Login weiterleitet, wenn sie den Prefix "/oauth2/authorization/" und den Postfix passend zum Key in der application.yml Datei hat - also hier "sam".
- loginProcessingUrl ist die URL, welche den Redirect des OpenID Providers nach erfolgreichem Login verarbeitet. Diese URL muss aus Sicherheitsgründen auch am OpenID Provider konfiguriert sein.
Benutzerdaten im ID Token lesen
Nach erfolgreichem Login holt Spring Boot automatisch das OpenID Token vom Token-Endpunkt des OpenID Providers. Dazu ist nichts weiter nötig als die zuvor gezeigte Konfiguration. Um die Daten im OpenID Token zu lesen, lassen wir es uns per Annotation von Spring injizieren:
@GetMapping
public ModelAndView defaultPage(
@AuthenticationPrincipal OidcUser principal) {
String email = principal.getEmail();
Map<String, Object> attributes = principal.getAttributes();
...
...
- Die defaultPage Methode befindet sich in einem Spring Controller und verarbeitet HTTP-Requests. Weitere Details zu Spring Controllern findet ihr hier: spring-mvc-thymeleaf.html
- @AuthenticationPrincipal injiziert den eingeloggte Benutzer automatisch als OidcUser Objekt in unsere Methode.
- Die OidcUser Klasse repräsentiert die Inhalte des OpenID Tokens. OidcUser bietet diverse Methode zum Auslesen der Daten im ID Token an.
- Z. B. liefert getEmail die Email-Adresse des eingeloggten Benutzers.
- getAttributes liefert alle Attribute im ID Token als Map.
Front-Channel Logout
In den vorherigen Abschnitten haben wir nur den Single Sign-On Login implementiert. Der Standard Logout von Spring melden den Benutzer nur in der Session der Spring Web-Application ab. Nach dem Standard Logout besteht immer noch eine Single Sign-On Session am OpenID Provider. Deshalb kann der "abgemeldete" Benutzer gesicherte URLs ohne erneuten Login direkt wieder verwenden. Um den Benutzer richtig abzumelden, machen wir im Zuge des Logouts zusätzlich einen Front-Channel Logout am OpenID Provider. Dazu ergänzen wir die SecurityFilterChain Bean wie folgt:
var rs = new DefaultRedirectStrategy();
http.logout()
.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
.logoutSuccessHandler(
(request, response, auth) -> rs.sendRedirect(
request, response, "https://id.provider.de/logout"));
request, response, "https://id.provider.de/logout"));
- Die Methode logout liefert uns den LogoutConfigurer. Mit diesem konfigurieren wir das Logout-Verhalten unser Spring Anwendung.
- Für eine einfachere Demo lege ich fest, dass der Logout mit der URL /logout gestartet wird. Damit können auch HTTP GET Requests den Logout starten. Ohne die hier verwendete Methode logoutRequestMatcher wäre der Logout nur mit HTTP POST Requests möglich.
- Front-Channel Logout bedeutet, dass wir den Logout auch am OpenID Provider machen. Das könnte wie hier mit einem einfachen Redirect zur Logout-URL des OpenID Providers passieren. Durch logoutSuccessHandler lege ich fest, dass dieser Redirect nach erfolgreichem Logout an unserer Spring Anwendung gestartet wird.
Fazit
Schaut euch noch mal das Schaubild zum OpenID Connect Prozess an. Trotz der Komplexität dieses Prozesses benötigen wir in unser Spring Boot Anwendung fast keinen eigenen Code. Das ist für mich die große Stärke von Spring Boot - Autokonfiguration und Starter Dependencies schenken uns eine einfache Lösung. Entsprechend eurer Security-Anforderungen könnt ihr diese Standard-Lösung für eure Firma anpassen. Achtet aber darauf, dass der Standard erkennbar bleibt, so dass der Single Sign-On Prozess eurer Anwendung wartbar bleibt.
Den Code zum Artikel findet ihr bei GitHub:
Kommentare
Wenn ich bei Anwendung A auf Login drücke und in Anwendung B schon eingeloggt war dann funktioniert das auch. Ich möchte allerdings beim aufrufen von A schon im Backend checken ob die Session (beim IDP) schon besteht und dann direkt übernehmen und nicht die Login Seite anzeigen. Wenn nicht soll er auf die Default Login Seite geleitet werden.
Wenn ich direkt einen redirect zum IDP machen würde wäre ich ja dort in der Login Maske "gefangen" wenn keine Session besteht und nicht auf meiner Seite.
Schau dazu mein YouTube Video https://youtu.be/lKXwxDM9pOk ab Minute 21:50 an.
Nachdem Neustart ist es Anwendung B statt A und das Aufrufen einer authenticated URL sorgt für den automatischen Login bzw. den Single Sign-On.
Auch wenn ich das an dieser Stelle im Video eigentlich nicht zeigen wollte ;-)