Suche
Aber sicher!

Angular-Security-Architektur in der Praxis – das Tutorial

Karsten Sitterberg

(c) Shutterstock / dencg

Sicherheit ist ein wichtiges Thema. Aber die Umsetzung auch im Kontext clientseitiger Webanwendungen muss nicht schwierig sein. Das zeigt das praktisches Beispiel einer Angular-Frontend-Anwendung, die auf ein Spring Boot Backend zugreift. Die Absicherung erfolgt mittels OAuth 2.0 und OpenID Connect. Als OpenID-Connect-Provider kommt Keycloak in Version 2.3 zum Einsatz.

Es gibt vier typische Anforderungen zur Absicherung von Anwendungen: Erstens müssen Nutzer identifiziert werden (Authentifizierung), zweitens dürfen Aktionen nur durch berechtigte Nutzer ausgeführt werden (Autorisierung). Deswegen sollten drittens Nutzern lediglich Aktionen zur Auswahl angezeigt werden, die sie auch ausführen können. Und viertens sollen Nutzer nur die Daten abrufen können, für die sie eine Berechtigung besitzen.

Bei Webseiten, die durch serverseitiges Rendering ihre Benutzungsoberfläche erhalten und bei denen der gesamte auszuführende Code zur Request-Zeit zur Verfügung steht, sind alle Anforderungen im gleichen Kontext realisiert: Die Anwendung ist gleichzeitig für Darstellung, Steuerung und Absicherung zuständig und kann damit alle Entscheidungen an einer Stelle treffen. Eine Anmeldung kann direkt an der Anwendung erfolgen und der Zustand z. B. durch eine HTTP Session auf Basis eines Cookies gehalten werden.

Bei JavaScript-Clients sind diese Verantwortlichkeiten nun geteilt: In der Frontend-Anwendung müssen Entscheidungen über die Darstellung getroffen werden. Dazu müssen Informationen über den Benutzer (Authentifizierung) und die Rechte (Autorisierung) des Nutzers dort zur Verfügung stehen. Werden umgekehrt auf dem Backend Services aufgerufen, benötigen diese ebenfalls Informationen, um festzustellen, ob der Aufruf erlaubt ist. Ist die Systemlandschaft umfangreicher, gibt es nicht nur ein einzelnes Backend, sondern mehrere, die durch das Frontend angesprochen werden können. Das gilt vor allem in einer Microservices-basierten Architektur. Eine Anmeldung bei jedem einzelnen System ist damit nicht mehr praktikabel. Da es auch zu kaskadierenden Aufrufen der Services kommen kann, folgt die Anforderung, dass die Authentifizierung separat implementiert und auch delegierbar sein sollte.

AngularJS vs. Angular 2.0

Angular 2.0 ist ein SPA-Framework, das zwar als Nachfolger von AngularJS gedacht ist, jedoch als komplette Neuentwicklung ausgeführt wurde. Lediglich einige Konzepte, wie etwa das der Komponenten, gingen in das neue Framework über. Das Anpassen der Anwendung an bestimmte Benutzerrechte hat sich in Angular 2.0 unter anderem in Hinblick auf das Routing verbessert. Möchte man einen Routingvorgang aufgrund bestimmter Nutzerberechtigungen abbrechen und den Benutzer gegebenenfalls auf ein Log-in-Formular umleiten, bietet AngularJS dafür lediglich umständliche, zumindest aber wenig explizite Lösungsansätze. Ein Ansatz besteht darin, ein Angular-Routing-Event abzufangen und anschließend im Event Handler zu überprüfen, ob der Benutzer angemeldet ist und über die erforderlichen Rechte verfügt. Eine zweite Möglichkeit nutzt die routerbasierte Resolve-Logik. Ein solcher Resolve dient eigentlich zum Beschaffen von Remotedaten, daher wirkt sich dieser Ansatz negativ auf Wartbarkeit und Verständlichkeit der Software aus.

Angular 2.0 sieht ein eigenes, explizites Konzept vor, das es erlaubt, Benutzer mit unzureichenden Berechtigungen abzufangen. Die notwendige Logik heißt Route Guard und bietet ein sauberes, ausdrucksstarkes API, das Lesbarkeit und Wartbarkeit des Codes deutlich erhöht. Im Grundsatz lässt sich das in diesem Artikel vorgestellte Autorisierungs- und Authentifizierungskonzept nicht nur mit Angular 2.0 verwenden, sondern bietet sich für andere Single-Page-Apps an.

