Authorization

OAuth 2.0

OAuth 2.0 ist ein Framework für delegierte Autorisierung, nicht für Authentifizierung. Es ermöglicht einem Benutzer, einer App begrenzten Zugriff auf seine Daten zu gewähren, ohne ein Passwort teilen zu müssen.

intermediate16 min readUpdated 16. Sept. 2026
authorize.ts
ts
// authorize.ts
import { randomBytes, createHash } from "node:crypto";

const verifier = randomBytes(32).toString("base64url");
const challenge = createHash("sha256").update(verifier).digest("base64url");

const url = new URL("https://auth.example.com/authorize");
url.search = new URLSearchParams({
  response_type: "code",
  client_id: "web-app",
  redirect_uri: "https://app.example.com/callback",
  scope: "openid email profile offline_access",
  state: randomBytes(16).toString("base64url"),
  code_challenge: challenge,
  code_challenge_method: "S256",
}).toString();

console.log(url.toString());
Erste Spec
2012
Core RFCs
RFC 6749 / 6750
PKCE RFC
RFC 7636
Rollen
4
Moderner Grant
Authorization Code + PKCE
Identity Layer
OpenID Connect

Warum es wichtig ist

Wofür OAuth gedacht ist

Delegierter Zugriff

Ein Benutzer gewährt einer Anwendung begrenzten, widerrufbaren Zugriff auf seine Daten, ohne jemals ein Passwort oder ein langfristiges Credential preiszugeben.

Scoped Permissions

Jeder Grant fordert explizite Scopes an, sodass ein Client genau den Zugriff erhält, dem der Benutzer zugestimmt hat, und keinen weiteren.

Ein Standard über Provider hinweg

Der gleiche Flow funktioniert mit Google, GitHub, Okta und deinem eigenen Server, was ein einheitliches Integrationsmuster für jeden Identity Provider bedeutet.

Das Gesamtbild

Die vier Rollen in einem Bild

Ein Resource Owner gewährt einem Client Zugriff auf einen Resource Server, vermittelt durch einen Authorization Server, der Tokens ausstellt.

Resource owner

Grant

Der Benutzer, dem die Daten gehören und der entscheidet, welche Anwendung darauf zugreifen darf und wofür.

Client

Request

Die Anwendung, die den Zugriff anfordert, identifiziert durch eine Client ID und, sofern möglich, ein Secret.

Authorization server

Broker

Authentifiziert den Benutzer, zeigt die Zustimmung an und stellt Access-, Refresh- und Identity-Tokens aus.

Resource server

Host

Die API, die die geschützten Daten hält und diese nur zurückgibt, wenn ein gültiges Access Token akzeptiert wird.

HTML5 auf einen Blick

Die Einzelteile

Authorize Endpoint

Der Browser-Redirect, bei dem sich der Benutzer authentifiziert und zustimmt.

Authorization Code

Ein kurzlebiger Einmal-Code, der an die Redirect URI des Clients zurückgegeben wird.

Token Endpoint

Ein Back-Channel POST, der einen Code oder ein Refresh Token gegen Tokens eintauscht.

Redirect URI

Der registrierte Callback, der exakt übereinstimmen muss, um Code-Diebstahl zu verhindern.

Resource Server

Die API, die ein Bearer Access Token akzeptiert und Daten zurückgibt.

Scopes

Leerzeichen-getrennte Berechtigungen, die einschränken, was ein Token tun darf.

Ablauf

Authorization Code + PKCE

Der Standard-Flow für Web-, Mobile- und Single-Page-Apps. Zwei Redirects und ein Back-Channel-Austausch.

  1. 1

    Redirect mit einer Challenge

    Der Client sendet den Browser an /authorize mit seiner Client ID, Redirect URI, Scopes, State und einer gehashten PKCE code_challenge.

  2. 2

    Der Benutzer stimmt zu

    Der Authorization Server authentifiziert den Benutzer und zeigt die angeforderten Scopes an. Der Benutzer bestätigt oder lehnt ab.

  3. 3

    Redirect zurück mit einem Code

    Der Browser kehrt zur registrierten Redirect URI zurück, zusammen mit einem kurzlebigen Einmal-Authorization-Code und dem ursprünglichen State.

  4. 4

    Austausch von Code und Verifier

    Der Client sendet den Code und den ursprünglichen code_verifier per POST an den Token Endpoint und erhält Access-, Refresh- und ID-Tokens.

  5. 5

    Aufruf des Resource Servers

    Der Client sendet das Access Token als Bearer-Credential bei API-Requests. Der Resource Server validiert es und setzt die Scopes durch.

  6. 6

    Refresh bei Ablauf

    Wenn das Access Token abläuft, tauscht der Client das Refresh Token gegen ein neues Paar ein und widerruft die Tokens beim Logout.

