Keycloak und Spring Security Teil 3: Kommunikation via KeycloakRestTemplate

Keine Kommentare

In den vorangegangenen Teilen haben wir eine durch Keycloak geschützte Applikation bestehend aus Frontendservice und Backendservice erstellt und dabei Keycloak in Spring Security integriert.
Nun möchten wir abschließend noch einen weiteren Service hinzufügen, der mit dem Backendservice via role-based authentication vor unbefugtem Zugriff geschützt kommuniziert. Glücklicherweise stellt Keycloak uns hierfür das KeycloakRestTemplate zur Verfügung, das dies sehr einfach ermöglicht.

Disclaimer: Um das Beispiel nicht zu verkomplizieren gehe ich hier davon aus, dass der User, der einen Eintrag hinzufügt und der User, dem es erlaubt ist, Mails zu verschicken, ein und derselbe ist.
Andere Implementierungen, z.B. via Confidential-Flow und REST API/ dem Java-Client von Keycloak, die eine Unterscheidung zwischen Frontenduser und Mailuser ermöglichen würden, wären hier ebenso passend.
Weiter setze ich wie gehabt Grundkenntnisse mit Spring Boot und Spring Security sowie eine lauffähige Keycloak-Instanz voraus.

Der Mailservice

Unser Mailservice verschickt schlicht Mails über einen externen SMTP-Server an eine in den application.properties gesetzte Adresse.

Projektstruktur des Mailservices

Bild 1: Projektstruktur des Mailservices – Diesmal ganz einfach gehalten.

Damit andere Services mit dem Mailservice kommunizieren können, stellt dieser einen mit Keycloak geschützten POST-Endpunkt /mail bereit, den die anderen Services ansprechen. Außerdem wollen wir unserem Testnutzer noch eine dedizierte Rolle „mail“ zuweisen, welche wir erst im Adminbereich anlegen und dem Nutzer zuweisen. Da wir diesen Vorgang schon in Teil 1 abgehandelt haben, soll hier ein Bild der fertigen Zuweisung reichen:

rolemapping mailuser

Bild 2: Rollenzuweisung der mail-Rolle (Ein Klick vergrößert das Bild)

Damit unser Mailservice auch Emails verschicken kann, nutzen wir als Maven-Dependencies neben den Startern für Spring Boot Web, Security und Keycloak(!) noch den Spring Boot Mail-Starter, der für unsere Zwecke völlig ausreicht. An dieser Stelle sei mir der obligatorische Hinweis auf das GitHub-Repository gestattet, in dem sich das vollständige Codebeispiel zum Mailservice findet.

Entsprechend erstellen wir unsere application.properties:

server.port=8082
 
#Spring Mail-Properties
spring.mail.host=smtp.yourhost.com
spring.mail.port=xxx
spring.mail.username=[yourmailsenderaccount]
spring.mail.password=[yourpassword]
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.ssl.enable = true
 
#keycloak
keycloak.auth-server-url=http://localhost:8080/auth
keycloak.realm=springboot-example
keycloak.bearer-only=true
keycloak.resource=guestbook-mail-app
keycloak.principal-attribute=preferred_username
keycloak.cors=true
 
#Custom Mail properties
mail.from=${mail_from:Guestbook <yourmailsenderaccount>}
mail.to=${mail_to:yourmailreceiveraccount}

Unser Mailservice läuft also auf Port 8082, kann Emails über einen festzulegenden Anbieter versenden und ist, wie schon in Teil 2 besprochen, für die Kommunikation mit Keycloak konfiguriert.

Die SecurityConfig unseres Mailservices entspricht vom Aufbau der Config des Backendservices, die wir im Detail in Teil 2 besprochen und implementiert haben. Nur in der Security Filter Chain ändern wir unseren Endpunkt und die Rolle entsprechend ab:

@Override
protected void configure(HttpSecurity http) throws Exception
{
    super.configure(http);
    http.cors()
            .and()
            .csrf()
            .disable()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .sessionAuthenticationStrategy(sessionAuthenticationStrategy())
            .and()
            .authorizeRequests()
            .antMatchers("/mail*").hasRole("mail")
            .anyRequest().permitAll();
}

