Angular Security - Teil 1

Hier kommst du nicht rein: Authentifizierung und Autorisierung mit Angular

Manfred Steyer, Hans-Peter Grahsl

© iStockphoto.com / Maximkostenko

Mit den Standards OAuth 2.0 und OpenID Connect lassen sich flexible Authentifizierungs- und Autorisierungslösungen auf der Basis von Security-Tokens entwickeln. Sie erlauben die Integration bestehender Identity-Lösungen wie Active Directory sowie die Nutzung zentraler Benutzerkonten für unterschiedliche Anwendungen.

Die wenigsten Geschäftsanwendungen kommen ohne Authentifizierung aus. Häufig müssen bestehende Identity-Lösungen wie Active Directory oder LDAP-Systeme integriert werden, um Single Sign-on zu ermöglichen. In modernen Webanwendungen muss der Client auch das Recht erhalten, im Namen des angemeldeten Benutzers auf Services zuzugreifen. All diese Anforderungen lassen sich elegant mit Security-Tokens lösen.

Dieser Artikel zeigt anhand eines Beispiels, wie tokenbasierte Sicherheit in einer Angular-Anwendung genutzt werden kann. Dazu kommen neben einer Angular-Anwendung ein auf Spring Boot basierendes Web-API und die zertifizierte Identity-Lösung Keycloak aus der Feder von Red Hat zum Einsatz. Der gesamte Quellcode findet sich unter diesem und diesem Link.

Angular im Videotutoriallogo-entwickler tutorials

Im entwickler.tutorial Angular – eine Einführung bereitet Sie Manfred Steyer auf die Entwicklung mit dem Angular Framework vor. Steyer zeigt anhand eines praxisnahen Beispiels, wie man Services und Dependency Injection integriert, Pipes zur Formatierung von Ausgaben nutzt, Komponenten mit Bindings verwendet und professionell mit dem Angular CLI umgeht.

Jetzt anmelden!

 

Artikelserie

Teil 1: Authentifizierung und Autorisierung mit Angular 2.0

Teil 2: Wiederverwendbare Pakete

Teil 3: Test und Build

Wer sich heutzutage mit tokenbasierter Sicherheit beschäftigt, kommt wohl kaum an den beiden populären Standards OAuth 2.0 und OpenID Connect vorbei. Sie beschreiben unter anderem, wie sich ein Benutzer bei einem verteilten System anmelden kann und wie ein Client das Recht erhält, im Namen des Benutzers Services zu konsumieren. Dazu kommt, dass diese Standards direkt auf HTTPS aufsetzen und sich somit wunderbar für leichtgewichtige Web-APIs eignen. Abbildung 1 verdeutlicht die Funktionsweise von OAuth 2.0 aus der Vogelperspektive.

Der Client leitet den Benutzer zur Anmeldung zu einem so genannten Authorization-Server weiter. Diese Instanz hat Zugriff auf zentrale Benutzerkonten. Hat sich der Benutzer dort angemeldet, erhält der Client ein so genanntes Access-Token, das ihm im Namen des Benutzers Zugriff auf Services im Backend gibt, so genannte Resource-Server.

Abb. 1: Funktionsweise von OAuth 2.0 aus der Vogelperspektive

Ein Access-Token informiert den Resource-Server unter anderem über den entsprechenden Benutzer sowie über die Rechte, die der Client im Namen des Benutzers wahrnehmen darf. Zusätzlich finden sich im Token meist auch Metadaten, wie der Aussteller, das Ausstellungsdatum oder die Gültigkeitsdauer. Diese vom Prinzip her einfache Vorgehensweise hat mehrere Vorteile. Jeder Benutzer kann ein zentrales Benutzerkonto für verschiedene Clients und Services nutzen. Da die Anmeldung beim Authorization-Server erfolgt, erhält der Client das Passwort nicht.

Die Authentifizierung ist vom Client entkoppelt und lässt sich somit in bestehende Identity-Lösungen integrieren. Tokens erhöhen außerdem die Flexibilität. Beispielsweise könnte ein Service das Token an einen weiteren Service weiterreichen, um zu beweisen, dass er im Namen des Benutzers agiert. Zum Zugriff auf anderen Sicherheitsdomänen kann der Service das Token auch gegen eines für diese Domäne tauschen. Die Lösung kommt ohne Cookies aus. Somit kann der Client auch auf Services zugreifen, die auf anderen Servern laufen oder eine andere Origin haben. Zusätzlich schränkt der Verzicht auf Cookies bestimmte Angriffe ein.

