Microservices absichern mit Spring Boot, Netflix ZUUL und OAuth 2.0 - Teil 2

Donnerstag, 13.6.2019

microservices - deiche für die inseln - schafe

Deiche für die Inseln: Im vorherigen Blogpost haben wir ein Beispiel-Szenario vorgestellt und gesehen, wie die Authentifizierung an einem OAuth 2.0 Authorization Server erfolgt. Darüber hinaus haben wir gesehen, wie wir OAuth hinter einem API Gateway verbergen und in eine klassische Formbased Authentifizierung übersetzen. In diesem Teil der Blogserie werden wir sehen, wie die Autorisierung an unserem Microservice auf Basis der bereits erfolgten Authentifizierung durchgeführt wird.

Herausforderungen bei der Autorisierung

Nachdem im Rahmen der Authentifizierung am OAuth 2.0 Authorization Server geklärt wurde, im Namen welchen Benutzers auf eine Ressource zugegriffen wird, führt der Resource Server die Autorisierung durch. Dabei prüft er, ob dieser Benutzer überhaupt berechtigt ist, die Ressource auf die angefragte Weise zu nutzen. Um dies beantworten zu können, benötigt der Resource Server allerdings Informationen. Welche Berechtigungen hat der Benutzer überhaupt? Wo bekommt der Resource Server die Informationen über die Berechtigungen her und wie werden diese verwaltet? Wie können die Berechtigungen in einer Microservices-Architektur verwaltet werden, sodass sie auch flexibel erweiterbar sind, wenn ein Microservice neue Berechtigungen erfordert? Diese Fragen beleuchten die nachfolgenden Abschnitte.

Wer kennt die Berechtigungen?

Die Verwaltung der Berechtigungen erfolgt in der Webshop-Anwendung nach dem RBAC-Prinzip (RBAC = Role Based Access Control). Dem Benutzer sind dabei Rollen zugeordnet, denen wiederum die Einzelberechtigungen wie z.B. "Produktbeschreibung ändern" zugewiesen sind. Die Berechtigungszuordnungen (Benutzer - Rollen - Berechtigungen) hinterlegen Administratoren der Anwendung im Authorization Server. Ein Resource Server fragt mit einem Access Token die Informationen zu einem Benutzer beim Authorization Server an und entscheidet, basierend auf den Berechtigungen des Benutzers, ob die Aktion durchgeführt werden darf oder nicht. Die Verwendung von OAuth spielt hier einen entscheidenden Vorteil aus: Ruft ein Microservice einen anderen Microservice auf, übergibt er beim Aufruf das Access Token und agiert damit im Berechtigungskontext des Benutzers.

Welcher Gestalt das Access Token ist, spielt hierbei keine große Rolle. Es kann z. B. ein informationsbehaftetes Token wie ein JWT (JSON Web Token) eingesetzt werden. In diesem sind Informationen zum Benutzer, insbesondere auch zu den Berechtigungen, hinterlegt. Der Resource Server prüft dann die Gültigkeit des Tokens über die Signatur des Authorization Servers, der dieses Token ausgestellt und signiert hat. Vorteile dieses Ansatzes sind die Unabhängigkeit von einer zentralen Stelle und die direkte Weitergabe von Informationen im Token. Dies führt zu einer Minimierung von erforderlichen Requests zwischen Resource Server und Authorization Server. Nachteilig bei diesem Ansatz ist jedoch, dass ein einmal ausgestelltes JWT bis zu seinem zeitlichen Ablauf gültig ist und nur über eine Revocation List als invalide markiert werden kann. Zudem ist die Aufnahmefähigkeit des Tokens begrenzt, wenn es im HTTP-Authorization-Header mitgeschickt wird. Dadurch kann die Liste der Berechtigungen des Benutzers schnell die maximal erlaubte Größe des JWT übersteigen. Des Weiteren muss der Key zur Prüfung der durch den Authorization Server ausgestellten Signatur des JWT an alle Resource Server verteilt werden. Alternativ zu einem informationsbehafteten Token kann ein abstraktes Token, wie z. B. eine zufällige UUID, eingesetzt werden. Die UUID selbst enthält keine direkt nutzbaren Informationen zur Überprüfung der Autorisierung. Mit dem UUID-Access Token kann der Resource Server beim Authorization Server aber alle benötigten Information abfragen. Vorteil dieses Ansatzes ist, dass das Access Token an zentraler Stelle, dem Authorization Server, invalidiert werden kann. Zudem können beliebig viele Informationen (Benutzerberechtigungen) beim Authorization Server über bereitgestellte Endpoints abgefragt werden. Ein Nachteil dieses Verfahrens ist jedoch die Abhängigkeit des Resource Server von einer zentralen Authorization-Server-Instanz, die nur mit zusätzlichem Aufwand horizontal skaliert werden kann.