Eine kurze Geschichte

Von signierten Requests zu PKCE

  1. 2007

    OAuth 1.0 und signierte Requests

    Eine frühe Spec signiert jeden Request mit Shared Secrets, was sicher ist, aber für Clients und Provider gleichermaßen mühsam.

    07
  2. 2012

    OAuth 2.0 ersetzt Signaturen

    RFC 6749 führt Bearer Tokens und das rollenbasierte Modell ein, das das Protokoll bis heute definiert.

    12
  3. 2014

    OpenID Connect fügt Identität hinzu

    OIDC legt ein ID Token und einen userinfo Endpoint über OAuth, was echtes Sign-in ermöglicht.

    14
  4. 2015

    PKCE schützt Public Clients

    RFC 7636 schließt die Lücke für Code-Interception bei Apps, die kein Client Secret sicher speichern können.

    15
  5. 2021

    OAuth 2.1 konsolidiert die Lehren

    Der Entwurf deprecated Implicit- und Password-Grants und macht PKCE obligatorisch, um Best Practices zu kodifizieren.

    21

Der vollständige Leitfaden

OAuth 2.0: Alles was Sie wissen müssen

Was OAuth 2.0 ist – und was nicht

OAuth 2.0 ist ein Authorization Framework. Es ermöglicht einem Nutzer, einer Drittanbieter-Anwendung begrenzten Zugriff auf seine Ressourcen zu gewähren, ohne sein Passwort teilen zu müssen. Das Ergebnis eines erfolgreichen Flows ist ein Access Token. Dieses Token beschreibt, was der Client tun darf, aber nicht zwangsläufig, wer der Nutzer ist.

Diese Unterscheidung führt immer wieder zu Verwirrungen. „Sign in with Google“ ist OAuth 2.0 plus OpenID Connect. Der OAuth-Teil beschafft den Zugriff auf die Google APIs; der OpenID Connect-Teil liefert ein ID Token zurück, das den Nutzer tatsächlich identifiziert. Wenn Sie nur OAuth implementieren und das Access Token als Identitätsnachweis behandeln, haben Sie einen delegierten Zugriff implementiert und diesen fälschlicherweise als Authentifizierung bezeichnet – ein Kategorienfehler mit entsprechenden Sicherheitsrisiken.

Das Protokoll wurde für ein ganz spezifisches Problem entwickelt: Eine Seite für Fotodrucke soll Fotos von einem Storage-Provider lesen können, ohne jemals das Passwort für den Storage-Account zu sehen. Behalten Sie diese ursprüngliche Intention im Hinterkopf, dann ergeben die Design-Entscheidungen einen Sinn.

OpenID Connect: Identität als zusätzliche Schicht

OpenID Connect ist eine schlanke Identitätsschicht über OAuth 2.0. Sie fügt den openid Scope, einen standardisierten userinfo Endpoint und vor allem ein ID Token hinzu – ein JWT, dessen Claims das Authentifizierungsereignis beschreiben.

{
  "iss": "https://accounts.example.com",
  "sub": "110169484474386276334",
  "aud": "web-app",
  "exp": 1760000000,
  "iat": 1759999100,
  "email": "[email protected]",
  "email_verified": true,
  "nonce": "n-0S6_WzA2Mj"
}

Das ID Token ist für den Client gedacht, nicht für die API. Senden Sie es niemals als Access Token an einen Resource Server. Verifizieren Sie die Signatur, iss, aud, exp und nonce, bevor Sie dem Token vertrauen, und verwenden Sie sub anstelle der E-Mail-Adresse als stabilen Benutzeridentifikator. E-Mail-Adressen ändern sich oder werden neu zugewiesen; der Subject bleibt über die gesamte Lebensdauer des Accounts stabil.

Die vier Rollen

OAuth definiert vier Teilnehmer. Wenn man diese präzise unterscheidet, wird der Rest des Protokolls fast von selbst klar.

  • Resource owner — der Nutzer, dem die Daten gehören und der den Zugriff gewährt.
  • Client — die Anwendung, die den Zugriff anfordert, zum Beispiel deine Web-App.
  • Authorization server — stellt Tokens aus, nachdem der Nutzer authentifiziert wurde und seine Zustimmung gegeben hat.
  • Resource server — die API, die das Access Token akzeptiert und die Daten zurückgibt.

Deine Web-App ist der Client. Google ist der Authorization server. Die APIs von Google sind der Resource server. Der Nutzer ist der Resource owner. Ein einziger Provider übernimmt oft beide Server-Rollen, weshalb die Unterscheidung theoretisch wirken kann, bis man selbst ein solches System baut.