Token-basierte Verfahren erlauben es, diese Anforderungen abzubilden und werden schon lange erfolgreich eingesetzt. Das Prinzip ist folgendes: Statt die Zugangsdaten des Nutzers stets aufs Neue anzugeben, wird ein Token vorgelegt. Von der Vorstellung her ist das Token vergleichbar mit einem Zimmerschlüssel: Die Ausgabestelle (Portier) ist getrennt von den zu nutzenden Räumen. Ebenfalls ist der Token delegierbar: Wer den Schlüssel besitzt, kann den Raum betreten und den Schlüssel auch an die Reinigungskraft weitergeben.

Lesen Sie auch: Angular 4 – das Update: Diese Features müssen Sie kennen

Mit OpenID Connect existiert ein modernes Token-basiertes Verfahren, das sich gut im Kontext von Webanwendungen einsetzen lässt. Dabei erfolgt die Anmeldung an einem Dienst – im Enterprise-Umfeld an einem eigenen zentralisierten –, dem alle beteiligten Parteien vertrauen. Ob es sich dabei um einen Social Log-in wie Google, GitHub, Facebook oder um einen eigenen Identity Provider (IdP) handelt, spielt dabei keine Rolle. Viele IdP unterstützen auch Federation, sodass der Log-in über mehrere IdP erfolgen kann. Dank Standardisierung ist das konkrete Produkt austauschbar. Für das Beispiel in diesem Artikel wird der Java-basierte Keycloak-Server verwendet.

Beispielanwendung: You shall not pass

Unsere Beispielanwendung in Angular 2.0 verfügt über drei Ansichten, die über Routen implementiert sind: Startseite, Impressum und Kontostand. In der Konfiguration des Routers wird daher jeder Route die entsprechende Komponente zugeordnet. Weiterhin wird ein Redirect angegeben, der auf die Hauptseite weiterleitet, wenn keine Route angegeben ist (Listing 1).

RouterModule.forRoot([
  { path: '', redirectTo: '/home', pathMatch: 'full' },
  { path: 'home', component: HomeComponent },
  { path: 'about', component: AboutComponent },
  { path: 'account', component: AccountComponent}
])

Die Ansicht des Kontostands soll nun geschützt werden, sodass nur berechtigte Personen darauf zugreifen können. Um ganze Bereiche einer Anwendung nur einem berechtigten Nutzerkreis verfügbar zu machen, lässt sich gut der Angular 2.0-Router verwenden. Der Router erlaubt es der Anwendung, darauf Einfluss zu nehmen, ob eine Navigationsaktion erlaubt ist oder unterdrückt werden soll. Um Navigationsaktionen zu überprüfen und gegebenenfalls zu verhindern, lassen sich Routen durch so genannte Route Guards schützten. Diese Guards gibt es in verschiedenen Ausführungen, von denen wir hier nur die einfachsten betrachten wollen: die CanActivate Guards. CanActivate ist ein Interface, das die canActivate()-Methode zur Verfügung stellt.

Diese Methode muss entweder einen booleschen Wert zurückgeben oder ein Promise bzw. Observable, das zu einem booleschen Wert auflöst. Kann der Rückgabewert zu true aufgelöst werden, ist die Navigation erlaubt und findet wie erwartet statt. Wird ein false-Wert zurückgegeben, wird die Navigation blockiert. Weiterhin ist es möglich, nach Überprüfung der Navigationsaktion auf eine andere als die angefragte Seite weiterzuleiten. Beispielsweise kann nach einer negativ ausgefallenen Überprüfung des Users dieser auf eine Log-in-Seite umgeleitet werden. Diese Funktionalität ist im LoginGuard in Listing 2 dargestellt, die Überprüfung des Users wird dabei durch den Aufruf der Methode this.authService.isLoggedIn() an den AuthService delegiert.

import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';

import { AuthService } from './auth.service';

@Injectable()
export class LoginGuard implements CanActivate{

  constructor(
    private authService: AuthService, private router: Router
  ) { }

  public canActivate(): boolean{
    if(this.authService.isLoggedIn()){
      return true;
    }
    this.router.navigateByUrl('/login');
    return false;
  }
}

