Teil 3: OAuth 2.0 und die neuen Security Best Practices

Sichere Angular-Projekte mit OAuth 2.0 erstellen
Keine Kommentare

OAuth 2.0 und das darauf aufbauende OpenID Connect erlauben das Anbinden von Identity-Lösungen wie Active Directory. Die OAuth 2.0 Security Best Current Practice schafft den bis dato für SPA verwendeten Implicit Flow ab und erlaubt Refresh-Tokens.

Keine erstzunehmende Geschäftsanwendung kommt ohne Authentifizierung und Autorisierung aus. Schließlich muss die Anwendung ja wissen, mit wem sie es zu tun hat und welche Berechtigungen vorliegen. Eine sehr flexible Lösung hierfür ist der Einsatz von Securitytokens. Sie erlauben die Anbindung verschiedener Identity-Lösungen wie Active Directory und sind die Basis für die Realisierung von Single Sign-On.

Artikelserie

Damit die Anwendung mit allen möglichen Identity-Lösungen funktioniert, benötigen wir ein wohldefiniertes Protokoll zur Kommunikation. Seit einigen Jahren sind OAuth 2.0 und OpenID Connect (OIDC) die De-facto-Standards dafür.

In diesem Artikel zeige ich, wie unsere Lösung aus den letzten beiden Teilen mit diesen Protokollen eine bestehende Identity-Lösung anbinden kann. Dabei berücksichtige ich die aktuellen Best Current Practices, an denen die OAuth Working Group gerade arbeitet und die in OAuth 2.1 einfließen sollen.

International JavaScript Conference

Effective Microservices Architecture In Node.js

by Tamar Stern (Palto Alto Networks)

React Components And How To Style Them

by Jemima Abu (Telesoftas)

Angular Camp 2020

Als Online- oder Präsenztraining!

Das 360°-Intensivtraining mit Angular-Koryphäe Manfred Steyer
Präsentiert von Entwickler Akademie

Das Beispiel dafür befindet sich wieder in meinem GitHub-Account. Der Branch artikel3 beinhaltet die hier beschriebenen Anpassungen.

Überblick zu OAuth 2.0 und OpenID Connect

Das Prinzip hinter OAuth 2.0 und OIDC lässt sich sehr einfach mit dem sogenannten Implicit Flow visualisieren. Dabei handelt es sich um eine simple Spielart, die ursprünglich für Single Page Applications entworfen wurde. Der Flow beginnt damit, den Benutzer zu einem Authorization-Server umzuleiten (Abb. 1).

Abb. 1: OAuth 2/ OIDC Implicit Flow

Abb. 1: OAuth 2/ OIDC Implicit Flow

Dieser Server kennt die einzelnen Konten und fordert den Benutzer auf, sich anzumelden. Die Art der Anwendung ist ein Implementierungsdetail, das nicht vom Standard vorgegeben wird. Häufig kommen an dieser Stelle Benutzernamen und Passwörter zum Einsatz. Innerhalb von Windows-Domänen bietet sich auch die Nutzung der Windows-Authentifizierung an. Dabei erkennt der Authorization-Server ohne weitere Interaktion den aktuellen Windows-Benutzer.

Danach stellt der Authorization-Server ein sogenanntes Access-Token sowie ein ID-Token (Identity-Token) aus und leitet den Benutzer damit zurück zum Client, bei dem es sich in unserem Fall um eine Angular-Lösung handelt.

Das durch OIDC spezifizierte ID-Token informiert den Client über den aktuellen Benutzer. Es beinhaltet Key/Value Pairs – sogenannte Claims –, die den Benutzer beschreiben. Einige Keys sind durch OIDC standardisiert. Beispiele dafür sind given_name, family_name oder email.

Das ID-Token liegt per Definition als JSON Web Token (JWT) vor. Dabei handelt es sich – salopp formuliert – um ein BASE64-kodiertes JSON-Dokument. Somit kann es jeder Client einfach parsen und interpretieren.