Im weiteren Verlauf dieses Blogposts wird mit einem abstrakten Access Token in Form einer Random UUID gearbeitet.

Token-Tausch im API Gateway

Nachdem sich ein Benutzer über das API Gateway am Authorization Server authentifiziert hat, wird das ausgestellte Access Token innerhalb des API Gateways in einer regulären Session vorgehalten. Die SPA erhält vom API Gateway im Erfolgsfall lediglich ein Session-Cookie. Das Ablegen des Access Tokens in der Session des API Gateways und die weitere Session-Behandlung übernimmt Spring Security für uns. Dazu liefert der weiter oben im Kontext der Authentifizierung beschriebene eigene Authentication Provider ein entsprechendes OAuth2AccessTokenAuthentication-Objekt, welches das Access Token beinhaltet und von Spring Security in der Session abgelegt wird.

Durch diesen Tausch des OAuth Access Tokens in ein Session-Cookie ergibt sich insbesondere ein Vorteil in Bezug auf die Gültigkeitsdauer einer Anmeldung: Ein OAuth Access Token ist für eine feste Zeit ab dessen Ausstellung gültig. Dabei ist diese unabhängig von der Benutzeraktivität. Die Session verfällt jedoch nach einer bestimmten Zeit ab dem letzten Request. Würde somit das OAuth Access Token durch das API Gateway selber z. B. in Form eines Cookies an die SPA gemeldet, so müsste sich der Benutzer nach dessen Ablauf erneut anmelden. Da dies jedoch unabhängig von dessen Aktivität wäre, würde in der Praxis entweder mit einer relativ langen Gültigkeit des Access Tokens gearbeitet werden, oder es müsste zusätzlich mit einem Refresh Token gearbeitet werden. Bei der Verwendung des Refresh Tokens müsste dieses zusätzlich zum Client transportiert werden und dieser müsste entsprechende Logik implementieren, im Bedarfsfall über das Refresh Token ein neues Access Token zu beziehen. Damit würden die Implementierung im Client komplexer ausfallen und darüber hinaus Aspekte von OAuth nach außen offengelegt werden.

Bei Verwendung eines JWT als Access Token ergibt sich durch den Token-Tausch zusätzlich die Möglichkeit, das Problem der Revocation des Tokens am API Gateway zu lösen: Wird die Session durch ein Logout invalidiert, geht damit auch das JWT verloren und muss nicht über eine Revocation List explizit invalidiert werden.

Als Nachteile bleiben jedoch der Verlust der Zustandslosigkeit sowie der damit verbundene zusätzliche Aufwand bei einer horizontalen Skalierung zu erwähnen. Hier bietet sich im vorgestellten Technologie-Stack mit Spring Session eine Option, dieses Problem zu lösen, was jedoch mit Blick auf den Umfang des Blogposts hier nicht weiter betrachtet wird.

Das Verwalten der Session im API Gateway übernimmt bereits Spring Security für uns. Es ist lediglich notwendig, bei einem eingehenden Request das Access Token aus einer evtl. vorhandenen Session zu entnehmen und als HTTP Header in dem Request an den Resource Server weiterzugeben. Dazu wird ein Zuul-Filter implementiert, welcher vor dem Weiterleiten des Requests diese Aufgabe übernimmt. Listing 11 zeigt einen solchen Filter.

Listing 11: Zuul-Filter zur Weitergabe des Access Tokens

Zunächst entfernt der Filter einen evtl. vorhandenen HTTP Authorization-Header. Wenn der Benutzer sich vorher am API Gateway erfolgreich angemeldet hat, so kann durch den SecurityContextHolder die erstellte OAuth2AccessTokenAuthentication abgefragt werden. Der Zugriff auf eine evtl. vorhandene Session erfolgt dabei im Hintergrund durch Spring Security.