Der AuthService wiederum überprüft den Log-in-Status anhand des momentanen authState (Listing 3). Ist der authState gesetzt, wird der User von Angular als eingeloggt angesehen. Der authState selbst besteht im Beispiel aus dem OpenID Connect id_token, das Informationen über den User enthält. Es kann aber auch ein access_token enthalten, das in API-Aufrufen zur Autorisierung genutzt wird.

import { Injectable } from '@angular/core';

interface AuthState{
  access_token: string;
  id_token: string;
  expires_in: number;
  token_type: string;
  "not-before-policy": number;
  session_state: string
}

@Injectable()
export class AuthService {

  private authState: AuthState;

  public isLoggedIn(): boolean{
    return !!this.authState;
  }
}

Um den Nutzer auf die Log-in-Seite umleiten zu können, muss dafür natürlich noch ein Eintrag in der Routerkonfiguration angelegt werden. Auch der LoginGuard muss eingepflegt werden, damit dieser die Accountroute vor unbeabsichtigten Zugriff beschützt bzw. die Aktivierung der Route überwacht und dem Nutzer so die Möglichkeit gibt, sich anzumelden. Der JavaScript/Angular-Client sollte nie als vertrauenswürdig angesehen werden. Alle an den Browser gesendeten Informationen sind unter Sicherheitsgesichtspunkten als öffentlich einzustufen. Die entsprechende Routerkonfiguration ist in Listing 4 zu sehen.

RouterModule.forRoot([
  { path: '', redirectTo: '/home', pathMatch: 'full' },
  { path: 'home', component: HomeComponent },
  { path: 'about', component: AboutComponent },
  { path: 'login', component: LoginComponent },
  {
    path: 'account',
    component: AccountComponent,
    canActivate: [LoginGuard]
  }
])

Klickt der bisher nicht angemeldete Nutzer nun auf den Menüpunkt ACCOUNT, leitet die Anwendung ihn zur Anmeldemaske weiter.

Sesam öffne dich …

Es gibt im Wesentlichen zwei Möglichkeiten, dem Nutzer eine Anmeldung am System zu ermöglichen. Der eine Weg ist die Nutzung der Anmeldemaske des IdPs. Dabei leitet die Anwendung, die eine Anmeldung erfordert, auf den IdP weiter. Hat sich der Nutzer dort angemeldet und der Weitergabe von nutzerbezogenen Daten an die Anwendung zugestimmt, stellt der IdP die Tokens aus und leitet den Nutzer an die Anwendung weiter. Dabei werden die Tokens als Resultat der Weiterleitung im URL transportiert und stehen der Anwendung dann zur Verfügung.

Dieser Ablauf hat den Vorteil, dass er einfach ein Single Sign-on unterstützt: Ist der Nutzer bereits mit einer Sitzung beim IdP bekannt, kann er direkt zur gewünschten Anwendung weitergeleitet werden und merkt von diesem ganzen Prozess wenig. Zudem findet die gesamte Anmeldung in einem vertrauenswürdigen Kontext statt. Der Nutzer muss seine Zugangsdaten nicht externen Anwendungen mitteilen. Allerdings verlässt der Anwender auch die Anwendung, um sich anzumelden, und verliert damit Zustand und Kontext. Hier gibt es Lösungsansätze auf der Basis von hidden Iframes. Alternativ kann die Anwendung eine eigene Log-in-Maske anbieten und sich mit den Zugangsdaten des Nutzers das passende Token vom IdP holen. Damit wird die Anwendung nicht verlassen und kann den eigenen Zustand einfach halten. Sie kennt dann aber Nutzername und Passwort. Gerade wenn es sich um eine Third-Party-Anwendung handelt, wird kein Nutzer seine Zugangsdaten dort bekannt geben wollen.

Um zu zeigen, wie eine Implementierung aussehen kann, verwendet das Beispiel in diesem Artikel eine Anmeldung durch die Angular-2.0-Anwendung mit dem OpenID Connect Implicit Flow, damit der Nutzer sich am IdP anmeldet. Die Verwaltung aller zugehörigen Informationen, wie Token, Anmeldestatus und Konfiguration des IdP, wird durch einen Angular-2.0-Service bereitgestellt, den AuthService. Durch das Verwenden eines Angular-2.0-Service können alle Teile der Anwendung auf die Informationen zugreifen: An den Stellen, an denen er benötigt wird, steht der Dienst per Dependency Injection zur Verfügung.