Grant Types

Ein Grant Type ist das „Rezept“, das ein Client verwendet, um ein Token zu erhalten. Heutzutage sind vier davon relevant.

  • Authorization Code + PKCE — der Standard für Web-, Mobile- und Single-Page-Apps. Der Benutzer authentifiziert sich am Authorization Server, und der Client tauscht einen kurzlebigen Code gegen Tokens ein.
  • Client Credentials — für Machine-to-Machine-Kommunikation. Es ist kein Benutzer involviert; der Client authentifiziert sich mit seinen eigenen Credentials.
  • Device Code — für Geräte mit eingeschränkter Eingabe, wie zum Beispiel Fernseher und CLI-Tools.
  • Refresh Token — kein Weg zum Einloggen, sondern die Standardmethode, um ein Access Token ohne erneuten Redirect zu erneuern.

Zwei Grants sind praktisch hinfällig. Der implicit Flow gab Tokens direkt im URL-Fragment zurück, wodurch sie in der History und über Referrer exponiert wurden. Der password Grant verlangte vom Client, das Passwort des Benutzers zu verarbeiten, was den eigentlichen Zweck von OAuth zunichtemacht. OAuth 2.1 entfernt beide, und kein neues System sollte sie mehr verwenden.

Authorization Code + PKCE

Dies ist der Flow, den man beherrschen sollte. Der Client leitet den Benutzer mit einer gehashten Challenge an den Authorization Server weiter, der Benutzer stimmt zu, der Server leitet mit einem einmaligen Code zurück und der Client tauscht diesen Code zusammen mit dem ursprünglichen Verifier gegen Tokens ein.

Der Code ist ohne den Verifier nutzlos, sodass ein Angreifer, der den Redirect abfängt, den Austausch nicht abschließen kann. Genau das macht PKCE sicher – selbst für Public Clients wie mobile Apps und Single-Page Apps, die keinerlei Secrets sicher speichern können.

Der Flow besteht aus zwei Browser-Redirects und einer Back-Channel-Anfrage. Die Redirects sind sichtbar und können manipuliert werden; der Austausch hingegen ist ein direkter Server-to-Server POST, den ein Angreifer nicht beobachten kann. Das gesamte Design basiert darauf, das geheime Material über den Back Channel zu übertragen.

Die Redirect URI und der State

Die redirect URI ist die Adresse, an die der Authorization Server den Benutzer zurückschickt. Sie muss exakt registriert sein und exakt übereinstimmen. Eine zu lockere Prüfung ist die Ursache für Open-Redirect-Schwachstellen, durch die Authorization Codes an vom Angreifer kontrollierte Hosts geleakt werden können. Akzeptieren Sie niemals eine redirect URI aus einem Query-Parameter und erlauben Sie keine Wildcard-Subdomains.

Der state-Parameter ist ein opaker Wert, den der Client generiert und überprüft, wenn der Callback zurückkehrt. Er schützt den Callback-Endpunkt vor CSRF. Ein Angreifer, der ein Opfer dazu bringt, einen Flow mit dem Code des Angreifers abzuschließen, wird die State-Prüfung nicht bestehen.

const state = randomBytes(16).toString("base64url");
session.oauthState = state;

// later, in the callback:
if (query.state !== session.oauthState) {
  throw new Error("state_mismatch");
}

Fügen Sie für OIDC einen nonce hinzu und verifizieren Sie diesen innerhalb des ID-Tokens. Der State schützt den Callback des Clients vor CSRF; der nonce bindet das ID-Token an diese spezifische Anfrage und verhindert Replay-Angriffe.

PKCE im Detail

PKCE (Proof Key for Code Exchange) ist in RFC 7636 definiert und mittlerweile für Public Clients obligatorisch. Es basiert auf drei Werten: Der Client generiert einen zufälligen code_verifier, leitet daraus einen code_challenge als SHA-256-Hash ab, sendet die Challenge mit dem Authorize-Request und den Verifier mit dem Token-Request. Der Server hasht anschließend den Verifier und vergleicht die Ergebnisse.

import { randomBytes, createHash } from "node:crypto";

const verifier = randomBytes(32).toString("base64url");
const challenge = createHash("sha256").update(verifier).digest("base64url");

Verwenden Sie immer die S256-Methode. Die plain-Methode existiert nur aus Kompatibilitätsgründen und macht den Zweck von PKCE zunichte, da ein Angreifer, der die Challenge sieht, gleichzeitig auch den Verifier sieht. Speichern Sie den Verifier in der Session des Benutzers, damit der Callback ihn finden kann, und löschen Sie ihn nach einmaliger Verwendung.

Den Authorize-Request erstellen

