OAuth 2.0 ist seit 2012 der Standard für delegierte Autorisierung — „Anmelden mit Google", „Zugriff auf GitHub-Repos für CI/CD". OAuth 2.1 (Konsolidierung 2024/2025, RFC 9700) räumt mit den unsicheren Flows der ersten Generation auf: Implicit und ROPC sind out, Authorization Code mit PKCE ist der universelle Default. OIDC legt einen Identity-Layer drüber. Dieser Artikel zeigt die Flows, die Pflicht-Parameter und die häufigsten Bug-Klassen.

Die drei Rollen

OAuth/OIDC arbeitet mit drei (bzw. vier) Rollen:

RolleWas sie istBeispiel
Resource OwnerDer UserPerson, die sich einloggen will
ClientDie App, die Zugriff willWeb-App, Mobile-App, CLI
Authorization ServerDer Token-AusstellerGoogle Auth, Auth0, Keycloak
Resource ServerDie API mit den DatenGoogle Gmail-API, GitHub-API

In OIDC-Setups ist der Authorization Server gleichzeitig der Identity Provider (IdP) — er stellt nicht nur Tokens für Resource-Access aus, sondern auch ID-Tokens mit User-Identitäts-Daten.

Der moderne Default: Authorization Code Flow mit PKCE

PKCE (Proof Key for Code Exchange, RFC 7636) ist seit OAuth 2.1 Pflicht für alle Client-Typen — Web-Apps, SPAs, Mobile-Apps, sogar Server-side-Apps. Schützt vor Authorization-Code-Interception.

Flow-Schritte:

Plain auth-code-pkce-flow.txt
1. Client erzeugt `code_verifier` (zufälliger String, 43-128 Zeichen)
   und `code_challenge = BASE64URL(SHA256(code_verifier))`.

2. Client redirected User zum Authorization Server:
   GET /authorize?response_type=code
                  &client_id=abc123
                  &redirect_uri=https://app.example.com/cb
                  &scope=openid+email
                  &state=random-csrf-token
                  &code_challenge=xyz789
                  &code_challenge_method=S256

3. User loggt sich ein, gibt Consent.

4. Authorization Server redirected zurück mit Code:
   https://app.example.com/cb?code=AUTH_CODE&state=random-csrf-token

5. Client prüft `state` (Anti-CSRF). Dann tauscht Code gegen Token:
   POST /token
      grant_type=authorization_code
      code=AUTH_CODE
      redirect_uri=https://app.example.com/cb
      client_id=abc123
      code_verifier=ORIGINAL_VERIFIER

6. Authorization Server prüft: ist SHA256(code_verifier) == code_challenge?
   Ja → Access-Token (+ ID-Token bei OIDC) zurück.

Warum PKCE strukturell schützt: Ein Angreifer, der den Auth-Code im Redirect abfängt (z. B. via Open-Redirect, schadhafte App auf demselben Gerät), kann ihn nicht einlösen — weil er den code_verifier nicht hat. Der code_verifier lebt nur im Speicher des legitimen Clients.

Client-Code (Beispiel: Node.js mit openid-client):

JavaScript oauth-pkce-node.js
import { Issuer, generators } from 'openid-client';

const issuer = await Issuer.discover('https://auth.example.com');
const client = new issuer.Client({
  client_id: 'my-app',
  redirect_uris: ['https://app.example.com/cb'],
  response_types: ['code'],
  token_endpoint_auth_method: 'none', // SPA / public client
});

// Vor Redirect zum Auth-Server
const code_verifier = generators.codeVerifier();
const code_challenge = generators.codeChallenge(code_verifier);
const state = generators.state();

// verifier und state in Session speichern
req.session.code_verifier = code_verifier;
req.session.oauth_state = state;

const authUrl = client.authorizationUrl({
  scope: 'openid email',
  state,
  code_challenge,
  code_challenge_method: 'S256',
});
res.redirect(authUrl);