Um die Anmeldung zu beginnen, wird die Funktion login() aufgerufen. In der Folge wird der Nutzer dann zum IdP weiter geleitet (Listing 5). Nach der Anmeldung leitet der IdP den Nutzer zur Angular-Anwendung zurück und übermittelt die zugehörigen ID-Tokens und Access-Tokes als Teil des Ziel-URLs. Zum Auslesen der Token aus dem URL wird beim initialen Laden der Angular-Anwendung die Methode parseLoginState() aufgerufen. Falls im URL Tokens gefunden werden, werden sie im authState abgelegt, der User gilt somit als eingeloggt. Die vom OpenID-Connect-Standard vorgeschriebene Validierung der Tokens wird hier ausgelassen. Im Produktionsbetrieb müsste entweder eine Signaturprüfung des JWT im Client stattfinden oder das JWT müsste zur Validierung an den IdP geschickt werden. Zum Log-out wird die Funktion logout() genutzt, in der lokal der AuthState gelöscht wird. Eine Invalidierung der Tokens am Server wird der Einfachheit halber hier nicht implementiert.

private clientId =  'service';
private redirectUrl: string = 'http://localhost:8080/auth/realms/master/protocol/openid-connect/auth';
private requestOptions = new RequestOptions(
  {
    headers: new Headers(
      { 'Content-Type': 'application/x-www-form-urlencoded' }
    )
  }
);

constructor(private http: Http){}

public parseLoginState(){
  let oidcResponse = window.location.hash.substr(1);
  if(!oidcResponse){
    return;
  }
  let oidcParams = oidcResponse.split("&")
  let splitParams: string[]
  let data = {};
  for (var i = 0; i < oidcParams.length; i++) {
    splitParams = oidcParams[i].split("=");
    splitParams[0] = decodeURIComponent(splitParams[0]);
    splitParams[1] = decodeURIComponent(splitParams[1]);
    data[splitParams[0]] = splitParams[1];
  }
  this.authState = data;
  let split_id_token = this.authState.id_token.split('.');
  this.id_token_payload = JSON.parse(atob(split_id_token[1]));
}

public login(){
  let url = this.redirectUrl +
    "?response_type=id_token+token&scope=openid%20profile" +
    "&client_id=" +
    encodeURIComponent(this.clientId) +
    "&redirect_uri=" +
    encodeURIComponent(window.location.href) +
    "&nonce=" +
    encodeURIComponent(this.createNonce());
  location.href = url;
}

private createNonce(): string{
  //Nonce-Generierung: Statische Demoimplementierung
  let nonce = Math.floor(Math.random()*1000) + '';
  localStorage.setItem("nonce", nonce);
  return nonce;
}

public logout(){
  this.authState = null;
}

JWT

Das JSON Web Token (JWT) ist ein leichtgewichtiges, standardisiertes Tokenformat (RFC 7519) zur Durchführung von Authentifizierung oder Autorisierung. Im OpenID-Connect-Standard wird seine Verwendung für den Austausch von ID-Tokens vorgeschrieben und für den Austausch aller anderen Tokens vorgeschlagen. Für weitere Informationen zu JWT sollte man sich mit dem entsprechenden RFC beschäftigen. Einen JWT-Parser findet man unter jwt.io.

Veränderungen sichtbar machen

Nach der Anmeldung ist es dem Nutzer nun erlaubt, alle Bereiche der Anwendung zu betreten, was sich in der Darstellung der Oberfläche widerspiegeln soll. Um UI-Elemente in Abhängigkeit des Zustands anzuzeigen, kann mit *ngIf gearbeitet werden, wie in Listing 6 zu sehen: Teile des Templates für die AccountComponent werden nur angezeigt, wenn durch den AuthService festgestellt wurde, dass der User nicht eingeloggt ist. Umgekehrt wird für angemeldete Nutzer ein Log-out-Knopf zum Abmelden angezeigt.

<div *ngIf="!authService.isLoggedIn()">
  <h3>Log into your Account</h3>
  <label for="username" class="middle">Username</label>
  <input #un type="text" id="username" placeholder="demo-user">

  <label for="password" class="middle">Password</label>
  <input #pw type="password" id="password" placeholder="demo">

  <a class="button" (click)="authService.login(un.value,pw.value)">
    Login
  </a>
</div>
<div *ngIf="authService.isLoggedIn()">
  <a class="button" (click)="authService.logout()">
    Logout
  </a>