Der Einfachheit halber lassen wir unseren Controller jetzt die ganze Arbeit übernehmen und implementieren ihn wie folgt:

@RestController
@CrossOrigin(origins = "*", methods = {RequestMethod.GET, RequestMethod.POST, RequestMethod.OPTIONS, RequestMethod.PUT})
public class GuestbookMailController {
 
	private final MailSender sender;
 
	@Value("${mail.from}")
	private String from;
 
	@Value("${mail.to}")
	private String to;
 
	@Autowired
	public GuestbookMailController(MailSender sender) {
		this.sender = sender;
	}
 
	@PostMapping(value = "/mail",
			consumes=MediaType.APPLICATION_JSON_VALUE)
	public ResponseEntity<Boolean> sendMail(@RequestBody GuestbookEntry entry, Principal p) {
		Boolean isSent = false;
		SimpleMailMessage msg = new SimpleMailMessage();
		msg.setFrom(from);
		msg.setTo(to);
		msg.setSubject("[Guestbook Entry] " + entry.getTitle());
		msg.setText(entry.getCommenter() + " schrieb:\r\n\r\n" + entry.getComment());
 
		sender.send(msg);
		try {
			Thread.sleep(1500);
			isSent = true;
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
 
		return ResponseEntity.ok().body(isSent);
	}
}

Die Methode sendMail verschickt unsere erzeugte Mail mit den Inhalten des neuen Eintrags. Das war es auch schon zur Implementierung des Mailservices.

Da das KeycloakRestTemplate als ein Decorator mit den benötigten Headern um das normale Spring RestTemplate fungiert, benötigen wir hier nicht mehr als die Einbindung des Starters und die Spring Security-Integration via SecurityConfig, um es zu verarbeiten. Wird nun ein Request eines Nutzers ohne mail-Rolle abgeschickt, erhält dieser einen 403-Status zurück.

Aufruf und Konfiguration im Backendservice

Für die Konfiguration des aufrufenden Backend-Services ändern wir unsere Security-Konfiguration so ab, dass sie in der Lage ist, auch asynchrone Requests vernünftig zu authentifizieren.
Dazu müssen wir die Strategie ändern, mit der der SecurityContextHolder in Spring diesen Context gegenüber Threads vorhält – standardmäßig passiert dies via ThreadLocal, welcher leider dafür sorgt, dass neu gespawnte Threads (@Async) die Authentication verlieren.
Das manifestiert sich als Fehler im KeycloakRestTemplate, da dieses intern über die KeycloakClientRequestfactory die Authentication wie folgt abruft:

...
SecurityContextHolder.getContext().getAuthentication();
...

Ist diese null, wird eine IllegalStateException ausgelöst, da das Template keine Möglichkeit hat, auf die sicherheitsrelevanten Daten zuzugreifen.

Lange Rede, kurze Lösung: Wir ändern die Strategy im Konstruktor unserer SecurityConfig, sodass für den SecurityContext statt eines ThreadLocal ein InheritableThreadLocal genutzt wird, der diese Informationen beim Spawnen von Childthreads weitergibt. Dann klappt’s auch mit dem @Async.

public SecurityConfig(KeycloakClientRequestFactory keycloakClientRequestFactory) {
        this.keycloakClientRequestFactory = keycloakClientRequestFactory;
 
        //to use principal and authentication together with @async
        SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
 
    }

Daneben müssen wir das KeycloakRestTemplate noch als @Bean deklarieren. Hier nutzt der Keycloak-Adapter und dessen Doku leider noch nicht die aktuellen Spring-Boot-Best Practices > Version 1.4. Eine entsprechende Änderung sollte aber a) relativ einfach sein, und wird b) mit einer der nächsten Versionen kommen – wir halten uns in diesem Fall erst einmal an die Dokumentation:

@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public KeycloakRestTemplate keycloakRestTemplate() {
    return new KeycloakRestTemplate(keycloakClientRequestFactory);
}

Im Codebeispiel sehen wir außerdem, dass das Bean den Scope Prototype besitzt. Nun gebe ich gern zu, dass ich mir nicht sicher bin, warum dies so sein muss. Die Dokumentation sagt an dieser Stelle nur:

Note that it must be scoped as a prototype to function correctly.

Soweit ich es sehen kann, haben RestTemplates in Spring den üblichen Singleton-Scope und sind thread-safe, mit der Einschränkung, dass durch Nutzung von eigenen MessageConvertern die thread-safety nicht mehr garantiert werden kann. Das KeycloakRestTemplate setzt aber nur den Authorization-Header, keine eigenen Converter.
Einige Kommentare sagen weiter, dass eine mehrfache Instanziierung durchaus Performanceprobleme auslösen kann.

Was ich bisher sicher sagen kann ist, dass es auch ohne den Prototype-Scope bestens funktioniert. Meine Nachfrage bei Sébastien Blanc von Red Hat hat weiter ergeben, dass der Spring Security Adapter in Version 3.3.0 auf den aktuellen Best Practice refaktorisiert wird und dort dann auch der Singleton-Scope genutzt wird. Bis dahin sei es euch überlassen, den Scope zu ändern oder auch nicht. 🙂

Damit sind wir jedenfalls auch schon fertig mit der Konfiguration – kommen wir also jetzt zum Aufruf des Mailservices aus dem Backendservice:

package de.codecentric.demo.guestbook.infrastructure.thirdParty;
 
@Component
public class SpringMailClient implements GuestbookMailClient {
 
    private final KeycloakRestTemplate template;
 
    public SpringMailClient(KeycloakRestTemplate template) {
        this.template = template;
    }
 
    @Override
    public void sendMail(GuestbookEntry entry) {
        String endpoint = "http://localhost:8082/mail";
        Boolean result = template.postForObject(endpoint, entry, Boolean.class);
        System.out.println("Mail sent: " + result.toString());
    }
}

Diesen registrieren wir als @Component und injizieren unser soeben konfiguriertes KeycloakRestTemplate. Dann rufen wir in altbekannter RestTemplate-Manier den Endpunkt via postForObject auf. Dabei übernimmt das KeycloakResttemplate für uns das komplette Handling des bearer-tokens.
Starten wir jetzt unsere 3 Services, melden uns an und entfernen die mail-Rolle des Testnutzers, so kann dieser beim nächsten Request zwar noch einen Eintrag erstellen, allerdings würde keine Mail mehr versandt werden, und unser Backendservice erhielte einen 403-Statuscode. Der Endpunkt ist also geschützt 🙂

Fazit:

Im Laufe dieser Artikelreihe haben wir gesehen, wie einfach es mit Spring Boot, Spring Security und Red Hats SSO-Lösung Keycloak ist, seine Systeme und APIs vor unbefugtem Zugriff zu schützen. Dabei haben wir in diesem Teil einen geschützten Endpunkt mittels KeycloakRestTemplate in wenigen Schritten einrichten und aufrufen können.

Dies soll zur Darstellung der Grundintegration von Keycloak in Spring Security erst einmal ausreichen, auch wenn die Themenvielfalt noch um einiges größer ist. Sehr spannend ist zum Beispiel auch die Anbindung bestehender Identity Provider, wie z.B. Active Directory, oder das Hinzufügen von Custom-Attributen zu einem User/Token.

Ich freue mich sehr über alle Leser und Leserinnen, die bis hierhin durchgehalten haben. Schreibt gern in die Kommentare, was euch gefallen hat, was ich besser machen oder erklären könnte und wo vielleicht noch Schwierigkeiten beim Verständnis vorhanden sind. Ich versuche, jeden Kommentar zeitnah zu beantworten.

Code: GitHub
Weitere Artikel dieser Serie: Teil 1 | Teil 2

Dominik arbeitet als erfahrener Software-Entwickler und Consultant im Bereich verteilte Systeme. Er unterstützt seine Kunden in der Entwicklung verteilter Systeme, sei es bei der Entwicklung von RESTful APIs und Webapplikationen oder der Entwicklung von Microservice- und Big-Data-Architekturen.

Share on FacebookGoogle+Share on LinkedInTweet about this on TwitterShare on RedditDigg thisShare on StumbleUpon

Kommentieren

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.