Das Format des Access-Tokens sowie die Maßnahmen, die der Resource-Server zum Validieren des Tokens unternimmt, sind von OAuth 2.0 nicht näher beschriebene Implementierungsdetails. Häufig kommen digitale Signaturen zum Einsatz, damit der Resource-Server einfach prüfen kann, ob das Token von einem vertrauenswürdigen Authorization-Server stammt. Alternativ dazu könnte das Token auch nur aus einer nicht vorhersehbaren ID bestehen, mit welcher der Resource-Server sich erneut an den Authorization-Server wendet.

Benutzer mit OpenID Connect authentifizieren

Als Ergänzung zu OAuth 2.0 definiert OpenID Connect (OIDC) unter anderem, wie der Client Informationen über den Benutzer bekommen kann. Diesen Aspekt deckt OAuth 2.0 nicht ab. Selbst das ausgestellte Token muss für den Client nicht lesbar sein. Dazu spezifiziert OIDC unter anderem ein so genanntes ID-Token, das der Client zusätzlich zum Access-Token erhalten kann. Während das Access-Token zum Zugriff auf das Backend bestimmt ist, kann der Client aus dem ID-Token direkt Informationen über den Benutzer entnehmen (Abb. 2).

Abb. 2: Funktionsweise von OpenID Connect aus der Vogelperspektive

Im Gegensatz zu Access-Tokens bei OAuth 2.0 ist der Aufbau von ID-Tokens vorgegeben. Es handelt sich dabei immer um ein JSON Web Token (JWT), das signiert und verschlüsselt sein kann. Zusätzlich definiert OIDC einen User-Info-Endpunkt. Dabei handelt es sich um einen Service, der dem Client weitere Informationen zum aktuellen Benutzer verrät, sofern er das erhaltene Access Token vorweisen kann.

Daten sicher übertragen mit JSON Web Tokens

Bei den von OpenID Connect verwendeten JWTs handelt es sich um einen offenen Standard der IETF, der ein kompaktes und in sich geschlossenes Containerformat zur sicheren Übertragung von Daten beschreibt. JSON-Objekte können dabei durch kryptografische Verfahren und gängige Algorithmen digital signiert und bei Bedarf auch verschlüsselt werden. Während JWTs vielfältige Einsatzzwecke abdecken können, werden sie häufig im Kontext von Authentifizerung und Authorisierung zum Informationsaustausch entitätsbezogener Daten verwendet. Ein signiertes und unverschlüsseltes JWT besteht aus den folgenden drei Teilen:

  • Header: ein JSON-Objekt, das zumindest den Typ des Tokens und den verwendeten Algorithmus zum Signieren beinhaltet sollte.
  • Payload: ein JSON-Objekt mit so genannten Claims, die Fakten über eine Entität (zumeist Benutzer) sowie zusätzliche Metadaten enthalten. Unterschieden werden dabei reservierte, öffentliche und private Claims.
  • Signature: eine digitale Signatur, die mit dem im Header angeführten Algorithmus über die Vereinigung von Header und Payload berechnet wird, basierend auf einem zuvor geteilten Secret oder privaten Schlüssen.

Header, Payload und Signature werden jeweils Base64-Safe-URL-codiert und mit einem Punkt getrennt konkateniert. Zur Illustration des beschriebenen Aufbaus dient ein beispielhaftes JWT in Listing 1.

HEADER
{"alg": "HS256","typ": "JWT"}
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

PAYLOAD
{"sub": "ABCDEF-123456","aud": "JavaMagazinDemo","iss": "https://..."}
eyJzdWIiOiJBQkNERUYtMTIzNDU2IiwiYXVkIjoiSmF2YU1hZ2F6aW5EZW1vIiwiaXNzIjoiaHR0cHM6Ly8uLi4ifQ

SIGNATURE (Secret = secret)
6VNJFCG-n8CCCgy6fm4iwuo-pYY3944kRAnKz5b1bzo

JWT
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJBQkNERUYtMTIzNDU2IiwiYXVkIjoiSmF2YU1hZ2F6aW5EZW1vIiwiaXNzIjoiaHR0cHM6Ly8uLi4ifQ.6VNJFCG-n8CCCgy6fm4iwuo-pYY3944kRAnKz5b1bzo