</div>

Durchgreifende Sicherheit

Der Vorteil von Tokens zeigt sich, wenn es darum geht, Delegation von Zugriffsberechtigungen zu implementieren. Im Beispiel gibt es die geschützte Ressource Account. Um Informationen über den Kontostand des Users von einer Backend-Ressource abzuholen, muss dem Backend ein Token zur Autorisierung des Users übermittelt werden. Das dafür genutzte Token ist das access_token, das bei jedem zu autorisierenden Aufruf mitgesendet werden muss. Dabei wird das Token im Authorization HTTP-Header nach dem Bearer-Schema übertragen. Im Beispiel wird der Authorization Header inklusive Token vom AuthService in Form der Methode getAuthHeaders() bereitgestellt (Listing 7).

public getAuthHeaders(): Headers{
  return new Headers(
    {'Authorization': 'Bearer ' + this.authState.access_token}
  );
}

Um nun Daten von der Account-Ressource zu laden, muss sich der AccountService lediglich den Header vom AuthService besorgen und damit die gewünschte Anfrage aufbauen. In der Methode getAccountData() ist dies exemplarisch dargestellt (Listing 8). Die Account-Ressource hat im Beispiel der URL http://localhost:9000/protected/data.

import { Injectable } from '@angular/core';
import { Http, RequestOptions } from '@angular/http';

import { AuthService } from '../auth.service';

@Injectable()
export class AccountService{

  constructor(
    private authService: AuthService, private http: Http
  ){}

  public getAccountData(): Observable{
    let requestOptions = new RequestOptions(
      { headers: this.authService.getAuthHeaders() }
    );
    return this.http.get(
      'http://localhost:9000/protected/data',
      requestOptions
    )    .map((res)=>res.json());
  }
}

Beim Umgang mit Access-Tokens sollte man im Hinterkopf behalten, dass sie aus Sicherheitsgründen meist kurzlebig sind, normalerweise im Bereich von einer Minute. Der verbleibende Gültigkeitszeitraum des Access-Tokens wird im Beispiel neben dem Access-Token selbst ebenfalls im AuthState gehalten. Bevor das alte Access-Token abläuft, sollte sich die Anwendung ein neues Access-Token besorgen, damit der User die Anwendung ohne längere Wartezeiten oder gar Neuanmeldungen weiternutzen kann. Dazu kann sich die Anwendung beispielsweise vor Ablauf eines Timers ein neues Token mithilfe einer Hidden-Iframe-Lösung holen. Dies sollte für einen produktiven Einsatz entsprechend vorgesehen werden.

Keycloak als Schlüsselmeister

Als Authentifizierungsserver wird im Beispiel Keycloak genutzt. Keycloak ist ein Java-basiertes Open-Source-Produkt. Red Hat bietet auch eine Version mit Support und Wartung als Enterprise-Version an, die jedoch auf einer älteren Keycloak-Version basiert. Wir werden in Keycloak Folgendes einrichten: Einen Client – damit wird die Nutzung der OAuth-2.0-Dienste durch eine Anwendung konfiguriert; für unser Beispiel kann das Keycloak-Access-Token für alle Anwendungen genutzt werden. Und einen User, der durch die lokale Keycloak-Nutzerverwaltung bereitgestellt wird.

Mehr zum Thema: Keycloak – Single Sign-on für Microservices und verteilte Anwendungen

Keycloak kann der Projekt-Homepage heruntergeladen oder als Docker Image mittels docker run jboss/keycloak gestartet werden. Nach der Installation von Keycloak kann man sich mit den Nutzerdaten „admin/admin“ an der Verwaltungsoberfläche von Keycloak unter http://localhost:8080/auth anmelden. Als Erstes wird ein Client mit der ID service und dem Protokoll openid-connect angelegt. Als Root URL wird http://localhost:4200/ angegeben. Hier wird später das Angular 2.0 Frontend zur Verfügung stehen (Abb. 1). Damit die Anmeldung mittels Implicit Flow möglich wird, muss dies auch auf enabled gesetzt werden.

Access Token vs. Refresh Token

