Fullstack-IAM Teil 2: Spring Boot als Resource Server mit Keycloak Integration
In diesem zweiten Teil der Blogreihe, wird aufgezeigt, wie sich die Integration eines Resource Servers (Spring Boot) mit Keycloak realisieren lässt.
Notwendiges Wissen
- OAuth 2.0
- Authentisierung, Authentifizierung und Autorisierung
- Docker & Docker Compose
- Keycloak
- Entwicklungskenntnisse in
Technische Voraussetzungen
- Laufende Keycloak Instanz (siehe Teil1 der Blogreihe)
- IDE mit entsprechender Java SDK
Spring Boot (Resource Server)
Bootstrapping Spring Boot
Zu Beginn soll ein Spring Boot Projekt mit allen erforderlichen Abhängigkeiten erstellt werden. Das Artefakt dient nachher als Basis für die Entwicklung und Konfiguration.
- Erstellung eines initialen Spring Boot Projektes
- Dazu besuchen wir die Website: https://start.spring.io/
- Hinzufügen der Dependencies
- Spring Web
- Spring Security
- Spring OAuth Resource Server
- Klick auf Generate
- Projekt wird heruntergeladen
Security Konfiguration
In Spring Boot lässt sich die Security Konfiguration anpassen, indem die eigene Klasse um den WebSecurityConfigureAdapter
erweitert wird. Zusätzlich benötigen wir noch eine Annotation über unserer Klasse @EnableWebSecurity
- Klasse namens SecurityConfiguration erstellen
- Klasse um WebSecurityConfigureAdapter erweitern
- Annotation @EnableWebSecurity über die SecurityConfiguration setzen
- Überschreiben der configure() Methode
- cors().configurationSource(request -> setupCorsConfiguration())
- Damit unser Client auf diesen Resource Server zugreifen darf.
- authorizeRequest().mvcMatchers(„/api/secured/**).hasAuthority(„SCOPE_default-user-scope“)
- Damit alle Anfragen, welche auf Schnittstelle gehen die mit „/api/secured/“ beginnen eine Authentifizierung benötigen und im SCOPE: default-user-scope von Keycloak enthalten sein müssen.
- anyRequest().permitAll()
- Damit alle Anfragen die nicht den obigen Regeln entsprechen keine Authentifizierung benötigen
- oauth2ResourceServer().jwt()
- Damit Spring Boot weiß, dass es die Authentifizierung mit einem Json Web Token (JWT) als Resource Server durchführen soll.
- cors().configurationSource(request -> setupCorsConfiguration())
package com.maxxrl.resourceserver; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.web.cors.CorsConfiguration; import java.util.Collections; import java.util.List; @EnableWebSecurity public class SecurityConfiguration extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.cors().configurationSource(request -> setupCorsConfiguration()).and() .authorizeRequests() .mvcMatchers("/api/secured/**").hasAuthority("SCOPE_default-user-scope") .anyRequest().permitAll() .and() .oauth2ResourceServer() .jwt(); } private CorsConfiguration setupCorsConfiguration() { List frontendOrigins = Collections.singletonList("http://localhost:4200"); CorsConfiguration cors = new CorsConfiguration(); cors.setAllowedOrigins(frontendOrigins); cors.setAllowedHeaders(List.of("*")); return cors; } }
Hinzufügen der notwendigen Parameter für OAuth2.0 in der application.properties (oder application.yml) Datei.
Somit sind bereits alle notwendigen Konfigurationen gesetzt, damit der Resource Server über Keycloak geschützt werden kann.
Schnittstelle entwickeln
Wir möchten zwei Schnittstellen entwickeln. Eine die über Keycloak abgesichert ist sowie eine andere welche immer aufgerufen werden kann.
—
package com.maxxrl.resourceserver; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RequestMapping("/api") @RestController public class SecuredController { @GetMapping(path = "/secured") public DoubleSlash securedHelloWorld() { return new DoubleSlash("Hello from doubleSlash secured"); } @GetMapping(path = "/unsecured") public DoubleSlash unsecuredHelloWorld() { return new DoubleSlash("Hello from doubleSlash unsecured"); } class DoubleSlash { private final String content; public DoubleSlash(String content) { this.content = content; } public String getContent() { return content; } } }
—
Da die Entwicklung des Resources Servers mit seinen zwei Schnittstellen soweit abgeschlossen ist. Kann der Resource Server via Spring gestartet werden.
Schnittstelle testen
Für die Tests soll zuerst die abgesicherte Schnittstelle ohne und dann mit Token angefragt werden. Ohne Token erwarten wir, dass wir eine 401 Unauthorized erhalten. Weil wir nicht berechtigt sind auf die Resource zuzugreifen. Wenn das Token an die Anfrage angehängt wird erwarten wir die gesicherte Antwort mit einem 200 Statuscode.
Anfrage an abgesicherte Schnittstelle ohne Token:
curl --request GET \
--url http://localhost:9090/api/secured
Response
HTTP: 401 – Unauthorized
Anfrage an Keycloak um ein Token zu erhalten
curl --request POST \
--url http://localhost:8080/auth/realms/secure-realm/protocol/openid-connect/token \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data client_id=secure-client \
--data username=client-user \
--data password=test \
--data grant_type=password
Response
HTTP: 200
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJjaUtjaHlTdXEzNzdHTHdqemVmcDFhNnN1QjlOSEpPdFdBMFZFRndfV0tRIn0.eyJleHAiOjE2NjYxNjcyNzIsImlhdCI6MTY2NjE2Njk3MiwianRpIjoiMzIyOWZlN2ItNzAyOS00ODM5LWI1MGYtNzZkZWFhNzgwMGMwIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL2F1dGgvcmVhbG1zL3NlY3VyZS1yZWFsbSIsImF1ZCI6ImFjY291bnQiLCJzdWIiOiI3YmE2MmFlZC1iODg0LTQwODAtYTYxYy0yMzU1MWExYjBlNmIiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJzZWN1cmUtY2xpZW50Iiwic2Vzc2lvbl9zdGF0ZSI6IjdmOTM5MzQ1LTgyOTQtNDZmNi1hZTAyLWE3NmI3MDA1NTU3MSIsImFjciI6IjEiLCJhbGxvd2VkLW9yaWdpbnMiOlsiKiJdLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsib2ZmbGluZV9hY2Nlc3MiLCJkZWZhdWx0LXJvbGVzLXNlY3VyZS1yZWFsbSIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJwcm9maWxlIGVtYWlsIGRlZmF1bHQtdXNlci1zY29wZSIsInNpZCI6IjdmOTM5MzQ1LTgyOTQtNDZmNi1hZTAyLWE3NmI3MDA1NTU3MSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJjbGllbnQtdXNlciJ9.SPQgs7GRZ9CNdveQys1PPHhg-3TUl2LIjowFGg9fW6PB-M94VOI7PJc0KNLneU4s5u09ypjHp9Dc-fShcRZeqgpiqjj9XZZpipeYtCw8kBCzxdEQvp6rOej-IjF2EMvraUTp00HfEXkM0eht7ETLRBcOUYPT4NWsrMrgTHbvbeaEyfpW4NkqxlAixjgj4AgYg7qWh7L430Bvl_1US19KWH7Ch4TpX4gRSmQ8hEzOe0U9Q5hKHBgWA2hD-rfdwAeHaOMIXI9SPnyZxfJBl-7oENyPnpSqE4QJkTacbs0kRTgtlQ5nL0FsjRxK6_KmRzOD9WCx_sZ3mQpYWowUCw_1xA",
"expires_in": 300,
"refresh_expires_in": 1800,
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJkNGFmNzNjMC0yNjdjLTQxMGQtYTQ3ZS1hNDM1MTc2YTNiZGQifQ.eyJleHAiOjE2NjYxNjg3NzIsImlhdCI6MTY2NjE2Njk3MiwianRpIjoiZjQxMDFmZjMtNjQwMy00MGU4LWI5OGQtM2ExNTI5N2E4OWNiIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL2F1dGgvcmVhbG1zL3NlY3VyZS1yZWFsbSIsImF1ZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4MC9hdXRoL3JlYWxtcy9zZWN1cmUtcmVhbG0iLCJzdWIiOiI3YmE2MmFlZC1iODg0LTQwODAtYTYxYy0yMzU1MWExYjBlNmIiLCJ0eXAiOiJSZWZyZXNoIiwiYXpwIjoic2VjdXJlLWNsaWVudCIsInNlc3Npb25fc3RhdGUiOiI3ZjkzOTM0NS04Mjk0LTQ2ZjYtYWUwMi1hNzZiNzAwNTU1NzEiLCJzY29wZSI6InByb2ZpbGUgZW1haWwgZGVmYXVsdC11c2VyLXNjb3BlIiwic2lkIjoiN2Y5MzkzNDUtODI5NC00NmY2LWFlMDItYTc2YjcwMDU1NTcxIn0._61qe4R_b4zB2b0xWj_XG4n3mbTIAonNtZjQLqzAIlM",
"token_type": "Bearer",
"not-before-policy": 0,
"session_state": "7f939345-8294-46f6-ae02-a76b70055571",
"scope": "profile email default-user-scope"
}
Nutzung des Tokens an der abgesicherten Schnittstelle:
curl --request GET \
--url http://localhost:9090/api/secured \
--header 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJjaUtjaHlTdXEzNzdHTHdqemVmcDFhNnN1QjlOSEpPdFdBMFZFRndfV0tRIn0.eyJleHAiOjE2NjYxNjcyNzIsImlhdCI6MTY2NjE2Njk3MiwianRpIjoiMzIyOWZlN2ItNzAyOS00ODM5LWI1MGYtNzZkZWFhNzgwMGMwIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL2F1dGgvcmVhbG1zL3NlY3VyZS1yZWFsbSIsImF1ZCI6ImFjY291bnQiLCJzdWIiOiI3YmE2MmFlZC1iODg0LTQwODAtYTYxYy0yMzU1MWExYjBlNmIiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJzZWN1cmUtY2xpZW50Iiwic2Vzc2lvbl9zdGF0ZSI6IjdmOTM5MzQ1LTgyOTQtNDZmNi1hZTAyLWE3NmI3MDA1NTU3MSIsImFjciI6IjEiLCJhbGxvd2VkLW9yaWdpbnMiOlsiKiJdLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsib2ZmbGluZV9hY2Nlc3MiLCJkZWZhdWx0LXJvbGVzLXNlY3VyZS1yZWFsbSIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJwcm9maWxlIGVtYWlsIGRlZmF1bHQtdXNlci1zY29wZSIsInNpZCI6IjdmOTM5MzQ1LTgyOTQtNDZmNi1hZTAyLWE3NmI3MDA1NTU3MSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJjbGllbnQtdXNlciJ9.SPQgs7GRZ9CNdveQys1PPHhg-3TUl2LIjowFGg9fW6PB-M94VOI7PJc0KNLneU4s5u09ypjHp9Dc-fShcRZeqgpiqjj9XZZpipeYtCw8kBCzxdEQvp6rOej-IjF2EMvraUTp00HfEXkM0eht7ETLRBcOUYPT4NWsrMrgTHbvbeaEyfpW4NkqxlAixjgj4AgYg7qWh7L430Bvl_1US19KWH7Ch4TpX4gRSmQ8hEzOe0U9Q5hKHBgWA2hD-rfdwAeHaOMIXI9SPnyZxfJBl-7oENyPnpSqE4QJkTacbs0kRTgtlQ5nL0FsjRxK6_KmRzOD9WCx_sZ3mQpYWowUCw_1xA'
Response:
HTTP: 200
{
"content": "Hello from doubleSlash secured"
}
Zusammenfassung
Es hat sich gezeigt, dass der Aufwand für die Integration von Keycloak in Spring Boot anhand eines Basisbeispiels sehr gering ist. Wir mussten lediglich in den application.properties die öffentliche CERT API von Keycloak eintragen und die SpringSecurity überschreiben, um zu definieren, welche Schnittstellen über OAuth2.0 abgesichert werden sollen. Die entwickelte Schnittstelle ermöglichte uns zuletzt einen Test in vorm von CURL durchzuführen, um zu beweisen, dass die Schnittstelle über Keycloak abgesichert ist. Im nächsten Teil der Blogserie soll die Anbindung eines Frontend mit Angular in die bestehende Architektur Keycloak + Resource Server integriert werden.
GitHub
Zum Keycloak Server & Spring Boot Resource Server
Weitere Interessante Links und Quellen zum Thema
Die Rollverteilung im OAuth 2.0 Standard verstehen und Vorstellung des Implizit Flow
https://ordina-jworks.github.io/security/2019/08/22/Securing-Web-Applications-With-Keycloak.html#/