Das Access-Token ist hingegen durch OAuth 2.0 definiert und gibt dem Client Zugriff auf das Backend, das die beiden Standards als Ressource-Server bezeichnen. Dazu inkludiert der Client das Token in einer HTTP-Kopfzeile. Da es sich beim Token um eine sensible Information handelt, ist hierbei – wie generell bei OAuth 2.0 und OIDC – auf HTTPS zu setzen.

Nachdem der Ressource-Server das Access-Token erfolgreich validiert hat, findet er darin Claims, die auf den Benutzer verweisen. Um herauszufinden, ob das Access-Token tatsächlich von einem vertrauenswürdigen Authorization-Server stammt, kommen häufig digitale Signaturen zum Einsatz. Alternativ dazu kann der Ressource-Server mit dem Authorization-Server Rücksprache halten, um sich darüber zu informieren, ob das Access-Token (noch) gültig ist. Ersteres ist einfacher und letzteres ist ein wenig sicherer, weil ständig geprüft wird, ob der Benutzer noch immer Zugriff hat.

Ein häufig praktizierter Mittelweg ist die Verwendung von kurzlebigen Access-Tokens. Wie weiter unten beschrieben, muss der Client bei dieser Spielart regelmäßig ein neues Token abrufen. Dabei wird geprüft, ob der Benutzer nach wie vor die nötigen Berechtigungen hat.

Im Gegensatz zum Id-Token ist das Format des Access-Tokens nicht spezifiziert. Es kann jeden beliebigen Aufbau haben und ist für den Client nicht zwangsweise lesbar.

OAuth 2.0 Security Best Current Practice

Der im letzten Abschnitt zusammengefasste Implicit Flow war die Standardlösung in Single Page Applications seit dem Erscheinen der beiden Standards. Das Dokument „OAuth 2.0 Security Best Current Practice“, an dem die OAuth Working Group gerade arbeitet, ändert das. Dieses Dokument ebnet den Weg zu OAuth 2.1 und schlägt vor, den Implicit Flow generell nicht mehr zu nutzen.

Wer den Implicit Flow in bestehenden Lösungen nutzt, muss jedoch nicht in Panik ausbrechen: Kommt er wohlüberlegt gemeinsam mit üblichen Best Practices zum Einsatz, hält er den bekannten Angriffen stand. Einige dieser Best Practices sind sogar durch OIDC vorgeschrieben, und jede vertrauenswürdige Bibliothek sollte sie berücksichtigen.

Ein Nachteil ist auch, dass der Implicit Flow die Tokens beim Redirect über URL-Parameter an den Client sendet (Abb. 1). Sie landen somit in der Browser-History, aber auch in den Logs des Authorization Servers, von wo ein Angreifer sie entwenden kann. Natürlich lässt sich die Browser-History mit JavaScript überschreiben und auch das Logging-Verhalten des Servers kann angepasst werden. Dazu sind allerdings wieder weitere Schritte notwendig, an die man denken muss.

Um diese Schritte samt vielen der etablierten Best Practices zur Absicherung des Implicit Flows überflüssig zu machen, schlägt das erwähnte Dokument einen anderen Flow vor. Dieser nennt sich Authorization Code Flow. In Kombination mit der Erweiterung PKCE (Proof Key for Code Exchange) gilt dieser Flow derzeit als sicherste Variante für die Implementierung von OAuth 2.0 und OIDC im Browser.

Code Flow und PKCE

Code Flow sieht vor, dass der Client statt des eigentlichen Tokens nur einen Access-Code empfängt (Abb. 2). Der Authorization-Server sendet ihn zwar auch in Form eines URL-Parameters an den Client, aber da es sich dabei um eine vergleichsweise unsensible Information handelt, ist das in Ordnung. Außerdem stellt PKCE sicher, dass ein Angreifer keinen Nutzen aus einem gestohlenen Access-Code ziehen kann. Dafür sieht dieser Standard vor, dass der Client bei der ursprünglichen Anfrage den Hash-Wert einer zufälligen Zeichenkette an den Authorization-Server sendet. Diese zufällige Zeichenkette nennt PKCE Verifier.

Abb. 2: Code Flow und PKCE

Abb. 2: Code Flow und PKCE

