Zum Inhalt springen

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.

3 Min. Lesezeit
Sequenzdiagramm des OAuth-2.0-Authorization-Code-Flows zwischen Client, Autorisierungsserver und Ressourcenserver

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.

FlowAnwendungsfallClient-TypNutzer beteiligt?
Authorization Code + PKCEWeb-Apps, mobile Apps, SPAsÖffentlichJa
Client CredentialsService-zu-ServiceVertraulichNein
Device AuthorizationSmart-TVs, CLI-ToolsÖffentlichJa
Refresh TokenVerlängerung der SitzungsdauerBeideNein (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.

tstypescript
// 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}`;
}
tstypescript
// 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.

tstypescript
// ✅ 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

tstypescript
// ❌ 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);
});
tstypescript
// ❌ 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

tstypescript
// 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();
}
TokenLebensdauerSpeicherungRefresh-Strategie
Access Token15-60 MinutenSpeicher oder HTTP-only-CookieRefresh Token verwenden
Refresh Token7-30 TageHTTP-only-Cookie oder sicher serverseitigErneute Authentifizierung bei Ablauf
ID TokenWie der Access TokenSpeicherWird nicht erneuert — wird bei der Aktualisierung des Access Tokens neu abgerufen

Die wichtigsten Punkte

  1. Authorization Code + PKCE verwenden für jede nutzerorientierte Anwendung — Web, Mobile oder SPA
  2. Client Credentials ist nur für Service-zu-Service-Kommunikation gedacht — Client Secrets dürfen nie an den Browser gelangen
  3. Den state-Parameter immer validieren — wird das übersprungen, entsteht eine CSRF-Lücke
  4. ID-Token-Signaturen verifizieren — wer ohne Verifizierung dekodiert, vertraut unsignierten Daten
  5. Kurzlebige Access Tokens plus Refresh-Rotation verkleinern das Zeitfenster, in dem ein gestohlener Token Schaden anrichten kann
  6. Der Implicit Flow ist veraltet — auf Authorization Code + PKCE migrieren
Wilfredo Rujel

Wilfredo Rujel

Full-Stack-Softwareentwickler

Diesen Beitrag teilenX