Hierbei ist noch zu erwähnen, dass Zuul bestimmte HTTP Header per Default herausfiltert. Dazu zählt insbesondere auch der verwendete HTTP Authorization-Header. Dies muss für dieses Setup jedoch deaktiviert werden, da andernfalls der oben gesetzte Header für das Access Token von Zuul vor der Zustellung an den Resource Server wieder entfernt wird. Listing 12 zeigt dazu die verwendete Spring-Konfiguration. Da im oben dargestellten Filter ein Authorization-Header zunächst grundsätzlich entfernt wird, erfüllt der Filter eine ähnliche Funktion und unterbindet, dass ein solcher Header von außen in die Infrastruktur zugestellt wird. Da Zuul im API Gateway erst hinter Spring Security zum Einsatz kommt, sollten die Cookie-Header weiterhin herausgefiltert werden. Die Verwendung des Session-Cookies innerhalb des API Gateways ist davon nicht betroffen. Dadurch wird das Session-Cookie zwar auch nicht an die hinter dem API Gateway liegenden Microservices weitergereicht, die könnten damit aber ohnehin nichts anfangen.

Listing 12: Zuul Konfiguration der Header im API Gateway
Eine Prüfung der Berechtigung erfolgt nicht im API Gateway. Dies wird dem jeweiligen Microservice entsprechend überlassen. Existiert keine Session im API Gateway, so wird der Request dennoch an den Resource Server zugestellt - jedoch ohne Authorization-Header. Es ist auch durchaus möglich, dass die angeforderte Ressource am Resource Server ohne Anmeldung verfügbar ist. Ist dies nicht der Fall, wird der Resource Server mit einem HTTP Status Code 401 (Unauthorized) antworten. Dabei wird im HTTP Header WWW-Authenticate die benötigte Authentifizierungs-Methode mitgeteilt werden. Im Fall des Resource Servers wird dies Bearer sein. Da die Anmeldung durch die SPA am API Gateway jedoch in unserem Beispiel via formbased Login erfolgt, würde dieser Header Details über die Verwendung von OAuth nach außen transportieren und darüber hinaus auch eine für die SPA falsche Authentifizierungs-Methode anfordern. Daher muss der Header im API Gateway mindestens entfernt oder besser noch entsprechend umgeschrieben werden. In unserem Beispiel entfernen wir den Header einfach durch Zuul. Es wäre jedoch auch recht einfach möglich, über einen weiteren Zuul-Filter den Header entsprechend anzupassen.

Ein in Listing 13 dargestellter Request am API Gateway würde somit entsprechend ohne Cookie, jedoch mit passendem Authorization-Header wie in Listing 14 dargestellt, weitergeleitet.

Listing 13: Ressourcen-Zugriff am API-Gateway

Listing 14: Ressourcen-Zugriff hinter dem API-Gateway

 


Seminar: Sichere IT-Architekturen


 

Autorisierung im Resource Server

Die eigentliche Autorisierung erfolgt im jeweiligen Resource Server. Die dazu notwendigen Rollen und Rechte werden dabei zentral im Authorization Server verwaltet. Die finale Prüfung des Zugriffs erfolgt jedoch auf Basis dieser Rechte im jeweiligen Resource Server. Daher müssen diese Informationen im Rahmen der Autorisierung vom Authorization Server abgefragt werden.

Im hier verwendeten Technologie Stack stellt Spring neben der Implementierung des Authorization Servers auch eine entsprechende Implementierung des Resource Servers zur Verfügung. Aktiviert wird diese über die Annotation @EnableResourceServer. Analog zur Konfiguration des Authorization Servers wird der Resource Server ebenfalls über eine spezielle Klasse, den ResourceServerConfigurerAdapter, vorgenommen. Auch im Fall des Resource Servers basiert die Implementierung auf Spring Security, was im Hintergrund durch die Implementierung konfiguriert wird. Listing 15 zeigt die exemplarische Konfiguration eines Resource Servers.

Listing 15: Konfiguration eines Resource Servers
Über die HttpSecurity kann, analog zum WebSecurityConfigurerAdapter, der Zugriff auf die Ressourcen festgelegt werden. Auch der Zugriffsschutz über Spring Security-Annotations wie @Secured kann über die Annotation @EnableGlobalMethodSecurity aktiviert werden. Die jeweiligen Benutzer-Details, inklusive der verfügbaren Rollen und Rechte, werden in diesem Beispiel durch den Resource Server vom Authorization Server abgefragt. Dazu muss dem Resource Server in der Spring-Konfiguration noch der Zugang zum Authorization Server mitgeteilt werden. Listing 16 zeigt die dazu notwendige Konfiguration. Dabei wird neben der URL der check_token Ressource des Authorization Servers die Client-Identifikation mit Passwort des Resource Servers hinterlegt.