Möchte der Client danach den Access Code gegen die Tokens eintauschen, muss er ihn gemeinsam mit dem Verifier übersenden (Abb. 3). Der Authorization-Server prüft nun, ob der Verifier zum zuvor übersendeten Hash-Wert passt. Ist das der Fall, kann sich der Authorization-Server sicher sein, dass er mit dem ursprünglichen Client und nicht mit einem Angreifer, der den Access-Code gestohlen hat, kommuniziert. Die Antwort beinhaltet die Tokens. Da das Eintauschen des Access-Codes nicht mehr über Umleitungen, sondern über eine verschlüsselte HTTPS-Verbindung (via AJAX) erfolgt, kann ein Angreifer die Tokens nicht mehr (so einfach) entwenden.

Abb. 3: Access-Code gegen Tokens tauschen

Abb. 3: Access-Code gegen Tokens tauschen

Refresh-Tokens

Üblicherweise haben Tokens eine kurze Lebensspanne, denn falls ein Angreifer sie doch – zum Beispiel via XSS – entwendet, soll er damit nicht lange arbeiten können. Eine Lebensdauer von zehn Minuten ist deswegen keine Seltenheit. Alle zehn Minuten ist also ein neues Token anzufordern und bei dieser Gelegenheit wird auch geprüft, ob der Benutzer in der Zwischenzeit gesperrt wurde.

Das sollte natürlich ohne Benutzerinteraktion erfolgen, denn der Nutzer möchte nicht alle zehn Minuten erneut sein Passwort eingeben. Die sicherste Möglichkeit hierfür besteht in der Nutzung von HTTP-only-Cookies, die sich nicht per XSS entwenden lassen. Leider existiert derzeit keine standardkonforme Möglichkeit zur Nutzung dieser Cookies ohne Umleitung zum Authorization-Server. Gerade solch eine Umleitung möchte man bei SPAs aber vermeiden und schon gar nicht alle zehn Minuten stattfinden lassen.

Eine etwas einfachere Möglichkeit sieht den Einsatz von Refresh-Tokens vor (Abb. 4). Der Authorization-Server stellt das Refresh-Token zusammen mit dem Access- und ID-Token aus, der Client nutzt es bei Bedarf zum Anfordern weiterer Token. Da ein Angreifer Refresh-Tokens im Gegensatz zu HTTP-only-Cookies mittels XSS entwenden kann, verbieten sowohl OAuth 2.0 als auch OIDC ihren Einsatz im Browser.

Abb. 4: Refresh-Tokens

Abb. 4: Refresh-Tokens

Das vorliegende Best-Practices-Dokument weicht von dieser Einschränkung jedoch ab, wenn einige Vorbedingungen gegeben sind. Dazu zählen unter anderem die folgenden:

  • Die möglichen Gefahren müssen im Rahmen einer Risikoabschätzung bewertet werden. Konkret bedeutet das auch, dass Maßnahmen zur Vermeidung von XSS zu ergreifen sind.
  • Jedes Refresh-Token darf nur einmal verwendet werden. Bei Bedarf ist danach ein neues auszustellen. Kommt dasselbe Refresh-Token mehrfach zum Einsatz, sind alle Benutzer, die es verwendet haben, abzumelden.

Identity Server 4 als Authorization-Server

Als Authorization-Server kommt in dieser Teststellung der auf ASP.NET Core basierende Identity Server 4 zum Einsatz. Es handelt sich dabei um ein einfach erweiterbares Open-Source-Projekt, das OpenID-Connect-zertifiziert ist.

Die genutzte Implementierung findet sich auf GitHub. Listing 1 zeigt die verwendete Konfiguration.

public static IEnumerable<Client> GetClients()
{
  return new List<Client>
  {
    new Client
    {
      ClientId = "spa",
      ClientName = "SPA (Code + PKCE)",

      // Kein Client-Secret für SPA
      RequireClientSecret = false,

      RedirectUris = { 
        "http://localhost:4200/index.html",
      },

      PostLogoutRedirectUris = { 
        "http://localhost:4200/index.html",
      },

      // Code Flow mit PKCE
      RequirePkce = true,
      AllowedGrantTypes = GrantTypes.Code,

      // Refresh-Token
      AllowOfflineAccess = true,
      RefreshTokenUsage = TokenUsage.OneTimeOnly,

      // Kurzlebige Token
      AccessTokenLifetime = 60 * 10,
      IdentityTokenLifetime = 60 * 10,

      [...]
    },
    [...]
  }
}