Bei OAuth 2.0 gibt es die kurzlebigen Access-Tokens, die auch genutzt werden, um andere Dienste im Auftrag eines Nutzers aufzurufen, und langlebige Refresh-Tokens. Refresh-Tokens werden lediglich zum OAuth-2.0-Server geschickt, um ein frisches Access-Token zu erhalten. Somit werden Refresh-Tokens nur zwischen OAuth-2.0-Server und dem Client ausgetauscht. Eine längere Lebensdauer ist aus der Sicherheitsperspektive daher kein Problem. Beim Implicit Flow sind keine Refresh-Tokens vorgesehen, sodass das Update der kurzlebigen Access-Tokens anders umgesetzt werden muss. Hier bieten sich versteckte Iframes an, mit denen sich zusammen mit etwas JavaScript-Code ein praktikables Refresh-Verfahren implementieren lässt, und das sich zudem für den Nutzer transparent verhält.

Abb. 1: Eintragen des Frontend URLs unter der Client-ID „service“ in der Keycloak-Konfiguration; als Protokoll wird OpenID Connect verwendet

Abb. 1: Eintragen des Frontend URLs unter der Client-ID „service“ in der Keycloak-Konfiguration; als Protokoll wird OpenID Connect verwendet

Nach dem Speichern können erweiterte Einstellungen vorgenommen werden. Bei Web Origins wird durch die Stern-Wildcard der CORS-Zugriff von überall erlaubt (Abb. 2). Das ist nötig, damit der Angular-2.0-Zugriff mittels XHR/Ajax funktioniert.

Abb. 2: Hinzufügen der Stern-Wildcard in den „Web Origins“; der Root URL muss auf das Frontend angepasst werden, um die Umleitung des Nutzers nach Anmeldung am Keycloack zu gewährleisten

Abb. 2: Hinzufügen der Stern-Wildcard in den „Web Origins“; der Root URL muss auf das Frontend angepasst werden, um die Umleitung des Nutzers nach Anmeldung am Keycloack zu gewährleisten

Als Nächstes wird der User angelegt. Als Name wählen wir demo-user, setzen den User auf Enabled und Email verified, damit eine Anmeldung erlaubt ist (Abb. 3). Auch hier müssen wir nach dem Speichern noch weitere Einstellungen vornehmen.

Abb. 3: Anlegen eines Beispielnutzers

Abb. 3: Anlegen eines Beispielnutzers

Der User braucht noch ein Passwort, das unter dem Reiter CREDENTIALS gesetzt wird. Hier kann man z. B. demo eintragen und setzt bei der Gelegenheit den Schalter Temporary auf Off (Abb. 4).

Abb. 4: Setzen des Userpassworts; für Entwicklungszwecke sollte der Schalter „Temporary“ auf „Off“ gesetzt werden

Abb. 4: Setzen des Userpassworts; für Entwicklungszwecke sollte der Schalter „Temporary“ auf „Off“ gesetzt werden

Damit ist das Einrichten der Infrastruktur abgeschlossen und wir kommen zur Backend-Seite.

Keycloak-Adapter

Keycloak bringt eigene Adapter zur Integration mit Spring Boot, Angular 2.0 und weiteren Frameworks mit. Diese erlauben es dann, z. B. auf die im Keycloak vergebenen Rollen zuzugreifen und nutzen ein hidden Iframe, um ein regelmäßiges Tokenupdate durchzuführen. Zur besseren Illustration der Prinzipien wurde darauf in diesem Artikel verzichtet. Außerdem ist die vorgestellte Lösung damit weniger von dem eingesetzten konkreten Produkt Keycloak abhängig.

Schlankes Spring Boot Backend

Das Backend für die Beispielanwendung ist dank Spring Boot schlank. Keycloak verwendet als OAuth-2.0-Access-Token ein JWT. Dies lässt sich lokal verifizieren. Es muss lediglich als Token-Storage ein JwtTokenStore konfiguriert werden. In der mit @Configuration annotierten JwtConfiguration-Klasse wird dazu dem JwtAcessTokenConverter der zur Verifikation der JWT-Tokens nötige öffentliche RSA-Schlüssel hinzugefügt (Listing 9).

@Configuration
public class JwtConfiguration
{
  @Value("${security.oauth2.resource.jwt.keyValue}")
  String publicKey;
  
  @Autowired
  JwtAccessTokenConverter jwtAccessTokenConverter;

  @Bean
  protected JwtAccessTokenConverter jwtAccessTokenConverter()
  {
    JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
    converter.setVerifierKey(publicKey);
    return converter;
  }