Listing 16: Konfiguration des Authorization Server im Resource Server

Erfolgt ein Zugriff auf den Resource Server mit einem Access Token, prüft der Resource Server dieses Token über die hier angegebenen Zugangsdaten am Authorization Server. Listing 17 zeigt exemplarisch diesen Zugriff. Als Authorization-Header wird die Client-Authentifizierung des Resource Servers mitgegeben. Das zu prüfende Token wird im POST-Body unter dem Parameter token übertragen.

Listing 17: HTTP-Request zur Prüfung des Access Tokens am Authorization Server Ist das Access Token valide, wird der Request mit dem HTTP Status-Code 200 beantwortet. Zusätzlich erhält der Resource Server die Benutzer-Details, inklusive der Rollen und Rechte, als JSON-Payload zurück. Listing 18 zeigt diese Struktur exemplarisch. Die dabei gelieferten Authorities werden von der Spring-Implementierung direkt für den Benutzerkontext übernommen. Die eingangs konfigurierte Prüfung des Zugriffs erfolgt somit auf Basis der hier gelieferten Daten.

Listing 18: JSON-Response einer erfolgreichen Access Token-Prüfung Die ebenfalls gelieferten Scopes können im Resource Server auch zur Zugriffsprüfung verwendet werden. Der Ausdruck zur Prüfung auf den Scope default in Spring Security wäre dabei hasScope('default'). Der vergebene Scope hängt dabei nicht vom Benutzer, sondern vom OAuth Client ab. Da es sich in diesem Setup bei dem Client um das API Gateway handelt, besteht dadurch die Möglichkeit, bei mehreren API Gateways den Zugriffsschutz im Resource Server auch vom verwendeten Gateway abhängig zu machen. So könnte ein API Gateway, welches öffentlich zugänglich ist, sensible Funktionen aufgrund seines Scopes unterbinden. Ein API Gateway, welches nur aus einem gesonderten Netz erreichbar ist, könnte hingegen einen Scope mit erweiterten Rechten erhalten.

Fazit

Mit OAuth2 steht ein mächtiger Standard zur Verfügung, in dessen Rahmen die Authentifizierung und Autorisierung in Microservices realisiert werden kann. Der Spring-Stack bietet hierzu eine ebenso mächtige Implementierung an, welche mit überschaubarem Aufwand in einer Microservice-Landschaft verwendet werden kann. Durch den Einsatz eines API Gateways lässt sich darüber hinaus OAuth2 als Angriffsfläche vor einem externen Zugriff verbergen und auf einfachere Authentifizierungs-Verfahren abbilden. Insbesondere das dabei verfügbare zentrale Benutzermanagement hilft in einer Microservice-Landschaft, die Kontrolle über die vorliegenden Benutzer und Berechtigungen zu behalten.

Das in diesem Blogpost vorgestellte Beispiel steht zusätzlich auf GitHub bereit.

Autoren

Andreas HellmannAndreas Hellmann ist als IT-Berater und Software-Architekt für die viadee IT-Unternehmensberatung tätig. Sein Schwerpunkt liegt in der Entwicklung von Enterprise-Anwendungen mit Java und Web-Technologien. Zudem engagiert er sich im Kompetenzbereich Security und vermittelt sein Wissen in Schulungen. Andreas Hellmann bei Xing


Michael TwelekmeierMichael Twelkemeier ist Senior-Berater bei der viadee. Sein Fokus liegt auf der Softwarearchitektur und der Entwicklung von Java/Spring-basierten Enterprise-Anwendungen. Darüber hinaus engagiert er sich im Kompetenzbereich Java.

zurück zur Blogübersicht

Diese Beiträge könnten Sie ebenfalls interessieren

Keinen Beitrag verpassen – viadee Blog abonnieren

Jetzt Blog abonnieren!

Kommentare

Michael Twelkemeier

Michael Twelkemeier

Michael Twelkemeier ist Senior-Berater bei der viadee. Er berät Kunden im Finanzdienstleistungs-Umfeld in Fragen der IT-Architektur und Systemintegration bei der Umsetzung individueller Softwaresysteme mit einem Schwerpunkt auf Java-basierte Enterprise-Anwendungen.

Er ist Leiter des Kompetenzbereichs Java / Software-Architektur und teilt seine Erfahrungen als Autor und Speaker u.a. auf dem NAVIGATE Kongress.

Michael Twelkemeier bei Xing Michael Twelkemeier bei LinkedIn