Da wir eine SPA anbinden wollen, wurde RequireClientSecret auf false gesetzt. Es ergibt nämlich keinen Sinn, dass sich eine SPA beim Identity Server mit einem Passwort anmeldet, zumal jeder den Quellcode der Anwendung und somit auch dieses Passwort einsehen kann. Durch PKCE wird jedoch verhindert, dass Angreifer den Access-Code nutzen können. Das explizite Whitelisting der URLs, an die der Identity Server den Access-Code senden darf (RedirectUris), verhindert, dass er in falsche Hände gerät.

Die Eigenschaft AllowOfflineAccess erlaubt den Einsatz von Refresh-Tokens und die Einstellung TokenUsage.OneTimeOnly bei RefreshTokenUsage legt fest, dass jedes Refresh-Token nur ein einziges Mal verwendet werden darf. Daneben definiert die Konfiguration kurze Lebenszeitspannen für die ausgestellten Token, sodass häufig ein Refresh erfolgen muss.

Umsetzung in Angular

Für die Realisierung des Code Flow und PKCE nutze ich hier die von mir bereitgestellte Bibliothek angular-oauth2-oidc. Sie erfreut sich einer weiten Verbreitung und wurde mit zwei verschiedenen Authorization-Servern getestet, um sicherzustellen, dass keine Überanpassung an einen bestimmten Hersteller stattfindet. Dabei handelt es sich um den IdentityServer und Keycloak. Außerdem ist sie von der OpenID Connect Foundation zertifiziert, was zu weiterem Vertrauen führt.

Nach der Installation mit npm (npm install angular-oauth2-oidc) lässt sie sich in das Hauptmodul der Anwendung importieren (Listing 2).

@NgModule({
  imports: [
    BrowserModule,
    HttpClientModule,
    OAuthModule.forRoot({
      resourceServer: {
        sendAccessToken: true,
        allowedUrls: ['http://www.angular.at/api/']
      }
    }),
  ],
  […]
})
export class AppModule {}

Der Parameter sendAccessToken gibt an, dass die Bibliothek das abgerufene Access-Token bei jedem mit Angular durchgeführten HTTP Call anhängen soll. Dazu kommt intern ein HttpInterceptor zum Einsatz. Damit das Token nicht aus Versehen an die falschen APIs gesendet wird, müssen die, die es erhalten dürfen, in eine Whitelist eingetragen werden. Hierzu reicht das Hinterlegen eines Präfixes. Im betrachteten Fall erhalten alle APIs, deren URLs mit http://www.angular.at/api/ beginnen, das Token.

An dieser Stelle soll nochmals betont werden, dass die hier betrachteten Mechanismen nur beim Einsatz von HTTPS als sicher angesehen werden können. Da es sich hier lediglich um eine Demonstration handelt, weichen wir in den Beispielen dieses Beitrags davon ab.

Um der Bibliothek Eckdaten wie die URL des Authorization-Servers mitzuteilen, ist ein Konfigurationsobjekt anzulegen (Listing 3).

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

export const authConfig: AuthConfig = {
 
  // URL des Authorization-Servers
  issuer: 'https://idsvr4.azurewebsites.net',
 
  // URL der Angular-Anwendung
  // An diese URL sendet der Authorization-Server den Access-Code
  redirectUri: window.location.origin + '/index.html',
 
  // Name der Angular-Anwendung
  clientId: 'spa',
 
  // Rechte des Benutzers, die die Angular-Anwendung wahrnehmen möchte
  scope: 'openid profile email offline_access api',

  // Code Flow (PKCE ist standardmäßig bei Nutzung von Code Flow aktiviert)
  responseType: 'code',

  // Refresh token after 75% of its live time 
  timeoutFactor: 0.75

}