Unter dieser Adresse können JWTs für Testzwecke anschaulich decodiert, verifiziert und auch generiert werden. Weiter wird eine Browserintegration in Form eines Chrome Plug-ins zur komfortablen Handhabung von JWTs im Zuge des Debuggings angeboten.

Go with the Flow

Für verschiedene Anwendungsfälle definieren OAuth 2.0 und OIDC so genannte Flows, die festlegen, welche Nachrichten auszutauschen sind, damit der Client die erwähnten Tokens erhält. Für Single Page Applications kommen zwei Flows in Frage: der Implicit Flow und der Resource Owner Password Credentials Flow. Der Implicit Code Flow (Abb. 3) ist für Clients geschaffen, die nicht in der Lage sind, ein geteiltes Secret sicher zu verwahren, wie alle JavaScript-Browseranwendungen.

Wie mit der eingangs präsentierten Übersicht gezeigt, sendet der Client eine Autorisierungsanfrage an den Authorization-Server, der auf einer eigenen, von ihm kontrollierten Seite den Benutzer zur Authentifizierung auffordert. Nach Eingabe der Zugangsdaten kann ein optionaler Zwischenschritt erfolgen, bei dem der Benutzer seine explizite Zustimmung erteilt, die vom Client angeforderten Rechte an ihn zu übertragen. Danach erfolgt eine Umleitung zurück zum Client, im Zuge derer auch ein ID-Token (Benutzerinformation) und ein optionales Access Token (Zugangsrechte) übermittelt werden. Der Client prüft das ID-Token (JWT) auf Plausibilität und Gültigkeit. Mit dem Access-Token können schließlich abgesicherte Bereiche und Ressourcen eines Web-API konsumiert werden.

Abb. 3: Implicit Code Flow

Resource Owner Password Credentials Flow

Der Resource Owner Password Credentials Flow ist Teil der in OAuth 2.0 definierten Flows und wird in der OIDC-Spezifikation nicht explizit erwähnt. Nachdem dieser Flow strenggenommen die Grundidee von OAuth 2.0 untergräbt – Clients keinerlei Zugangsinformationen von Benutzern offenzulegen – sollte er ausschließlich zum Einsatz kommen, sofern ein starkes Vertrauensverhältnis zwischen Benutzern und Clients vorherrscht.

Im Unterschied zum Implicit Flow geben Benutzer in diesem Fall ihre Zugangsinformationen direkt dem Client preis, der an den Authorization-Server delegiert und im Zuge dessen die Authentifizierung und Autorisierung von Benutzern erfolgt. Während der Implicit Flow öffentlichen Clients zur Verfügung steht, muss der Client in diesem Fall beim Authorization Server registriert sein. Er muss sich selbst über ein im Vorfeld geteiltes Secret authentifizieren können, damit Tokens erfolgreich ausgestellt werden können (Abb. 4).

Abb. 4: Resource Owner Password Credentials Flow im Überblick

Identity und Access Management mit Keycloak

Wenn es um die Absicherung von Web-APIs geht, bieten sich viele Alternativen. Der Fokus dieses Artikels liegt auf Keycloak, einer im Unternehmensumfeld flexibel einsetzbaren und gut dokumentierten Lösung fürs Identity- und Access-Management. Keycloak ist ein Open-Source-Projekt der Red-Hat-Community, das von der OpenID Foundation im Herbst letzten Jahres zertifiziert wurde und viele Funktionalitäten bietet, um es z. B. als zentrale Single-Sign-on/out-Lösung für Webanwendungen im Unternehmenskontext zu etablieren. Neben einer eigenständigen, vollwertigen Benutzerverwaltung kann Identity Brokering mittels Social Log-ins in Google oder Twitter ebenso einfach realisiert werden wie ein User-Federation-Ansatz zur Authentifizierung von Benutzern mit Active Directory oder LDAP – alles quasi out of the box.

Lesen Sie auch: Flexibel sein mit Keycloak – Single Sign-on für Microservices und verteilte Anwendungen

