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:
| Rolle | Was sie ist | Beispiel |
|---|---|---|
| Resource Owner | Der User | Person, die sich einloggen will |
| Client | Die App, die Zugriff will | Web-App, Mobile-App, CLI |
| Authorization Server | Der Token-Aussteller | Google Auth, Auth0, Keycloak |
| Resource Server | Die API mit den Daten | Google 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:
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):
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:
| Token | Wer ist Audience? | Was sagt es? |
|---|---|---|
| Access Token | Resource Server (API) | „Diese App darf im Namen von User X auf Scope Y zugreifen" |
| ID Token | Client selbst | „User X ist authentifiziert, hier sind seine Identity-Claims" |
| Refresh Token | Authorization 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).
issmuss zum Authorization Server passen.audmuss die eigeneclient_identhalten.expmuss 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:
| Flow | Wann |
|---|---|
| Authorization Code + PKCE | Universell — Web, SPA, Mobile, Server |
| Client Credentials | Service-to-Service (kein User involviert) |
| Device Authorization Grant | Geräte ohne Browser (TV, IoT) — RFC 8628 |
| Refresh Token | Token-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 registrierthttps://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.exampleals 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
localhostim Dev). - Pre-Registration aller akzeptierten URIs beim Client-Setup.
// 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:
// 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-Loginemail— E-Mail-Adresse des Usersprofile— Name, Bildhttps://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:
| Typ | Beispiele | Geheimnis-Storage | Authentifizierung |
|---|---|---|---|
| Confidential Client | Server-side Apps, Backend-APIs | Möglich | client_secret plus PKCE |
| Public Client | SPAs, Mobile-Apps, CLI | Nicht möglich | Nur 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
- RFC 9700 — OAuth 2.0 Security Best Current Practice
- RFC 6749 — OAuth 2.0 (Original)
- RFC 7636 — PKCE
- OpenID Connect Core 1.0
- RFC 8628 — Device Authorization Grant
- RFC 9068 — JWT Profile for OAuth Access Tokens
- OWASP OAuth2 Cheat Sheet
- oauth.net OAuth 2.1 Overview
- openid-client (Node.js)