Die redirectUri und die clientId müssen gemeinsam beim Authorization-Server hinterlegt sein. Existiert für diese Kombination kein Eintrag, muss der Authorization-Server die Anfrage aus Sicherheitsgründen ablehnen. Ansonsten könnte ein Angreifer vortäuschen, ein bestimmter Client zu sein.

Die Scopes openid, profile und email sind durch OIDC definiert und beschreiben die Informationen, die die Anwendung über den Benutzer bekommen möchte. Im einfachsten Fall wird nur openid angefordert. Das hätte zur Folge, dass die Anwendung lediglich die ID des Benutzers erhält. Hinter profile verbergen sich Profilinformationen wie Vorname oder Nachname und hinter email – wenig überraschend – die E-Mail-Adresse des Benutzers, aber auch der Hinweis, ob diese bereits durch eine Test-E-Mail verifiziert wurde.

Der Scope offline_access gibt an, dass die Anwendung ein Refresh-Token haben möchte. Bei api handelt es sich hingegen um einen benutzerspezifischen Scope. Im betrachteten Fall gibt er an, dass der Client im Namen des Benutzers auf das Demo-API zugreifen darf. In der Regel gibt es pro API solch einen Scope, wobei sich diese auch auf (Teile von) Use Cases herunterbrechen oder zu übergeordneten Scopes aggregieren lassen.

Laut OAuth 2 müssen Benutzer diesen Scopes ihre Zustimmung erteilen. Somit könnten sie festlegen, dass ein Client doch nicht in ihrem Namen auf ein bestimmtes API zugreifen darf oder ihre E-Mail-Adresse erhält. Alle gewährten Scopes finden sich im Access-Token wieder. Der Ressource-Server kann so prüfen, welche Rechte der Benutzer dem Client zugewiesen hat.

Während ein solch explizites Zustimmen bei Consumer-Anwendungen wichtig ist, gestaltet es sich bei Geschäftsanwendungen häufig als lästig. Deswegen erlauben viele Produkte, den sogenannten Consent zu überspringen. In diesem Fall gehen sie davon aus, dass vorab bereits alle Scopes bestätigt wurden. Wichtig ist an dieser Stelle auch, dass die Scopes nicht die Rechte des Benutzers wiederspiegeln, sondern nur jene Rechte, die der Benutzer der Anwendung überträgt.

Damit die Bibliothek das Konfigurationsobjekt verwendet, ist es beim Programmstart zu übergeben (Listing 4).

@Component({ ... })
export class AppComponent {
  constructor(private oauthService: OAuthService) { 
    oauthService.configure(authConfig);
    oauthService.loadDiscoveryDocumentAndTryLogin();
    oauthService.setupAutomaticSilentRefresh();
  }
}

Die Methode loadDiscoveryDocumentAndTryLogin lädt weitere Konfigurationsdaten vom Authorization-Server. Dieses als Discovery bekannte Verfahren ist durch OIDC standardisiert. Danach prüft diese Methode, ob sich bereits ein Access-Code in der URL befindet. In diesem Fall versucht sie, den Flow abzuschließen, was im Erfolgsfall zum Erhalt der diskutierten Tokens führt.

Mit setupAutomaticSilentRefresh gibt die Anwendung an, dass die Tokens automatisch zu erneuern sind, wenn ihre Lebensspanne zu einem bestimmten Grad vorüber ist. Dieser Grad lässt sich als Faktor (ein Wert zwischen 0 und 1) mit der Konfigurationseinstellung timeoutFactor festlegen.

Danach wird es sehr geradlinig. Die Methode initLoginFlow beginnt mit dem Code Flow und leitet den Benutzer zum Authorization-Server um:

this.oauthService.initLoginFlow();

Mit der Methode logOut lässt sich der Benutzer hingegen wieder abmelden:

this.oauthService.logOut();

Das bedeutet zum einen, dass die Tokens verworfen werden, aber auch, dass der Benutzer durch eine Umleitung beim Authorization-Server abgemeldet wird.

Die Methode getIdentityClaims liefert Key/Value Pairs, die den Benutzer beschreiben:

const claims = this.oauthService.getIdentityClaims();
if (!claims) return null;
return claims['given_name'];

Mit dem Token arbeiten

