Rest-Schnittstellen absichern mit Java Spring, OAuth2.0 & JSON Web Token

Rest-Schnittstellen absichern mit Java Spring, OAuth2.0 & JSON Web Token

Durch die zunehmende Verwendung des Microservice-Architektur-Patterns ist eine zentrale Instanz zur Verwaltung der Benutzeranmeldeinformationen unabdingbar. Das Autorisierungsprotokoll OAuth 2.0 bietet hierfür im Zusammenspiel mit Java Spring eine perfekte Basis. Dieser Artikel veranschaulicht anhand eines Beispiels, wie sich dies in der Praxis realisieren lässt.

OAuth2.0

Das offene Autorisierungsprotokoll OAuth 2.0 gehört zu den bekanntesten Autorisierungsverfahren der heutigen Zeit. Ziel dieses Protokolls ist es, einem Endnutzer Zugriff zu seinen Ressourcen (bzw. REST-Schnittstellen) zu gewähren.

Das OAuth2.0 Protokoll unterstützt hierbei verschiedene Verfahren zur Autorisierung von Nutzern [1]. Grundsätzlich wird bei den Verfahren immer zwischen dem Resource-Owner, dem Ressource-Server und dem Autorisierungs-Server unterschieden. Der Resource-Owner (oder auch Endnutzer) autorisiert sich mittels Nutzername und Passwort gegenüber dem Autorisierungsserver. Dieser stellt daraufhin ein Access-Token (dt. „Zugriffs-Marke") aus. Mithilfe dieses Access-Tokens kann der Nutzer nun eine Anfrage an den Ressourcenserver stellen.

Der Ressourcenserver prüft daraufhin die Integrität des Access-Tokens und verarbeitet dann den Aufruf. Das in diesem Artikel verwendete „Password-Credentials"-Verfahren wird in der Abbildung 1 genauer dargestellt. Weitere Informationen zu diesem Verfahren lassen sich der Link-Sammlung entnehmen.

Abb. 1: OAuth2.0 – Password Credentials Flow

JSON Web Token

Innerhalb des OAuth2.0-Verfahrens wird die Beschaffenheit des Access-Tokens nicht weiter erläutert. Als Standard hat sich hier das JSON Web Token etabliert, welches sich für JavaScript Clients, wie beispielsweise Angular- oder React-Anwendungen, sehr gut verarbeiten lässt.

Das JSON Web Token (JWT) ist ein JSON-Objekt, dass laut RFC 7519 [2] als sicherer Weg definiert ist, um Informationen zwischen zwei Parteien auszutauschen. Ein JWT ist eine Aneinanderreihung von Header, Payload und Signatur. Diese drei Bestandteile werden durch einen Punkt ​"." voneinander getrennt und enthalten folgende Informationen:

  • Header
    Der Header enthält Informationen bzgl. des Verschlüsselungsverfahrens.
  • Payload
    Innerhalb des Payloads werden die Daten des Tokens vorgehalten. Hierunter fallen Informationen wie das
    Subjekt, für das das Token ausgestellt wurde – also der Nutzer, der Name des Nutzers, wie auch Informationen über die Haltbarkeit des JSON Web Tokens.
  • Signatur
    Die Signatur ist zur Validierung des Tokens gedacht. Hierdurch lässt sich errechnen, ob der Token kompromittiert wurde. Die Berechnung der Signatur ist abhängig von dem verwendeten Verschlüsselungsverfahren.

Für Testzwecke lassen sich diese JSON Web Tokens auch online valideren. Hierfür kann die Webseite von JWT.io [3] verwendet werden.

Architektur

Um einer Microservice-Landschaft gerecht zu werden, wird für dieses Beispiel auf zwei unabhängige Microservices gesetzt. Beide Applikationen verwenden als Grundlage Java und setzten darüber hinaus das Framework Spring ein. Das Spring-Framework [4] hat sich in den letzten
Jahren als De-facto-Standard für die Entwicklung von Micro­services im Bereich Java etabliert und bietet eine Reihe von Tools zur Unterstützung in diesem Architektur-Pattern an. Als Paketmanager wird Maven eingesetzt. Die grund­legenden Abhängigkeiten beider Microservices
können der Abbildung 2 entnommen werden.

Die Applikationen werden in ihren Funktionalitäten wie folgt geschnitten:

  • Autorisierungsserver
    Der Autorisierungsserver ist verantwortlich für die Ausstellung der JSON Web Tokens. Darüber hinaus ist er
    für die Validierung eingehender Anfragen gegen den REST-Service verantwortlich.
  • Ressourcenserver
    Der Ressourcenserver stellt Endpunkte zur Abfrage von Informationen (Ressourcen) für einen Endbenutzer zur Verfügung. Eingehende Anfragen an diesen Server
    werden zunächst vom Autorisierungsserver validiert und dann ausgeführt.

Zur Veranschaulichung werden die Anfragen später mittels Postman [1] an den Ressource-Server gestellt. Postman bietet darüber hinaus die Möglichkeit, sich gegenüber einem OAuth-Server automatisch zu autorisieren.

...
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.3.RELEASE</version>
</parent>
<properties>
<java.version>1.8</java.version>
</properties>
... 

Abb. 2: Grundlegende Abhängigkeiten beider Applikationen (pom.xml)

Der Autorisierungsserver

Als Herzstück dieses Projektes kann der Autorisierungsserver angesehen werden. Dieser ist, wie bereits oben beschrieben, für die Ausstellung und Validierung der JSON Web Tokens verantwortlich. Zur Konfiguration dieser Schnittstellen werden zwei Abhängigkeiten aus dem Spring-Framework verwendet (siehe Abbildung 4):

  • Spring Security OAuth2 [5]
    Dieses Paket enthält alle notwendigen Funktionalitäten zur Abwicklung von OAuth2.0-Protokollen. Es ermöglicht eine einfache Konfiguration des Autorisierungsservers.
  • Spring Security JWT
    Mithilfe von Security JWT wird die Möglichkeit geschaffen, einen Access Token aus dem OAuth2.0-Protokoll als JSON Web Token zu übermitteln.

Neben diesen beiden Abhängigkeiten ist die Konfiguration des Autorisierungsservers denkbar einfach. Um den Server nun vollständig betriebsbereit zu machen, müssen nur noch folgende zwei Konfigurationsdateien erstellt werden:

<? xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
…
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.0.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.3.3.RELEASE</version>
</dependency>
</dependencies>
…
</project> 

Abb. 4: Ausschnitt der Abhängigkeiten des Autorisierungsservers (pom.xml)

Security-Konfiguration

Die Security-Konfiguration kann der Abbildung 5 entnommen werden. Diese Konfiguration ist grundsätzlich für die Einrichtung der Nutzer zuständig. Die Klasse ​SecurityConfig erbt hierbei von der Klasse ​WebSecurityConfigurerAdapter. Hierdurch ist es notwendig, die Methode ​configure zu überschreiben, welche die Konfiguration übernimmt. Der in dieser Klasse verwendete ​BcryptPasswordEncoder verfügt über einen Hash-Algorithmus, damit die verwendeten Passwörter nicht im Klartext in der Anwendung verwendet werden.

In diesem Fall wird eine ​InMemoryAuthentication verwendet. Das bedeutet, dass die Autorisierung nicht gegen eine Datenbank stattfindet, sondern zur Laufzeit der Anwendung von eben dieser bereitgestellt wird. In diesem Fall wird nur ein Nutzer mit dem Namen „John" und dem Passwort „password" zugelassen. Des Weiteren wird das Passwort encodiert, um zusätzliche Sicherheit zu schaffen und das Passwort nicht im Klartext abspeichern zu müssen.

Hier ist es ebenso möglich, anstelle der Methode ​InMemoryAuthentication eine Datenbank anzubinden oder eine andere Quelle, wie beispielsweise ein LDAP, zu verknüpfen. Um dieses Beispiel möglichst einfach zu halten, ist hier allerdings keine andere Quelle für Nutzer­informationen verknüpft.

package de.ordix.news.OAuth2.autorisierungserver.config;
@Configuration
@Order(1)
public class SecurityConfig extends WebSecurityConfigurerAdapter
{
@Override
protected void configure(AuthenticationManagerBuilder
auth) throws Exception {
auth.parentAuthenticationManager(authenticationManag
erBean())
.inMemoryAuthentication()
.withUser("John")
.password(passwordEncoder().
encode("password"))
.roles("USER");
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean()
throws Exception {
return super.authenticationManagerBean();
}
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
} 

Abb. 5: Security-Konfiguration (SecurityConfig.java)

Autorisierungsserver-Konfiguration

Mithilfe der Konfiguration des Autorisierungsservers werden die verschiedenen Schnittstellen, die auch in Abbildung 3 zu sehen sind, konfiguriert. Die vollständige Implementierung kann der Abbildung 6 entnommen werden.

Die Annotation ​@EnableAuthorizationServer stellt die wichtigste Zeile innerhalb dieser Konfigurationsklasse dar, da durch diese die Applikation grundsätzlich als Autorisierungs-Server verstanden wird und für das OAuth2.0-Protokoll vorbereitet wird. Das Spring-Framework benötigt nun zur Konfiguration des Autorisierungsservers nur noch die notwendigen Informationen für das Protokoll wie Client-ID, Client-Secret sowie das zu verwendenden OAuth2.0-Verfahren. Diese Informationen werden in den Methoden ​configure() übergeben, die aus der Klasse ​AuthorizationServerConfigurerAdapter überschrieben werden.

Die Methode ​configure hat hierbei drei Überladungen mit unterschiedlichen Eingangsparametern. Nachfolgend wird die Funktionalität der drei Methoden kurz beschrieben:

  • Configure (AuthorizationServiceSecurityConfigurer oAuthServer)
    In dieser Methode werden die Zugriffsrechte für die zwei Endpunkte ​token sowie ​check_token definiert. Grundsätzlich sollte hier der Endpunkt ​token für jeden Nutzer zur Verfügung stehen, da dieser für die
    Anmeldung am System benötigt wird. Der Endpunkt ​check_token muss nur für die Nutzer zur Verfügung stehen, die bereits am System angemeldet sind. Daher wird hierbei ​isAuthenticated() als Berechtigungsmethode verwendet.
  • Configure (ClientDetailsServiceConfigurer clients)
    Hierdurch werden die notwendigen Client-Informationen bereitgestellt, d. h., dass die OAuth2.0-Protokolle für verschiedene Clients konfiguriert werden. In diesem Beispiel wird nur ein Client mit der ID: ​OrdixSampleID und dem Secret: ​ordixSecret verwendet. Diesem Client wird der Grant_Type ​password mitgegeben, was dem gleichnamigen Flow von OAuth2.0 entspricht.
  • Configure (AuthorizationServerEndpointsConfigurer endpoints)
    Diese Methode dient zum Koppeln der standard­mäßig von Spring zur Verfügung gestellten Endpunkte für OAuth2.0 mit den Implementierungen unserer Applikation. Da es sich in diesem Beispiel um ein JSON Web Token als Access-Token handelt, wird hier der dazugehörige ​JwtTokenStore mit dem Endpunkt verknüpft. Des Weiteren wird hier der ​AuthentcationManager mitgegeben, der zuvor in der Security-Konfiguration mit Nutzerinformationen versorgt wurde.

Sind alle Konfigurationen soweit getätigt, lässt sich die Applikation über das Kommando ​mvn spring-boot:run in der Kommandozeile starten.

Abb. 3: Die grundlegende Beispielarchitektur
package de.ordix.news.OAuth2.autorisierungserver.config;
@Configuration
@EnableAuthorizationServer
public class AuthServerConfig extends AuthorizationServerConfigurerAdapter
{
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private AuthenticationManager authenticationManager;
@Override
public void configure(final AuthorizationServerSecurityConfigurer
oauthServer) throws Exception {
oauthServer.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()");
}
@Override
public void configure(final ClientDetailsServiceConfigurer
clients) throws Exception {
clients.inMemory()
.withClient("OrdixSampleID")
.secret(passwordEncoder.
encode("ordixSecret"))
.authorizedGrantTypes("password")
.scopes("user_info")
.autoApprove(true);
}
@Override
public void configure(final AuthorizationServerEndpointsConfigurer
endpoints) throws Exception {
endpoints
.tokenStore(tokenStore())
.accessTokenConverter(accessTokenConverter())
.authenticationManager(authenticationManag
er);
}
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("dein-signing-key");
return converter;
}
} 

Abb. 6: Konfiguration des Autorisierungsservers (AuthServerConfig.java).

Der Ressourcen-Server

Der Ressourcen-Server stellt Ressourcen (REST-Schnittstellen) zur Verfügung, die durch den Autorisierungsserver abgesichert sein sollen. Als Beispiel wird hierbei ein einzelner Endpunkt zur Verfügung stehen, der unter ​api/hello erreichbar ist. Dieser Endpunkt wird daraufhin den angemeldeten Nutzer begrüßen. Somit kann sichergestellt werden, dass die Anbindung am Autorisierungsserver funktioniert.

Die Maven-Abhängigkeiten für diese Applikation können der Abbildung 8 entnommen werden. Zum Starten der Anwendung werden für einen Ressourcenserver verschiedene Konfigurationen zum Start der Anwendung erwartet, die in der Abbildung 7 zu finden sind. Diese Konfigurationen werden von der Annotation ​@EnableResourceServer in der Konfigurationsklasse ​ResourceServerConfig erwartet. Diese Klasse stellt alle relevanten Informationen zur Verfügung, um die Anbindung an den Autorisierungsserver bereitzustellen (siehe Abbildung 9). Die Vererbung findet von der Klasse ​ResourceServerConfigurerAdapter statt. Diese benötigt zur Verbindung zum Autorisierungsserver einen Token-Service, welcher durch die notwendigen Verbindungsdaten (Client-ID, Client-Secret sowie die URL des Autorisierungsservers) initialisiert wird.

Des Weiteren ist es notwendig zu definieren, welche Endpunkte durch diese Autorisierung abgesichert werden sollen. Dies wird in der Methode ​configure() definiert. In diesem Beispiel werden alle Endpunkte abgesichert, die mit ​api/ beginnen. Alle weiteren Endpunkte sind ohne Autorisierung erreichbar.

Nun wird nur noch der Endpunkt ​api/hello benötigt. Hierfür wird eine Klasse ​HelloController.java angelegt, der diesen bereitstellt (siehe Abbildung 10). Über das Objekt Principal lässt sich der aktuell eingeloggte Benutzer des eingehenden Requests ermitteln. In diesem Beispiel wird über den Aufruf ​principal.getName() der Name zurückgegeben.

Sind alle Konfigurationen abgeschlossen, lässt sich die Applikation über das Kommando ​mvn spring-boot:run in der Kommandozeile starten.

security:
oauth2:
client:
clientId: OrdixSampleID
clientSecret: ordixSecret
accessTokenUri: http://localhost:8081/oauth/token
userAuthorizationUri: http://localhost:8081/oauth/authorize
resource:
token-info-uri: http://localhost:8081/oauth/check_token  

Abb. 7: Applikations Einstellungen Ressourcenservers (application.yml)

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.3.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot
</groupId>
<artifactId>spring-security-oauth2-autoconfigure
</artifactId>
<version>2.0.0.RELEASE</version>
</dependency>
</dependencies>Aciis iam nostabe mendum quit.  

Abb. 8: Ausschnitt der Abhängigkeiten des Ressourcenservers (pom.xml)

package de.ordix.news.OAuth2.ressourceserver.config;
@Configuration
@EnableResourceServer
@Order(1)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter
{
@Value("${security.oauth2.client.clientId}")
private String clientId;
@Value("${security.oauth2.client.clientSecret}")
private String clientSecret;
@Value("${security.oauth2.resource.token-info-uri}")
private String tokenEndpoint;
@Override
public void configure(final HttpSecurity httpSecurity)
throws Exception {
httpSecurity.requestMatchers()
.antMatchers("/api/**")
.and()
.authorizeRequests()
.anyRequest()
.authenticated().antMatchers("/").permitAll();
}
@Primary
@Bean
public RemoteTokenServices tokenServices() {
RemoteTokenServices tokenService = new RemoteTokenServices();
tokenService.setCheckTokenEndpointUrl(tokenEndpoint);
tokenService.setClientId(clientId);
tokenService.setClientSecret(clientSecret);
return tokenService;
}
}   

Abb. 9: Resource-Server-Konfiguration (ResourceServerConfig.java)

package de.ordix.news.OAuth2.ressourceserver.controller;
@RestController
@RequestMapping("/api/hello")
public class HelloController {
@GetMapping()
public String hello(Principal principal) {
return "Hallo " + principal.getName();
}
}  

Abb. 10: Principal aus dem Request erhalten (HelloController.java)

Ein kleiner Test

Will man nun die Schnittstelle ​api/hello vom Ressourcenserver aufrufen, so muss zuerst ein Access-Token vom Autorisierungsserver abgerufen werden.

Über den Button ​Get New Access Token kann dies im Postman geschehen, wenn die Autorisierung auf OAuth 2.0 gestellt wird. Im nachfolgenden Dialog müssen, dann die Daten für den Autorisierungsserver eingegeben werden (siehe Abbildung 11). Durch einen Klick auf ​Request Token wird der Autorisierungsserver mittels ​HTTP-POST nach einem Access-Token gefragt. Dieser kann dann im weiteren Verlauf verwendet werden.

Mittels ​HTTP-GET kann dann eine Anfrage an den Ressource-Server gestellt werden (siehe Abbildung 12). Hat alles funktioniert, so sollte als Antwort „Hallo John" zurückgesendet werden.

Beendet man nun den Autorisierungs-Server innerhalb der Kommandozeile (STRG + C), so sollte bei einem wiederholten Aufruf an den Ressourcenserver ein Fehler zurückgegeben werden, dass der mitgelieferte Token nicht validiert werden konnte.

Abb. 11: Access Token anfordern mittels Postman
Abb. 12: Schnittstelle Aufruf mit Access-Token

Fazit

Dieser Artikel zeigt anschaulich, wie REST-Schnittstellen innerhalb einer Spring-Applikationen abgesichert werden können. Mittels einfacher Konfigurationsklassen kann so in kürzester Zeit eine zentrale Komponente zur Autorisierung von Nutzern aufgebaut werden.

Ein zentraler Autorisierungsserver kann in einer Microservice-Landschaft für mehrere Applikationen gleichzeitig verwendet werden und die Komplexität von Security-Anforderungen an einer Stelle bündeln. Unter Zunahme weiterer Security-Aspekte wie der Rollenverwaltung lassen sich so verschiedene Schnittstellen für unterschiedliche Nutzergruppen absichern.

Der gesamte Source-Code der Anwendung kann online heruntergeladen werden. [8]

Phillipp Kürsten (Diese E-Mail-Adresse ist vor Spambots geschützt! Zur Anzeige muss JavaScript eingeschaltet sein!)

Links/Quellen

[1] OAuth 2.0: https://oauth.net/2/

[2] JSON Web Token - Standard: https://tools.ietf.org/html/rfc7519#

[3] JSON Web Token - Encoder: https://jwt.io/

[4] Java - Spring: https://spring.io/

[5] Spring Security OAuth 2: https://projects.spring.io/spring-security-oauth/docs/oauth2.html

[6] Postman – API Development: https://www.getpostman.com/

[7] OAuth 2.0 – Password Flow: https://developer.okta.com/blog/2018/06/29/what-is-the-oauth2-password-grant

[8] GitHub – Beispiel-Projekt: https://github.com/PhilKuer/spring-jwt-oauth2-sample

[Q1] IT-Security – ORDIX Blog: https://blog.ordix.de/component/easyblog/tags/it-security

[Q2] Spring Power Workshop – ORDIX Seminare: https://seminare.ordix.de/seminare/entwicklung/java-ee/spring-power-workshop.html

[Q3] Microservices Workshop mit Spring Boot – ORDIX Seminare: https://seminare.ordix.de/seminare/entwicklung/java-ee/microservices-workshop-mit-spring-boot.html

BILDNACHWEIS © istockphoto.com | Cecilie_Arcurs | Lassen Sie uns eintauchen in diesen code

 

Kommentare

Derzeit gibt es keine Kommentare. Schreibe den ersten Kommentar!
Gäste
Freitag, 20. September 2019

Sicherheitscode (Captcha)