Keycloak setzt dabei auf Standards wie OpenID Connect, OAuth 2 und SAML. Um die Clientintegration noch ein Stück weit zu erleichtern, liefert das Open-Source-Projekt eine Reihe an Clientadaptern für verschiedene Plattformen und Sprachen mit. Eine detaillierte Betrachtung rund um Aufbau und Funktionsumfang von Keycloak findet sich in einer früheren Ausgabe des Java Magazins [1]. Im vorliegenden Artikel werden hingegen die grundlegenden Aspekte betrachtet, die zur tokenbasierten Absicherung eines einfachen Web-APIs nötig sind. Nach dem Download und Entpacken der Keycloak-Stand-alone-Server-Distribution lässt sich Keycloak aus dem Unterverzeichnis bin/ mit standalone(.bat|.sh)-Skript lokal auf Defaultport 8080 starten. Sofern Änderungen an der Startkonfiguration benötigt werden, können sie unter standalone/configuration/standalone.xml vorgenommen werden. Öffnet man nach dem erstmaligen Start im Browser die Keycloak-Einstiegsseite (http://localhost:8080/auth), erfolgt die Aufforderung, zunächst einen Administratoraccount anzulegen. Sobald er existiert, ist eine Anmeldung über die Administration Console (http://localhost:8080/auth/admin/) möglich.

Zur Absicherung der Flights Web-APIs wird im ersten Schritt ein eigener, so genannter Realm im Administrationsbereich erstellt. Dazu im Menü links oben auf Master | Add Realm klicken und den Namen definieren – für dieses Beispiel angular-spring. Realms stellen innerhalb von Keycloak unterschiedliche, voneinander isolierte Sicherheitsbereiche dar und bieten die Möglichkeit, Benutzer, Rollen, Gruppen sowie Applikationen oder Clients darin zu verwalten.

Für das Beispiel dieses Artikels werden letztlich drei Clients im neu definierten angular-spring-Realm konfiguriert, um sowohl die beiden Flows für das Angular Frontend als auch das Spring Backend zu adressieren. Für das Spring Backend wird ein Client mit der Client-ID spring-webapi erstellt (Abb. 5). Dazu links im Menü auf Clients wechseln, die ID eintragen und mit Create Button bestätigen. Im Tab Settings für diesen neuen Client sollte man unter Access Type „Bearer only“ auswählen. Diese Einstellung bedeutet, dass deratige Clients als Web Service agieren und nur Requests zulassen, die sich mit einem gültigen Bearer-Access-Token im HTTP-Authorization-Header ausweisen können. Die Konfiguration sollte mit Save Button gespeichert werden.

Abb. 5: Back-Client konfigurieren

Nun wird ein zweiter Client mit der Client-ID angular-app-1 für den Implicit Flow der Frontend-Applikation registriert (Abb. 6). Im Tab Settings für diesen Client sind ein paar mehr Konfigurationen zu tätigen, je nachdem, welche OIDC/OAuth 2.0 Flows benötigt werden. Als Protokoll wird openid-connect definiert. Der Access Type ist public, nachdem es sich beim Client um eine Single Page Application im Browser handeln wird, die ein Client Secret nicht sicher verwahren kann. Für diesen Client wird Implicit Flow aktiviert.

Wichtig sind noch die Einstellungen für die beiden Optionen Valid Redirect URIs und Web Origins, womit im Endeffekt bestimmt werden kann, von welchen domainfremden Hosts Anfragen erlaubt sein sollen. Für Demozwecke wird hier jeweils * als Wildcard verwendet, um Anfragen von jeglichen Hosts zu akzeptieren. In realen Projekten sollte an dieser Stelle unbedingt restriktiv auf die betreffenden URIs eingeschränkt werden. Durch die Aktivierung der Option „Consent Required“ wird der jeweilige Benutzer im Rahmen der Authentifizierung explizit um seine Zustimmung gefragt, ob die angeforderten Rechte auch tatsächlich an die Clientapplikation übertragen werden sollen. Auch diese Konfiguration ist mit Save Button zu speichern.

Abb. 6: Frontend-Client 1 konfigurieren

Der dritte Client mit der Client ID angular-app-2 bedient den Password Credentials Flow der Frontend-Applikation. Als Access Type und Protocol werden dieselben Einstellungen wie vorhin gewählt. Zur Unterstützung dieses Flows ist die Option „Direct Access Grants Enabled“ zu aktivieren. Die Option „Consent Required“ bleibt in diesem Fall deaktiviert, weil der Password Credentials Flow keine direkte Möglichkeit für die explizite Zustimmung der Rechtedelegation seitens des Benutzers bietet (Abb. 7).

Abb. 7: Frontend-Client 2 konfigurieren

Für Testzwecke werden dem angular-spring Realm noch eine Rolle und ein Benutzer hinzugefügt. Unter Roles | Add Role wird eine Rolle namens flightapi_user erstellt, die die Berechtigung zur Verwendung des Flight Web API repräsentiert. Die Option „Scope Param Required“ soll aktiviert werden, womit sichergestellt ist, dass diese Berechtigung nur dann erteilt wird, wenn der Client den Rollennamen als Scope-Parameter in seiner Anfrage anführt. Abschließend wird unter Users | Add User ein Benutzer erstellt. Neben dem Pflichtfeld „Username“ sollten auch die Felder „E-Mail“ sowie „First & Last Name“ ausgefüllt werden. Nach dem Speichern kann im Credentials Tab ein Passwort gesetzt werden.

Dabei sollte die Option „Temporary“ deaktiviert und mit „Reset Password Button“ bestätigt werden. Ebenso wird dem neuen Benutzer im Role Mappings Tab die zuvor angelegte Rolle flightapi_user zugeordnet, die die Berechtigung – in Form eines Claims im Access Token – beim Zugriff für die spätere Verwendung des abzusichernden Web-API symbolisiert. Die gesamte Realm-Konfiguration wird als JSON-Datei im GitHub Repository bereitgestellt, um sämtliche hier beschriebenen Einstellungen einfach importieren zu können.

Ungesichertes Spring Boot Web-API

Ausgangspunkt für die Absicherung des Backends mittels OIDC/OAuth 2.0 ist eine simpel gestricktes, auf Spring Boot basierendes Web-API, das es konsumierenden Clients ermöglichen soll, nach Flügen zu suchen. Eine detaillierte Beschreibung findet sich im Readme des GitHub Repositorys. Daher soll an dieser Stelle nur ein kurzer Überblick über die ungesicherte Anwendung gegeben werden. Das Domänenmodell des Web-API besteht lediglich aus einer einzigen Entitätsklasse namens Flight, die Attribute von Flügen kapselt. Als Repository für die Flight-Entitäten fungiert ein Spring Data CrudRepository. Wir deklarieren dafür lediglich ein paar verschiedene find*-Methoden, um Flüge basierend auf unterschiedlichen Suchangaben finden zu können.

Lesen Sie auch: Spring-Boot-Anwendungen überwachen – das Tutorial

Nachdem sich in den Maven-Abhängigkeiten des Projekts bereits Spring Boot Starter Data JPA sowie H2 als Datenbank befinden, ist die einfache In-memory-Persistenz für Flight-Entitäten damit bereits verwendbar. Clientanwendungen benötigen entsprechende HTTP-Endpunkte, um das Flights-Web-API zu konsumieren. Diese werden über einen Spring Controller, genauer gesagt @RestController, bereitgestellt, der das Flight Repository verwendet, um Suchanfragen zu Flügen beantworten zu können. Die Spring-Boot-Applikation bietet zunächst noch keinerlei Absicherungsmaßnahmen. Daher ist das Backend von beliebigen Clients verwendbar.

Absicherung des Web-API

Für die Absicherung des Flights-Web-API mit Keycloak kann entweder der bereitgestellte Spring-Security-Adapter oder der Spring-Boot-Adapter verwendet werden. Letzterer ist aufgrund bestimmter Spring-Boot-Konventionen und automatischer Konfigurationen komfortabler und erfordert weniger manuellen Aufwand bei der Integration. Um den Spring-Boot-Adapter ins Projekt zu integrieren, werden zunächst die benötigten Maven-Abhängigkeiten eingefügt:

<dependency>
  <groupId>org.keycloak</groupId>
  <artifactId>keycloak-spring-boot-adapter</artifactId>
  <version>2.4.0.Final</version>
</dependency>
<dependency>
  <groupId>org.keycloak</groupId>
  <artifactId>keycloak-tomcat8-adapter</artifactId>
  <version>2.4.0.Final</version>
</dependency>

Der Keycloak-Adapter selbst kann einfach und wie gewohnt in der application.properties-Datei (alternativ auch im YAML-Format application.yml) wie in Listing 2 konfiguriert werden.

 
keycloak.cors = true
keycloak.realm = angular-spring
keycloak.auth-server-url = http://localhost:8080/auth
keycloak.bearer-only = true
keycloak.resource = spring-webapi

keycloak.securityConstraints[0].securityCollections[0].name = secured controller
keycloak.securityConstraints[0].securityCollections[0].authRoles[0] = flightapi_user
keycloak.securityConstraints[0].securityCollections[0].patterns[0] = /api/flight/*

Im ersten Konfigurationsblock werden die Basiseinstellungen definiert. Dazu gehören der Realm-Name (angular-spring), der Auth-Server-URL, die Client-ID (spring-webapi) des Backends und der Access Type (bearer-only). Danach erfolgt die eigentliche Absicherung von bestimmten URL-Mustern (patterns) mit den dafür benötigten Berechtigungen (authRoles). Wichtig ist an dieser Stelle der Hinweis, dass sämtliche URL-Muster, die nicht auf die konfigurierten passen, standardmäßig ungesichert sind.

In diesem Beispiel sind sämtliche Endpunkte des Flight Controllers (/api/flight/*) vor unautorisierten Clientzugriffen geschützt. Nur für Anfragen mit der entsprechenden Berechtigung (flightapi_user) werden Daten ausgeliefert. Keycloak prüft dabei, ob sich ein gültiges Bearer-Access-Token in Form eines JSON Web Tokens im Authorization-Header des HTTP Requests befindet. Das Access-Token muss außerdem den Realm_Access.Roles Claim flightapi_user aufweisen, damit der Zugriff auf das Flight-Web-API genehmigt wird. Im Fehlerfall wird zwischen zwei HTTP-Statuscodes unterschieden: 401 (Unauthorized) bei fehlendem oder ungültigem JWT Bearer Token im Authorization Header oder 403 (Forbidden) bei nicht vorhandenen Claims und damit fehlenden Berechtigungen.

Angular konfigurieren

Um den Einsatz der beiden vorgestellten Flows zu demonstrieren, bietet die hier genutzte Angular-Anwendung auch zwei Arten der Anmeldung. Der Benutzer kann sich zur Anmeldung somit entweder zum Authorization-Server umleiten lassen oder dem Client sein Passwort direkt anvertrauen.

Für die clientseitige Umsetzung der beiden Flows nutzt der Client die Bibliothek angular-oauth2-oidc, die der Autor über npm zur Verfügung gestellt hat: npm install angular-oauth2-oidc –save. Um die Bibliothek nach dem Herunterladen der Angular-Anwendung bekannt zu machen, ist das OAuthModule-Root-Modul zu importieren (Listing 3).

 

import { OAuthModule } from 'angular-oauth2-oidc';

@NgModule({
  imports: [
    [...]
    OAuthModule.forRoot()
  ],
    [...]
})
export class AppModule {
}

Danach ist die Bibliothek mit Eckdaten zum registrierten Client sowie zum Authorization-Server zu registrieren. Die hier betrachtete Implementierung übernimmt diese Aufgabe im Konstruktor der AppComponent. Dazu lässt sie sich den OAuthService injizieren und hinterlegt die benötigten Informationen in ihren Eigenschaften (Listing 4).

import { Component } from '@angular/core';
import { OAuthService } from 'angular-oauth2-oidc';

@Component({
  selector: 'flight-app',
  templateUrl: './app.component.html'
})
export class AppComponent {

  constructor(private oauthService: OAuthService) {

    // Für den Client registrierte Id
    this.oauthService.clientId = "angular-app-1";

    // Url des Angular-Clients, an die das Token zu senden ist
    this.oauthService.redirectUri = window.location.origin + "/index.html";

    // Rechte, die der Client wahrnehmen möchte
    this.oauthService.scope = "openid profile email flightapi_user";

    // Definieren, dass auch ein Id-Token abgerufen werden soll
    this.oauthService.oidc = true;

    // Festlegen, ob Tokens im localStorage oder im sessionStorage zu speichern sind
    this.oauthService.setStorage(sessionStorage);

    let url = 'https://hpg-keycloak.northeurope.cloudapp.azure.com/auth/realms/angular-spring/.well-known/openid-configuration';
    this.oauthService.loadDiscoveryDocument(url).then((doc) => {
      // Eventuelle Tokens aus Url entnehmen
      this.oauthService.tryLogin({});
    });
  }
}

Zu diesen Informationen gehören die ID des Clients sowie dessen URL, an den die Token beim Implicit Flow zu senden sind. Aus Sicherheitsgründen müssen diese beiden Informationen im Vorfeld beim Authorization-Server registriert werden. Auf diese Weise stellt er sicher, dass tatsächlich der Client mit der angegebenen ID und somit jener Client, für den sich der Benutzer anmeldet, die Tokens erhält. Der Scope repräsentiert die einzelnen Berechtigungen, die der Client im Namen des Benutzers durchführen möchte. Die ersten drei hier definierten Werte stammen aus der Welt von OpenID Connect. Sie erlauben Zugriff auf die Benutzer-ID (openid), Profilinformationen wie Vorname und Nachname (profile) und die E-Mail-Adresse (email) des Benutzers. Der vierte Scope (flightapi_user), der im Authorization-Server zu definieren ist, ist Use-Case-spezifisch und ermöglicht Zugriff auf das Spring-Boot-basierte Web-API.

Weitere Eckdaten bezieht der Client über das Discovery-Dokument, das der Authorization-Server bereitstellt. Dabei handelt es sich um ein durch OpenID Connect definiertes JSON-Dokument, das unter anderem die einzelnen Endpunkte zum Anfordern von Tokens oder Benutzerinformationen widerspiegelt. Per Definition findet es sich unter jenem URL, der sich ergibt, wenn man an den URL des Authorization-Servers die Segmente .well-known/openid-configuration anhängt. In Fällen, bei denen der Authorization-Server der Wahl kein solches Dokument anbietet, nimmt die Bibliothek die einzelnen Einstellungen auch über Eigenschaften entgegen. Informationen dazu finden sich in der Dokumentation.

Der Aufruf von tryLogin prüft gleich zu Programmstart, ob sich in dem URL Tokens befinden. Das ist der Fall, wenn der Authorization-Server den Benutzer am Ende des Implicit Flows wieder zur Anwendung umleitet. Sind Tokens vorhanden, entnimmt tryLogin diese aus dem URL und validiert sie. Anschließend verstaut der OAuthService die extrahierten Tokens im konfigurierten Storage. Entsprechend der betrachteten Konfiguration kommt hierzu der sessionStorage zum Einsatz, der Daten für die Dauer einer Benutzersitzung vorhält.

Log-in mit Implicit Flow

Ist die Bibliothek konfiguriert, gestaltet sich die weitere Vorgehensweise einfach. Um den Implicit Flow anzustoßen, ist lediglich die Methode initImplicitFlow beim OAuth-Service aufzurufen (Listing 5).

Listing 5: „initImplicitFlow“ über OAuth-Service aufrufen
export class HomeComponent {

  [...]

  constructor(private oauthService: OAuthService) {
  }

  login() {
    this.oauthService.initImplicitFlow();
  }

  logout() {
    this.oauthService.logOut();
  }

  get givenName() {
    var claims = this.oauthService.getIdentityClaims();
    if (!claims) return null;
    return claims.given_name;
  }
  […]
}

Für das Abmelden steht die Methode logOut zur Verfügung. Sie löscht die im Storage hinterlegten Tokens und leitet den Benutzer zu einem Logout-URL des Authorization-Servers um. Auf diese Weise erfährt auch dieser, dass sich der Benutzer abgemeldet hat. Um Informationen über den Benutzer in Erfahrung zu bringen, ruft der Client die Methode getIdentityClaims auf. Diese enthält die Claims aus dem ID-Token, die durch OpenID Connect definiert sind. Bei Bedarf lassen sich zusätzliche Claims beim Authorization-Server registrieren.

Log-in mit Resource Owner Password Credentials Flow

Über den Resource Owner Password Credentials Flow lässt sich ähnlich einfach ein Access-Token beziehen. Die Bibliothek angular-oauth2-oidc stellt hierzu die Methode fetchTokenUsingPasswordFlowAndLoadUserProfile zur Verfügung (Listing 6).

userName: string;
password: string;
loginFailed: boolean = false;

loginWithPassword() {

  this.oauthService.clientId = "angular-app-2";

  this
    .oauthService
    .fetchTokenUsingPasswordFlowAndLoadUserProfile(this.userName, this.password)
    .then(() => {
      console.debug('successfully logged in');
      this.loginFailed = false;
    })
    .catch((err) => {
      console.error('error logging in', err);
      this.loginFailed = true;
    })
    .then(() => {
      this.oauthService.clientId = "angular-app-1";
    });
}

Wie der Name vermuten lässt, kümmert sie sich um zwei Dinge: Zum einen tauscht sie Benutzername und Passwort gegen ein Access-Token ein. Zum anderen ruft sie Daten über den Benutzer ab. Für Letzteres kommt der durch OpenID Connect definierte User-Info-Endpunkt zum Einsatz. Das explizite Abfragen dieser Daten ist notwendig, da die Spezifikationen beim Einsatz des Resource Owner Password Credentials Flows kein ID-Token vorsehen. Das erhaltene Access-Token kommt zur Authentifizierung beim User-Info-Endpunkt zum Einsatz. Die auf diese Weise erhaltenen Informationen behandelt die Bibliothek wie jene aus dem ID-Token und stellt sie auch über die oben besprochene Methode getIdentityClaims zur Verfügung.

Zugriff auf das Web-API mit Access Token

Auch der Zugriff auf das Web-API gestaltet sich einfach. Hierzu ist lediglich das Access-Token über den Authorization-Header in der Form Authorization: Bearer …Token… an den Server zu übersenden wie in Listing 7.

@Injectable()
export class FlightService {

  constructor(
    private oauthService: OAuthService,
    private http: Http,
    @Inject(BASE_URL) private baseUrl: string
  ) {
  }

  public flights: Array<Flight> = [];

  find(from: string, to: string): void {

    let url = this.baseUrl + "/api/flight";

    let headers = new Headers();
    headers.set('Accept', 'application/json');
    headers.set('Authorization', 'Bearer ' + this.oauthService.getAccessToken());

    let search = new URLSearchParams();
    search.set('from', from);
    search.set('to', to);

    this
      .http
      .get(url, {headers, search})
      .map(resp => resp.json())
      .subscribe(
        (flights) => { 
          this.flights = flights; 
        },
        (err) => { 
          console.warn('status', err.status); 
        }
      );
  }

}

Wichtig ist es hier auch, auf einen eventuellen Fehler zu reagieren, den das Web-API zurückmeldet. Das betrachtete Listing deutet das an, indem es den Fehlercode ausgibt. Besonders die Fehler 401 (Unauthorized) und 403 (Forbidden) sind im Fall der Zugriffskontrolle zu beachten. Ersterer gibt darüber Auskunft, dass der aktuelle Benutzer nicht bekannt ist, und Letzterer informiert darüber, dass der Benutzer nicht die nötigen Berechtigungen aufweist.

War der Benutzer zuvor bereits angemeldet, könnten diese Fehler darauf hinweisen, dass das Token nicht mehr gültig und die Benutzersitzung abgelaufen ist. In beiden Fällen könnte man den Benutzer – z. B. mit dem Router von Angular – auf die Log-in-Seite umleiten und dort eine entsprechende Information ausgeben. Damit die Anwendung prüfen kann, ob gültige Tokens vorliegen, stellt der OAuth-Service die Methoden hasValidIdToken und hasValidAccessToken zur Verfügung. Im Zug des Routings könnte die Anwendung mit einem so genannten Guard damit prüfen, ob der Benutzer geschützte Bereiche der Anwendung nutzen darf. Informationen dazu finden sich unter folgendem Link.

Fazit und Ausblick

Dank bestehender Implementierungen lassen sich die populären Standards OAuth 2.0 und OpenID Connect relativ einfach zur Schaffung moderner Authentifizieurngs- und Autorisierungslösungen nutzen: Mit wenigen Maßnahmen lassen sich damit sowohl Web-APIs als auch Clients absichern, wie Single Page Applications. Diese vorhandenen Lösungen sollten aber nicht darüber hinwegtäuschen, dass eine nähere Betrachtung der jeweiligen Standards unumgänglich ist. Gerade wenn es um Detailfragen geht, müssen Architekten wissen, was möglich und was gefährlich oder gar aus Sicherheitsgründen explizit verboten ist. Auch bei der Bewertung unterschiedlicher Optionen und Flows sowie bei der Fehlersuche kommt man zumeist nicht ohne genauere Kenntnis der Standards aus.

Geschrieben von
Manfred Steyer
Manfred Steyer
Manfred Steyer ist selbstständiger Trainer und Berater mit Fokus auf Angular 2. In seinem aktuellen Buch "AngularJS: Moderne Webanwendungen und Single Page Applications mit JavaScript" behandelt er die vielen Seiten des populären JavaScript-Frameworks aus der Feder von Google. Web: www.softwarearchitekt.at
Hans-Peter Grahsl
Hans-Peter Grahsl
Hans-Peter Grahsl arbeitet im Java-Bereich als Technical Trainer und ist für das interne Education Department bei Netconomy Software & Consulting GmbH in Graz verantwortlich. Außerdem unterstützt er Kunden als selbstständiger Trainer und Berater bei der Konzeption und Umsetzung von on-premise- oder Cloud-basierten Datenarchitekturen im NoSQL-Umfeld. Nebenberuflich unterrichtet er an der FH CAMPUS 02.
Kommentare

Schreibe einen Kommentar

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