Um die Funktionsweise zu prüfen, reicht es, nach dem Anmelden die Claims zu beziehen sowie einen HTTP-Service aufzurufen. In letzterem Fall sollte die Bibliothek das Access-Token über den HTTP-Header Authorization übersenden (Abb. 5).

Abb. 5: HTTP-Header mit Access-Token

Abb. 5: HTTP-Header mit Access-Token

Standardmäßig legt die Bibliothek die Tokens im Session Storage ab, wobei sich auch das über die Konfiguration ändern lässt. Wer neugierig ist, kann sie somit über die Dev-Tools des Browsers auslesen.

Eine sehr einfache Möglichkeit, BASE64-kodierte JWTs zu analysieren, bietet die Seite www.jwt.io (Abb. 6). Das betrachtete Beispiel zeigt ein ID-Token. Der Anzeige zufolge besteht es aus drei Teilen. Der in Rot dargestellte Header beinhaltet allgemeine Informationen wie die für die Signatur verwendeten Kryptoalgorithmen und öffentlichen Schlüssel. Der Payload beinhaltet neben technischen Informationen wie dem Ablaufdatum (exp) die angeforderten Claims. Zu diesen zählen die Felder sub (Subject, Benutzer-ID), given_name, family_name, email, email_verified und website. Der dritte, hier nur angedeutete Abschnitt beinhaltet eine optionale digitale Signatur, die das Überprüfen des Ausstellers erlaubt.

Abb. 6: Dekodiertes JWT auf www.jwt.io

Abb. 6: Dekodiertes JWT auf www.jwt.io

Fazit und Ausblick

OAuth 2 und OIDC setzen auf HTTPS auf und sind im Prinzip her einfach. Der Teufel steckt jedoch im Detail und sogar große Konzerne wie Facebook öffneten Angreifern die Türen, indem sie ein wenig vom Standard abwichen. Deswegen empfiehlt es sich, auf bewährte Lösungen zurückzugreifen, statt diese Standards von Hand zu implementieren.

Durch das neue Best-Practices-Dokument, das in OAuth 2.1 einfließen soll, wird der Umgang mit OAuth 2 und OIDC ein wenig einfacher: Der Implicit Flow fällt weg, somit werden viele Best Practices zur Verhinderung von Angriffen unnötig. Daneben machen Refresh-Tokens den Token-Refresh im Browser geradliniger und stabiler.

Da Refresh-Tokens äußerst sensible Informationen darstellen und im Browser nicht vor XSS-Angriffen sicher sind, muss man hier jedoch Strategien für die Verhinderung von XSS nutzen. Angular macht an dieser Stelle durch die standardmäßige Kodierung von Ausgaben einen sehr guten Job. Allerdings gilt es auch, zu verhindern, dass npm-Pakete und Skripte von Drittanbietern in der Anwendung ihr Unwesen treiben. Die Auditinformationen von npm, aber auch kommerzielle Produkte zum Validieren von Bibliotheken helfen hierbei.

Die wohl sicherste Möglichkeit, um den Diebstahl von Refresh-Tokens zu verhindern, ist der Einsatz von HTTP-only-Cookies. Das ist zwar heute schon möglich, aber leider nicht durch OAuth 2 oder OIDC standardisiert. Vor allem für einen AJAX-basierten Token-Refresh unter Verwendung von HTTP-only-Cookies mit Refresh-Tokens müssten die Standards erweitert werden. Da solch eine Erweiterung die Sicherheit von SPAs erheblich verbessern würde, befindet sie sich ganz oben auf meiner Wunschliste.

Windows Developer

Windows DeveloperDieser Artikel ist im Windows Developer erschienen. Windows Developer informiert umfassend und herstellerneutral über neue Trends und Möglichkeiten der Software- und Systementwicklung rund um Microsoft-Technologien.

Natürlich können Sie den Windows Developer über den entwickler.kiosk auch digital im Browser oder auf Ihren Android- und iOS-Devices lesen. Außerdem ist der Windows Developer weiterhin als Print-Magazin im Abonnement erhältlich.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu:
X
- Gib Deinen Standort ein -
- or -