Der Authorize-Request ist ein Browser-Redirect und daher ein GET-Request mit Query-Parametern. Der Benutzer sieht den Consent-Screen des Providers und nicht Ihre Anwendung.

export function buildAuthorizeUrl(state: string, challenge: string) {
  const url = new URL("https://auth.example.com/authorize");
  url.search = new URLSearchParams({
    response_type: "code",
    client_id: "web-app",
    redirect_uri: "https://app.example.com/callback",
    scope: "openid email profile offline_access",
    state,
    code_challenge: challenge,
    code_challenge_method: "S256",
  }).toString();
  return url.toString();
}

response_type=code wählt den Authorization Code Flow aus. Der Scope openid macht den Request zu einem OIDC-Request. offline_access ist die gängige Konvention, um einen Refresh Token anzufordern; einige Provider schalten diesen hinter eine zusätzliche Bestätigung (Consent Prompt).

Token-Austausch

Der Callback liefert code und state. Nach der Überprüfung des State sendet der Client den Code per POST an den Token-Endpunkt. Dies ist ein Back-Channel-Aufruf von Ihrem Server, kein Browser-Redirect.

export async function exchangeCode(code: string, verifier: string) {
  const res = await fetch("https://auth.example.com/token", {
    method: "POST",
    headers: { "content-type": "application/x-www-form-urlencoded" },
    body: new URLSearchParams({
      grant_type: "authorization_code",
      code,
      redirect_uri: "https://app.example.com/callback",
      client_id: "web-app",
      code_verifier: verifier,
    }),
  });

  if (!res.ok) throw new Error("token_exchange_failed");
  return res.json();
}

Die Antwort enthält access_token, token_type, expires_in, normalerweise refresh_token und bei OIDC ein id_token. Ein Confidential Client, also einer, der ein Secret sicher speichern kann, authentifiziert sich bei dieser Anfrage zudem mittels client_secret oder, noch besser, über eine Private-Key-Assertion. Ein Public Client verlässt sich ausschließlich auf PKCE.

Access-, Refresh- und ID-Tokens

Diese drei Tokens haben unterschiedliche Aufgaben, und eine Verwechslung führt oft zu subtilen Bugs.

  • Access token — wird dem Resource Server präsentiert. Es ist häufig ein JWT, aber die Spezifikation verlangt lediglich, dass es für den Client opak ist. Es kann sich also auch um einen zufälligen String handeln, den der Server nachschlägt.
  • Refresh token — wird ausschließlich dem Authorization Server präsentiert, um ein neues Access token zu erhalten. Es ist langlebig und hochsensibel.
  • ID token — ein OIDC JWT, das dem Client mitteilt, wer sich gerade angemeldet hat. Es wird niemals an eine API gesendet.

Behandeln Sie das Access token als Bearer-Credential: Jeder, der es besitzt, kann es verwenden. Halten Sie die Lebensdauer kurz, fordern Sie nur die minimal benötigten Scopes an und überlassen Sie die langfristige Beziehung dem Refresh token. Das Token-Format selbst wird im JWT-Guide behandelt.

Scopes drücken aus, wofür der Client anfragt. Der Authorization Server zeigt diese dem Benutzer in einem Consent-Screen an und kodiert die gewährte Teilmenge in das Access Token.

Fordern Sie nur das Minimum an. Eine Kalender-App, die vollen Zugriff auf das gesamte Postfach verlangt, wird Nutzer abschrecken und den potenziellen Schaden bei einer Sicherheitslücke (Blast Radius) vergrößern. Provider veröffentlichen zudem reservierte Scopes: openid ist für OIDC erforderlich, und offline_access steuert in der Regel die Refresh Tokens.

Scopes sind keine Rollen. Ein Scope beschreibt eine Berechtigung, die der Client für diesen Grant angefordert hat; eine Rolle beschreibt, wer der Benutzer innerhalb Ihres Systems ist. Bilden Sie diese auf Ihrem Server aufeinander ab und gehen Sie niemals davon aus, dass die Scopes eines Providers etwas über Ihr eigenes Autorisierungsmodell aussagen. Für diesen Aspekt des Problems lesen Sie bitte unter RBAC.

Den Resource Server aufrufen

Sobald ein Access Token vorliegt, wird dieser bei API-Aufrufen im Authorization Header mittels des Bearer-Schemas übermittelt.

const res = await fetch("https://api.example.com/me", {
  headers: { authorization: `Bearer ${accessToken}` },
});

Der Resource Server validiert den Token – entweder durch die lokale Verifizierung eines JWT oder per Introspection –, prüft den Scope und gibt die Daten zurück. Er sieht niemals den Refresh Token oder den ID Token und sollte diese ablehnen, falls sie dennoch übermittelt werden.