  @Bean
  @Qualifier("tokenStore")
  public TokenStore tokenStore()
  {
    return new JwtTokenStore(jwtAccessTokenConverter);
  }
}

Den benötigten öffentlichen Schlüssel bekommt man von Keycloak unter Realm Settings im Reiter KEYS (Abb. 5).

Abb. 5: Abfrage des öffentlichen RSA-Schlüssels im Keycloack unter „Realm Settings | Keys“

Abb. 5: Abfrage des öffentlichen RSA-Schlüssels im Keycloack unter „Realm Settings | Keys“

Die Konfiguration erfolgt wie üblich über die application.yml. Hier werden der öffentliche Schlüssel und auch der Name der Ressource gesetzt. Als Name verwenden wir denselben Namen, den auch der angelegte Client hat. Listing 10 zeigt die Konfiguration mit gekürztem Schlüssel.

---
spring:
  profiles: default
server:
    port: 9000
security:
  oauth2:
    client:
      clientId: service
    resource:
      userInfoUri: http://localhost:8081/auth/realms/master/protocol/openid-connect/userinfo
      jwt:
        keyValue:
          -----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAy5b3tTKsQ2tsCIbYuu8P0+B5sUXUBx+6FsW/H+HuA4bpFAqPD7YMcECAwEAAQ==
          -----END PUBLIC KEY-----
      id: backend
serviceId: ${PREFIX:}resource

Die Absicherung der Ressourcen erfolgt mit Spring Security. Hier ist darauf zu achten, dass für CORS Requests die Http.Method.OPTIONS auch ohne Authentifizierung erlaubt werden. Listing 11 zeigt die Konfiguration für Security und die Aktivierung des vorher konfigurierten Token-Stores zur JWT-Token-Validierung.

@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter
{
  @Value("${security.oauth2.resource.id}")
  String resourceId;

  @Autowired
  TokenStore tokenStore;

  @Override
  public void configure(HttpSecurity http) throws Exception
  {
    http
      .csrf().disable()
      .authorizeRequests()
      .antMatchers(HttpMethod.OPTIONS).anonymous() //CORS requests
      .antMatchers("/protected/**").authenticated()
      .antMatchers("/**").anonymous();
  }

  @Override
  public void configure(ResourceServerSecurityConfigurer serverSecurityConfigurer) throws Exception
  {
    serverSecurityConfigurer.resourceId("service").tokenStore(tokenStore);
  }
}

Fazit

Dieser Artikel zeigt, wie der OpenID-Connect- und OAuth-2.0-Standard sowohl für ein Angular-2.0-Frontend als auch für eine Java-Spring-Anwendung implementiert werden kann. Mit einer dedizierten Anwendung wie Keycloak lassen sich die Verantwortlichkeiten für Authentifizierung und Autorisierung in einer verteilten Anwendungslandschaft sicher und robust implementieren. Die Beispielanwendung ist auf GitHub zu finden.

Im Rahmen des Artikels wurden einige Details ausgelassen, die für ein produktives Deployment relevant sein können. Dazu gehören beispielsweise verschiedene Zugriffsrechte, Mapping von Rollen zu Rechten sowie Management von Schlüsseln und Zertifikaten. Auf die Angular-Services bezogen sollte vor allem auf Testbarkeit und die korrekte Umsetzung der verschiedenen OpenID-Connect-Szenarien geachtet werden, Implicit Flow.

Im Beispiel findet keine korrekte Prüfung des Identity-Tokens statt, auch der zufällige Nonce-Wert wird nicht dahingehend überprüft, dass er zwischen Aufruf und Antwort unverändert ist. Weiterhin sollte eine Logik zum Token-Refresh bedacht werden. Statt dies wiederholt selbst zu implementieren, sollte für einen produktiven Einsatz entweder eine eigene Auth-Library entworfen oder eine bestehende Library genutzt werden, z. B. vom Identity-Provider-Hersteller.

Mehr zum Thema:

Angular Tutorial: Rascher Projektstart mit dem Angular CLI

Verwandte Themen:

Geschrieben von
Karsten Sitterberg
Karsten Sitterberg
Karsten Sitterberg ist als freiberuflicher Entwickler, Trainer und Berater für Java und Webtechnologien tätig. Karsten ist Physiker (MSc) und Oracle-zertifizierter Java Developer. Seit 2012 arbeitet er mit trion zusammen.
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.