// Callback-Handler
app.get('/cb', async (req, res) => {
  const params = client.callbackParams(req);
  const tokenSet = await client.callback(
    'https://app.example.com/cb',
    params,
    { state: req.session.oauth_state, code_verifier: req.session.code_verifier }
  );
  // tokenSet.access_token, tokenSet.id_token (bei openid scope)
});

Was ist OIDC?

OAuth allein ist ein Autorisierungs-Protokoll: „App darf im Namen des Users auf API X zugreifen." Es sagt nichts darüber, wer der User ist.

OIDC (OpenID Connect) legt darüber einen Identity-Layer: zusätzlich zum Access-Token bekommt der Client ein ID-Token — ein JWT mit User-Identitäts-Claims (sub, email, name, picture, ...).

Wichtige Unterscheidung:

TokenWer ist Audience?Was sagt es?
Access TokenResource Server (API)„Diese App darf im Namen von User X auf Scope Y zugreifen"
ID TokenClient selbst„User X ist authentifiziert, hier sind seine Identity-Claims"
Refresh TokenAuthorization Server„Stelle mir neue Access/ID-Tokens aus"

Häufiger Bug: ID-Token wird fälschlich an die API geschickt (statt Access-Token), oder Access-Token wird fälschlich vom Client zur Identitäts-Bestimmung gelesen (statt ID-Token). Beides ist semantisch falsch.

ID-Token-Validierung beim Client (Pflicht-Schritte):

  • Signatur prüfen (JWKS-Endpoint des Issuer).
  • iss muss zum Authorization Server passen.
  • aud muss die eigene client_id enthalten.
  • exp muss in der Zukunft sein.
  • nonce (falls gesetzt im Auth-Request) muss zum gespeicherten Wert passen.
  • Bei Multiple Audiences: azp-Claim prüfen.

Vertieft in jwt-stateless-tokens.

Deprecated Flows — was nicht mehr genutzt werden darf

OAuth 2.1 hat aufgeräumt. Drei Flows sind explizit deprecated:

1. Implicit Flow (response_type=token):

War für SPAs gedacht (vor PKCE), liefert Access-Token direkt im URL-Fragment des Redirects. Probleme:

  • Token taucht in Browser-History, Referer-Header, Server-Logs auf.
  • Kein Refresh-Token-Support.
  • PKCE löst dasselbe Problem strukturell sauberer.

Status: Mit OAuth 2.1 entfernt. Existing-Code migrieren auf Auth Code + PKCE.

2. Resource Owner Password Credentials (ROPC, grant_type=password):

Client schickt User-Passwort direkt zum Authorization Server. Probleme:

  • Client sieht das User-Passwort — widerspricht dem OAuth-Grundprinzip „Client braucht das Passwort nie".
  • MFA funktioniert nicht.
  • Phishing-Vektoren.

Status: Mit OAuth 2.1 entfernt. War nur für Legacy-Migration gedacht.

3. Hybrid Flow (response_type=code id_token):

OIDC-Variante, die Code UND ID-Token im Redirect zurückgibt. Inzwischen als unnötig komplex bewertet — der pure Code-Flow ist sicherer und ausreichend.

Status: Wird in OIDC-Konformitäts-Tests nicht mehr aktiv gefördert.

Was bleibt im OAuth 2.1:

FlowWann
Authorization Code + PKCEUniversell — Web, SPA, Mobile, Server
Client CredentialsService-to-Service (kein User involviert)
Device Authorization GrantGeräte ohne Browser (TV, IoT) — RFC 8628
Refresh TokenToken-Rotation, mit Rotation-Detection

Redirect-URI-Validierung

Die redirect_uri ist eine zentrale Schwachstellen-Quelle. Der Authorization Server sendet den Auth-Code an die im Request angegebene URI. Wenn ein Angreifer eine fremde URI hier einschmuggeln kann, geht der Code zu ihm.