Introspection und Revocation

Nicht jeder Access Token ist ein JWT. Wenn es sich um einen opaque Token handelt, fragt der Resource Server den Authorization Server mittels Token Introspection (definiert durch RFC 7662) ab, ob dieser noch gültig ist.

POST /introspect HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded
Authorization: Basic <client credentials>

token=2YotnFZFEjr1zCsicMWpAA

Die Introspection gibt active sowie den Scope, den Subject und das Ablaufdatum zurück. Diese Antwort ist maßgeblich, erfordert jedoch einen Netzwerkaufruf; cachen Sie das Ergebnis daher für einige Sekunden.

Revocation (definiert durch RFC 7009) ermöglicht es einem Client, den Authorization Server anzuweisen, einen Token ungültig zu machen – in der Regel beim Logout.

await fetch("https://auth.example.com/revoke", {
  method: "POST",
  headers: { "content-type": "application/x-www-form-urlencoded" },
  body: new URLSearchParams({ token: refreshToken, client_id: "web-app" }),
});

Widerrufen Sie Refresh Tokens beim Logout konsequent und bedenken Sie, dass das Widerrufen eines Refresh Tokens nicht automatisch bereits ausgestellte Access Tokens ungültig macht. Kurze Lebensdauern für Access Tokens sorgen dafür, dass die Revocation unmittelbar wirkt.

Machine-to-machine: client credentials

Wenn kein Benutzer involviert ist, agiert der Client als er selbst. Er authentifiziert sich am Token-Endpoint und erhält ein Access Token, das auf seine eigenen Berechtigungen beschränkt ist.

const res = await fetch("https://auth.example.com/token", {
  method: "POST",
  headers: {
    "content-type": "application/x-www-form-urlencoded",
    authorization: `Basic ${btoa(`${clientId}:${clientSecret}`)}`,
  },
  body: new URLSearchParams({
    grant_type: "client_credentials",
    scope: "reports:read",
  }),
});

Es gibt keinen Consent-Screen und kein Refresh Token; der Service fordert einfach ein neues Token an, wenn das alte abläuft. Bevorzugen Sie die private-key JWT client authentication gegenüber einem shared secret, sofern der Provider dies unterstützt, und rotieren Sie Secrets in regelmäßigen Abständen. Dies ist dieselbe Nische, die auch langlebige API keys besetzen, jedoch mit standardisiertem Ablaufdatum und Scoping, wie in API Keys beschrieben.

Wann man OAuth nicht verwenden sollte

OAuth dient dazu, den Zugriff an eine dritte Partei zu delegieren. Wenn Ihre eigene Web-App ihre eigenen Benutzer gegenüber ihrem eigenen Backend authentifiziert, benötigen Sie kein OAuth. Ein Session-Cookie oder ein First-Party JWT, den Sie selbst ausstellen, ist einfacher und leichter abzusichern. Einen Authorization Server zwischen Ihr Login-Formular und Ihre Datenbank zu schalten, bringt Komplexität, aber keine zusätzliche Sicherheit.

Greifen Sie zu OAuth, wenn Sie einen externen Provider integrieren, selbst als Provider für Drittanbieter-Clients fungieren oder einen Standard für den Machine-to-Machine-Zugriff benötigen. Beginnen Sie ansonsten mit Session Auth und fügen Sie OAuth erst hinzu, wenn ein echter Delegationsbedarf entsteht. Da jeder hier beschriebene Flow über Redirects und Header läuft, ist der HTTP-Guide eine nützliche Ergänzung.

Häufige Fallstricke

  • Implicit flow — Tokens im URL-Fragment können über den Verlauf, Logs und Referrer durchsickern. Verwenden Sie stattdessen Authorization Code mit PKCE.
  • Tokens in localStorage — Ein XSS-Bug führt hier direkt zur Übernahme des Benutzerkontos. Speichern Sie Tokens in httpOnly cookies oder server-seitigen Sessions.
  • Open redirects — Eine zu lockere Prüfung der Redirect-URI ermöglicht es Angreifern, Codes zu stehlen. Die registrierte URI muss exakt übereinstimmen.
  • Verzicht auf state — Ohne diesen Parameter ist der Callback anfällig für CSRF.
  • Zu weit gefasste Scopes — Wenn alles angefordert wird, verliert die Zustimmung (Consent) ihre Bedeutung und Datenlecks werden schwerwiegender.
  • Langlebige Access Tokens — Diese können nicht schnell widerrufen werden. Halten Sie die Laufzeit kurz und rotieren Sie die Refresh Tokens.
  • Verwendung des ID tokens als API-Credential — Dieses ist für den Client gedacht, nicht für den Resource Server.

