OAuth-2.0-Flows entmystifiziert
Authorization Code, PKCE, Client Credentials — welcher OAuth-Flow für welchen Anwendungstyp passt und welche Sicherheitsfallen sich in jedem verbergen.

OAuth 2.0 ist der Standard für delegierte Autorisierung, aber seine Spezifikation ist verwirrend flexibel. Mehrere Grant-Types, optionale Parameter und anbieterspezifische Erweiterungen erschweren es, den richtigen Flow für die eigene Anwendung zu bestimmen. Die meisten Sicherheitslücken in OAuth-Implementierungen entstehen dadurch, dass der falsche Flow gewählt oder wichtige Validierungsschritte übersprungen werden.
Die vier wichtigsten Flows
OAuth 2.0 definiert mehrere Grant-Types. In der Praxis decken vier davon so gut wie jeden modernen Anwendungsfall ab.
| Flow | Anwendungsfall | Client-Typ | Nutzer beteiligt? |
|---|---|---|---|
| Authorization Code + PKCE | Web-Apps, mobile Apps, SPAs | Öffentlich | Ja |
| Client Credentials | Service-zu-Service | Vertraulich | Nein |
| Device Authorization | Smart-TVs, CLI-Tools | Öffentlich | Ja |
| Refresh Token | Verlängerung der Sitzungsdauer | Beide | Nein (nach der Authentifizierung) |
Der Implicit Flow und der Resource Owner Password Flow gelten als veraltet. Wer noch einen von beiden einsetzt, sollte jetzt migrieren.
Authorization Code mit PKCE
Dies ist der empfohlene Flow für jede Anwendung, bei der sich ein Nutzer anmeldet. PKCE (Proof Key for Code Exchange) verhindert, dass ein abgefangener Authorization Code missbraucht werden kann.
// Step 1: Generate PKCE challenge
import crypto from "crypto";
function generatePKCE() {
const verifier = crypto.randomBytes(32).toString("base64url");
const challenge = crypto
.createHash("sha256")
.update(verifier)
.digest("base64url");
return { verifier, challenge };
}
// Step 2: Redirect user to authorization server
function getAuthorizationUrl(pkce: { challenge: string }) {
const params = new URLSearchParams({
response_type: "code",
client_id: process.env.OAUTH_CLIENT_ID!,
redirect_uri: "https://myapp.com/callback",
scope: "openid profile email",
state: crypto.randomBytes(16).toString("hex"),
code_challenge: pkce.challenge,
code_challenge_method: "S256",
});
return `https://auth.provider.com/authorize?${params}`;
}// Step 3: Exchange authorization code for tokens
async function exchangeCode(code: string, verifier: string) {
const response = await fetch("https://auth.provider.com/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "authorization_code",
code,
redirect_uri: "https://myapp.com/callback",
client_id: process.env.OAUTH_CLIENT_ID!,
code_verifier: verifier, // PKCE verification
}),
});
return response.json();
// { access_token: "...", refresh_token: "...", id_token: "...", expires_in: 3600 }
}Der Parameter state schützt vor CSRF-Angriffen. Der code_verifier beweist, dass derselbe Client, der den Flow gestartet hat, ihn auch abschließt.
Client Credentials Flow
Gedacht für die Kommunikation zwischen Diensten, bei der kein Nutzer beteiligt ist. Der Client authentifiziert sich mit seinen eigenen Zugangsdaten.
// ✅ Service-to-service authentication
async function getServiceToken() {
const response = await fetch("https://auth.provider.com/token", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Authorization: `Basic ${Buffer.from(
`${process.env.CLIENT_ID}:${process.env.CLIENT_SECRET}`,
).toString("base64")}`,
},
body: new URLSearchParams({
grant_type: "client_credentials",
scope: "api:read api:write",
}),
});
return response.json();
}
// Cache the token until near expiry
let cachedToken: { token: string; expiresAt: number } | null = null;
async function getValidToken(): Promise<string> {
if (cachedToken && cachedToken.expiresAt > Date.now() + 60000) {
return cachedToken.token;
}
const data = await getServiceToken();
cachedToken = {
token: data.access_token,
expiresAt: Date.now() + data.expires_in * 1000,
};
return cachedToken.token;
}Client Secrets dürfen niemals im Frontend-Code offengelegt werden. Client Credentials ist ausschließlich für serverseitige Anwendungen gedacht.
Häufige Sicherheitsfehler
// ❌ Not validating the state parameter — CSRF vulnerability
app.get("/callback", async (req, res) => {
const { code } = req.query;
const tokens = await exchangeCode(code); // Missing state check!
});
// ✅ Always validate state
app.get("/callback", async (req, res) => {
const { code, state } = req.query;
const savedState = req.session.oauthState;
if (!state || state !== savedState) {
return res.status(403).json({ error: "Invalid state parameter" });
}
delete req.session.oauthState;
const tokens = await exchangeCode(code, req.session.pkceVerifier);
});// ❌ Not validating the ID token
const user = jwt.decode(tokens.id_token); // Decode without verify!
// ✅ Verify the ID token signature, issuer, and audience
const user = jwt.verify(tokens.id_token, publicKey, {
issuer: "https://auth.provider.com",
audience: process.env.OAUTH_CLIENT_ID,
algorithms: ["RS256"],
});Speicherung und Lebenszyklus von Tokens
// Access token: short-lived, used for API calls
// Refresh token: long-lived, used to get new access tokens
async function refreshAccessToken(refreshToken: string) {
const response = await fetch("https://auth.provider.com/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "refresh_token",
refresh_token: refreshToken,
client_id: process.env.OAUTH_CLIENT_ID!,
}),
});
if (!response.ok) {
// Refresh token expired or revoked — user must re-authenticate
throw new AuthenticationError("Session expired");
}
return response.json();
}| Token | Lebensdauer | Speicherung | Refresh-Strategie |
|---|---|---|---|
| Access Token | 15-60 Minuten | Speicher oder HTTP-only-Cookie | Refresh Token verwenden |
| Refresh Token | 7-30 Tage | HTTP-only-Cookie oder sicher serverseitig | Erneute Authentifizierung bei Ablauf |
| ID Token | Wie der Access Token | Speicher | Wird nicht erneuert — wird bei der Aktualisierung des Access Tokens neu abgerufen |
Die wichtigsten Punkte
- Authorization Code + PKCE verwenden für jede nutzerorientierte Anwendung — Web, Mobile oder SPA
- Client Credentials ist nur für Service-zu-Service-Kommunikation gedacht — Client Secrets dürfen nie an den Browser gelangen
- Den
state-Parameter immer validieren — wird das übersprungen, entsteht eine CSRF-Lücke - ID-Token-Signaturen verifizieren — wer ohne Verifizierung dekodiert, vertraut unsignierten Daten
- Kurzlebige Access Tokens plus Refresh-Rotation verkleinern das Zeitfenster, in dem ein gestohlener Token Schaden anrichten kann
- Der Implicit Flow ist veraltet — auf Authorization Code + PKCE migrieren