Schwache Validierung:

  • Prefix-Match (https://app.example.com*) — Angreifer registriert https://app.example.com.attacker.example.
  • Wildcard-Subdomain (https://*.example.com) — XSS auf einer beliebigen Subdomain reicht.
  • Open Redirect auf der eigenen Domain — https://app.example.com/redirect?to=attacker.example als Redirect-URI.

Pflicht (RFC 9700):

  • Exact-String-Match zwischen registrierter und Request-redirect_uri. Keine Wildcards, keine Prefix-Matches.
  • HTTPS-Pflicht (außer für localhost im Dev).
  • Pre-Registration aller akzeptierten URIs beim Client-Setup.
JavaScript redirect-uri-validation.js
// Auth-Server-Seite
const REGISTERED_REDIRECT_URIS = new Set([
  'https://app.example.com/callback',
  'https://app.example.com/callback/alt',
]);

function validateRedirectUri(requestUri) {
  // EXACT MATCH, keine Patterns
  return REGISTERED_REDIRECT_URIS.has(requestUri);
}

state und nonce — CSRF und Replay-Schutz

state-Parameter (Pflicht):

Vor dem Redirect zum Auth-Server generiert der Client einen zufälligen state-Wert, speichert ihn lokal (Session, Memory) und packt ihn in den state-Query-Parameter. Nach dem Callback prüft der Client, ob der zurückgegebene state zum gespeicherten passt.

Was es verhindert:

  • OAuth-CSRF: Angreifer initiiert einen OAuth-Flow auf seinem Konto, fängt den Auth-Code ab und bringt das Opfer dazu, den Callback-URL zu besuchen. Ohne state-Check würde der Code im Opfer-Browser eingelöst — Account-Verknüpfung mit Angreifer-Konto.

nonce-Parameter (OIDC, Pflicht für Auth-Code-Flow mit ID-Token):

Ähnlich wie state, aber speziell für ID-Token-Replay-Schutz. Im ID-Token kommt der nonce wieder zurück; der Client prüft, ob er zum Original passt.

Was es verhindert:

  • ID-Token-Replay: Angreifer fängt ein altes ID-Token ab, schickt es an einen anderen Client. Ohne nonce-Check könnte es akzeptiert werden.

Implementierung:

JavaScript state-nonce-validation.js
// Vor Redirect
const state = crypto.randomBytes(32).toString('base64url');
const nonce = crypto.randomBytes(32).toString('base64url');
req.session.oauth_state = state;
req.session.oauth_nonce = nonce;

const authUrl = new URL('https://auth.example.com/authorize');
authUrl.searchParams.set('state', state);
authUrl.searchParams.set('nonce', nonce);
// ...

// Callback-Handler
app.get('/cb', async (req, res) => {
  if (req.query.state !== req.session.oauth_state) {
    return res.status(400).send('Invalid state');
  }
  const idToken = await exchangeCodeForTokens(req.query.code);
  const decoded = await verifyIdToken(idToken);
  if (decoded.nonce !== req.session.oauth_nonce) {
    return res.status(400).send('Invalid nonce');
  }
  // sicher ab hier
});

Scopes — granulare Berechtigungen

Der scope-Parameter beschreibt, welche Berechtigung der Client beim Resource Server bekommt.

Beispiel-Scopes (Google):

  • openid — OIDC-Login
  • email — E-Mail-Adresse des Users
  • profile — Name, Bild
  • https://www.googleapis.com/auth/drive.readonly — Drive-Read

Sicherheits-Regeln für Scopes:

  • Least Privilege: nur die Scopes anfragen, die tatsächlich gebraucht werden.
  • Granular — separate Scopes für Read und Write, statt eines „all access"-Scopes.
  • User-sichtbarer Consent — der User soll auf der Consent-Seite sehen, was die App will.
  • Scope-Validierung server-seitig — auch wenn der User Consent gegeben hat, Resource-Server prüft pro Endpoint, ob der nötige Scope im Token ist.

Client-Typen und ihre Sicherheits-Anforderungen

OAuth 2.1 kennt zwei Client-Typen mit unterschiedlichen Anforderungen:

TypBeispieleGeheimnis-StorageAuthentifizierung
Confidential ClientServer-side Apps, Backend-APIsMöglichclient_secret plus PKCE
Public ClientSPAs, Mobile-Apps, CLINicht möglichNur PKCE

Public Clients dürfen kein client_secret verwenden, weil ein:e Angreifer:in dieses Geheimnis aus dem App-Code extrahieren kann. PKCE ersetzt strukturell das Secret bei Public Clients.

Confidential Clients können zusätzlich zum PKCE-Schutz ein client_secret mitschicken — das ist eine Defense-in-Depth-Schicht. Bei sehr sensiblen Setups: Private Key JWT Client Authentication (RFC 7521/7523) statt Shared Secret.

Hybrid-Setup (Backend-for-Frontend, BFF):

Eine moderne Architektur für SPAs: Frontend ist Public Client, aber kommuniziert mit einem eigenen Backend, das als Confidential Client gegenüber dem Authorization Server agiert. Tokens bleiben im Backend, Frontend bekommt nur Session-Cookies. Reduziert die XSS-Angriffsfläche erheblich.

Besonderheiten

OAuth ist Autorisierung, nicht Authentifizierung — pure

Reines OAuth sagt nur „dieser Client darf X". Es sagt nicht „der User ist authentifiziert". Wer OAuth-Flows als Login-Mechanismus nutzt, baut implizit auf das ID-Token aus OIDC. OAuth-only ohne OIDC für Login ist eine Fehl-Architektur — der Client weiß formal nicht, wer der User ist. Konsequenz: für „Login with X" immer OIDC-Erweiterung nutzen, nie nur OAuth.

Token-Audience-Verwechslung als Bug-Klasse

ID-Token und Access-Token haben unterschiedliche Audiences. Wenn der Resource Server fälschlich ein ID-Token akzeptiert (oder umgekehrt), kann das zu Auth-Bypass führen. RFC 9068 standardisiert das Access-Token-Format als JWT — macht die Unterscheidung explizit über den typ-Header (at+jwt für Access-Token).

"Login with Google" hat Account-Linking-Fallen

Wenn der User vorher einen Passwort-Account mit derselben E-Mail hatte und jetzt zum ersten Mal Google-Login nutzt — was passiert? Klassische Falle: automatisches Linking ohne E-Mail-Verifikation. Angreifer kann mit Passwort auf Opfer-Mail-Adresse einen Account anlegen, wenn das Opfer dann Google-Login macht, landen beide im selben Account. Saubere Implementierung: E-Mail-Verifikation als Pflicht-Schritt vor Linking.

JWKS-Caching ist nötig, aber rotiert auch

Der OIDC-Authorization-Server publiziert seinen Public Key unter /.well-known/jwks.json. Validator-Code cached das (sonst Latenz pro Request). Bei Schlüssel-Rotation: alter Key bleibt eine Weile im JWKS (Overlap-Period), neuer Key wird publiziert. kid-Header im JWT zeigt, welcher Key zu nehmen ist. Wer JWKS hardcoded oder zu lang cached: Login-Bruch bei Rotation.

Refresh-Token-Rotation mit Detection

Bei jedem Refresh ein neues Refresh-Token ausgeben, altes invalidieren. Wenn ein altes Refresh-Token nochmal genutzt wird — Token-Theft erkannt, alle Tokens des Users invalidieren. Standard-Pattern in RFC 9700. Implementiert von Auth0, Okta, Cognito, Keycloak.

Device Authorization Grant für TVs und IoT

Geräte ohne Browser (Smart-TV, IoT-Gerät, CLI) nutzen den Device Authorization Grant: Gerät zeigt einen Code, User loggt sich auf einem anderen Gerät (Phone) ein und gibt den Code ein. GitHub-CLI, AWS-CLI, gcloud machen es so. Sicherer als Username/Passwort über minimal-UI.

OAuth 2.1 ist ein Konsolidierungs-Standard, kein Major-Change

OAuth 2.1 ist 2024/2025 als Update zu OAuth 2.0 entstanden. Hauptänderungen: Implicit und ROPC entfernt, PKCE Pflicht, Redirect-URI Exact-Match, Refresh-Token-Rotation. Praktisch heißt das: Apps, die schon OAuth 2.0 Security BCP (RFC 9700) gefolgt sind, sind automatisch 2.1-konform.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Authentifizierung (Entwickler)

Zur Übersicht