Den richtigen Flow auswählen

Die meisten Entscheidungen bei der Integration lassen sich auf zwei Fragen reduzieren: Gibt es einen Benutzer und kann der Client ein Geheimnis bewahren?

  • Ein Benutzer ist vorhanden, der Client ist öffentlich (SPA, mobile App, Desktop): Authorization Code mit PKCE.
  • Ein Benutzer ist vorhanden, der Client ist vertraulich (server-rendered Web App): Authorization Code mit PKCE plus Client-Authentifizierung.
  • Kein Benutzer, der Client ist ein Service (Cronjob, Microservice): Client Credentials.
  • Das Gerät hat keinen Browser oder keine Tastatur (TV, CLI): Device Code.

Alles andere ist veraltet. Wenn ein Provider nur den Implicit Flow dokumentiert, sollte dies als Warnsignal gewertet werden; prüfen Sie in diesem Fall, ob PKCE verfügbar ist.

Das Backend-for-Frontend-Pattern

Der sicherste Ort für die Speicherung von Tokens ist ein Server, den Sie selbst kontrollieren. Das Backend-for-Frontend-Pattern setzt einen schlanken Server zwischen den Browser und den Authorization-Server: Der Browser erhält ein Session-Cookie, während der Server die Access- und Refresh-Tokens verwaltet.

app.get("/callback", async (req, res) => {
  if (req.query.state !== req.session.oauthState) {
    return res.status(400).json({ error: "state_mismatch" });
  }

  const tokens = await exchangeCode(
    String(req.query.code),
    req.session.codeVerifier,
  );

  req.session.tokens = tokens;
  delete req.session.oauthState;
  delete req.session.codeVerifier;

  res.redirect("/dashboard");
});

Da der Browser niemals ein Token sieht, können diese nicht per XSS gestohlen werden, und der Server kann die Tokens im Hintergrund (silent) aktualisieren. Der Kompromiss besteht in einem zusätzlichen Netzwerk-Hop und einem zu betreibenden Server – was in der Regel günstiger ist als der Vorfall, den man dadurch vermeidet.

Native und mobile Clients

Native Apps können kein Client Secret sicher speichern, weshalb PKCE obligatorisch ist. Zudem gibt es ein Problem mit dem Redirect: Ein benutzerdefiniertes Scheme wie myapp://callback kann von einer bösartigen App beansprucht werden. Die moderne Lösung ist ein Tab im Systembrowser – entweder über die Authentication Session API der Plattform oder einen eingebetteten Browser, der Cookies mit dem System teilt.

Verwenden Sie niemals ein eingebettetes WebView für OAuth. Die App könnte das Passwort des Nutzers auslesen, was die Trust Boundary verletzt, die das gesamte Protokoll schützen soll. Öffnen Sie den Systembrowser, empfangen Sie den Callback und speichern Sie die Tokens im Secure Storage der Plattform.

Einen eigenen Authorization Server betreiben

Sie müssen nicht selbst einen bauen. Etablierte Server wie Keycloak, Ory Hydra und Cloud-Identity-Provider implementieren das Protokoll, Consent-Screens, das Key-Management und die Token-Speicherung für Sie. Die Entwicklung eines eigenen Servers ist ein mehrmonatiges Projekt, bei dem jeder Fehler sicherheitskritisch ist.

Falls Sie dennoch einen eigenen betreiben, gehören zu den Mindestanforderungen: exaktes Matching der Redirect-URIs, die Erzwingung von PKCE, kurze Lebensdauern für Access-Token, Refresh-Token-Rotation mit Reuse-Detection, ein JWKS-Endpoint sowie Revocation. Wenn Sie auch nur einen dieser Punkte vernachlässigen, haben Sie eine Sicherheitslücke implementiert und kein Feature.

Eine OAuth-Integration testen

End-to-End-Tests für OAuth sind langsam und fehleranfällig, da sie von einem echten Provider und einem echten Browser abhängen. Testen Sie stattdessen in Schichten.

  • Unit-Tests für die URL-Konstruktion, die PKCE-Ableitung und den State-Vergleich als reine Funktionen.
  • Integrationstests für den Token-Austausch gegen einen Mock-Authorization-Server, der vordefinierte Tokens zurückgibt.
  • Behalten Sie einen Smoke-Test gegen den echten Provider bei, markieren Sie diesen jedoch so, dass er nicht bei jedem Commit ausgeführt wird.
test("builds an authorize URL with PKCE", () => {
  const url = new URL(buildAuthorizeUrl("state-1", "challenge-1"));
  expect(url.searchParams.get("response_type")).toBe("code");
  expect(url.searchParams.get("code_challenge_method")).toBe("S256");
  expect(url.searchParams.get("state")).toBe("state-1");
});

Im Callback verbergen sich oft die meisten Bugs. Testen Sie daher die Fehlerpfade explizit: Ein nicht übereinstimmender State, ein bereits verwendeter Code und ein abgelaufener Token sollten jeweils einen klaren Fehler auslösen, anstatt eine halb authentifizierte Session zu erzeugen.

Logout und Single Sign-On

Das Ausloggen aus Ihrer Anwendung ist nicht dasselbe wie das Ausloggen beim Provider. Ein vollständiger Logout umfasst drei Schritte: Er zerstört die lokale Session, widerruft den Refresh Token am Authorization Server und beendet bei Single Sign-On die Provider-Session, damit der nächste Login diese nicht stillschweigend wiederverwendet.

app.post("/logout", async (req, res) => {
  if (req.session.tokens?.refresh_token) {
    await revoke(req.session.tokens.refresh_token);
  }
  req.session.destroy(() => res.redirect("/"));
});

Single Sign-On ergibt sich aus demselben Mechanismus: Sobald ein Benutzer eine Session beim Provider hat, erhalten nachfolgende Clients einen Code, ohne dass das Passwort erneut eingegeben werden muss. Das ist bequem, und es ist auch der Grund, warum das Ausloggen an gemeinsam genutzten Computern wichtiger ist, als viele erwarten.

Refresh Token Rotation und Reuse Detection

OAuth schreibt nicht vor, dass Refresh Tokens nur einmal verwendet werden dürfen, aber die sichersten Implementierungen setzen auf Rotation. Jeder Refresh-Vorgang liefert ein neues Refresh Token zurück und invalidiert das vorherige. Wenn ein altes Token erneut präsentiert wird, erkennt der Server, dass es gestohlen wurde, und widerruft die gesamte Token-Familie.

export async function refresh(grant: { refresh_token: string; client_id: string }) {
  const stored = await db.refreshToken.findByHash(hash(grant.refresh_token));

  if (!stored || stored.revoked) {
    if (stored) await revokeFamily(stored.familyId);
    throw new Error("invalid_grant");
  }

  await db.refreshToken.revoke(stored.id);
  return issueTokens(stored.userId, stored.familyId);
}

Die Reuse Detection ist hierbei der entscheidende Vorteil: Ein gestohlenes Refresh Token wird so zu einer Stolperfalle statt zu einer permanenten Hintertür. Kombinieren Sie dies mit einem Sliding Expiry, sodass aktive Nutzer angemeldet bleiben, während ein verlassenes Token schließlich abläuft.

Best Practices

  • Verwenden Sie Authorization Code mit PKCE für jeden benutzerseitigen Client, einschließlich SPAs und mobile Apps.
  • Nutzen Sie Client Credentials für Machine-to-Machine-Kommunikation, idealerweise mit Private-Key-Authentifizierung.
  • Registrieren Sie exakte Redirect URIs und lehnen Sie jede Anfrage ab, die nicht zeichengenau übereinstimmt.
  • Senden und verifizieren Sie immer state; fügen Sie für OIDC einen nonce hinzu und verifizieren Sie diesen.
  • Fordern Sie nur die minimal notwendigen Scopes an und behandeln Sie den Consent-Screen als bedeutsames Element.
  • Halten Sie Access Tokens kurzlebig, rotieren Sie Refresh Tokens und widerrufen Sie diese beim Logout.
  • Verifizieren Sie die Signatur des ID Tokens sowie iss, aud, exp und nonce, bevor Sie diesem vertrauen.
  • Speichern Sie Client Secrets und Private Keys in einem Secret Manager, niemals im Frontend.
  • Cachen Sie Introspection nur kurzzeitig und setzen Sie für einen zeitnahen Widerruf auf kurze Laufzeiten.

Häufige Fehler

  • OAuth als Authentifizierung behandeln, ohne OpenID Connect zu nutzen.
  • Den Implicit Flow implementieren, nur weil er einen Redirect weniger benötigt.
  • Access Tokens im localStorage speichern und an jeden Origin senden.
  • Das Client Secret in einer Single-Page App hinterlegen, wo es für jeden lesbar ist.
  • Jede beliebige Redirect URI oder eine per Query-Parameter übergebene URI akzeptieren.
  • Vergessen, state beim Callback zu validieren.
  • Jeden verfügbaren Scope anfordern und die Liste nie wieder zu überarbeiten.
  • Davon ausgehen, dass das Widerrufen eines Refresh Tokens sofort alle Access Tokens ungültig macht.
  • Das ID Token als Bearer Token für die eigene API verwenden.

Wie geht es weiter?

OAuth ist die Methode, mit der Tokens beschafft werden; JWT erklärt, wie diese aufgebaut und verifiziert werden. Wenn es sich bei deiner App um eine First-Party-Anwendung handelt und du eine sofortige Widerrufbarkeit (Revocation) benötigst, ist Session Auth der einfachere Startpunkt. Für Machine-Clients, bei denen kein Benutzer involviert ist, bietet API Keys die langlebige Alternative. Und da jeder dieser Flows über das Netzwerk läuft, lohnt sich eine Auffrischung zum Thema HTTP.

In der Praxis

Autorisieren, austauschen, aufrufen, refreshen

Der vollständige Lebenszyklus des Authorization Codes in vier Requests.

authorize.ts
import { randomBytes, createHash } from "node:crypto";

export function buildAuthorizeUrl(session: { state?: string }) {
  const verifier = randomBytes(32).toString("base64url");
  const challenge = createHash("sha256").update(verifier).digest("base64url");
  const state = randomBytes(16).toString("base64url");

  session.state = state;
  session.codeVerifier = verifier;

  const url = new URL("https://auth.example.com/authorize");
  url.search = new URLSearchParams({
    response_type: "code",
    client_id: "web-app",
    redirect_uri: "https://app.example.com/callback",
    scope: "openid email profile offline_access",
    state,
    code_challenge: challenge,
    code_challenge_method: "S256",
  }).toString();

  return url.toString();
}

Authorization Code + PKCE vs Implicit

PKCE hält Tokens aus der URL und dem Browserverlauf fern und funktioniert für Public Clients, die kein Secret halten können.

Bevorzugen
// Browser is redirected, then the backend exchanges the code.
const url = new URL("https://auth.example.com/authorize");
url.search = new URLSearchParams({
  response_type: "code",
  client_id: "web-app",
  redirect_uri: "https://app.example.com/callback",
  scope: "openid email",
  state,
  code_challenge: challenge,
  code_challenge_method: "S256",
}).toString();
Vermeiden
// Tokens land in the URL fragment where history,
// logs and referrers can leak them.
const url = new URL("https://auth.example.com/authorize");
url.search = new URLSearchParams({
  response_type: "token",
  client_id: "web-app",
  redirect_uri: "https://app.example.com/callback",
  scope: "openid email",
}).toString();

scopes vs roles

Ein Scope ist das, was der Client in diesem Grant angefordert hat. Eine Rolle ist das, was der Benutzer innerhalb deines Systems ist. Verwechsle die beiden nicht.

Scope
// Requested capability, granted through consent.
const scope = "reports:read reports:export";

if (!req.user.scope.includes("reports:read")) {
  return res.status(403).json({ error: "insufficient_scope" });
}
Role
// Position in your own authorization model.
const role = "analyst";

// A provider scope never substitutes for a local
// role check; map scopes to roles on your server.
if (!["analyst", "admin"].includes(req.user.role)) {
  return res.status(403).json({ error: "forbidden" });
}

Abwägungen

Solltest du auf OAuth aufbauen?

OAuth ist das richtige Werkzeug für Delegation und Integration, aber das falsche für First-Party-Login.

Strengths

  • Keine Passwortteilung

    Benutzer gewähren scoped Zugriff über den Provider, dem sie bereits vertrauen, und können diesen widerrufen, ohne ihre Credentials zu ändern.

  • Eine Integration, viele Provider

    Der gleiche Authorization Code Flow funktioniert über verschiedene Provider hinweg, sodass das Hinzufügen eines zweiten Identity Providers meist nur Konfiguration ist.

  • Von Haus aus widerrufbar

    Access- und Refresh-Tokens können widerrufen werden, und kurze Lebensdauern von Access-Tokens begrenzen den Schadensradius eines Leaks.

Trade-offs

  • Es ist keine Authentifizierung

    OAuth allein beweist, dass ein Client auf eine Ressource zugreifen darf, nicht wer der Benutzer ist. Für die Identität benötigst du OpenID Connect.

  • Gefährliche Fallstricke

    Unpräzise Redirect URIs, fehlender State und im Browser gespeicherte Tokens führen direkt zur Account-Übernahme, wenn man sie ignoriert.

  • Komplexität hat ihren Preis

    Den eigenen Authorization Server zu betreiben bedeutet Key-Management, Consent-Screens, Token-Speicherung und kontinuierliche Security-Reviews.

Häufig gestellte Fragen

Häufig gestellte Fragen

Keep learning

Related topics from the roadmap.

$ Lernen Sie jetzt

Bereit, OAuth 2.0 zu lernen?

Unser interaktives Tutorial führt Sie Schritt für Schritt durch OAuth 2.0 — mit Quizzen und echtem Code, den Sie im Browser ausführen können.