Use Cases (Aktualisiert: 2.6.2026)

OAuth mit Claude Code: PKCE, state, nonce und sichere Token-Speicherung

Praxisguide für OAuth 2.1/2.0 Authorization Code + PKCE mit Claude Code, inklusive state, nonce und Tokens.

OAuth mit Claude Code: PKCE, state, nonce und sichere Token-Speicherung

OAuth ist nicht erledigt, sobald ein “Mit Google anmelden”-Button sichtbar ist. Kleine Fehler können ein falsches Konto verknüpfen, einen Authorization Code wiederverwenden, die Redirect URI austauschen oder Tokens im Browser offenlegen. Dieser Guide zeigt, wie Claude Code eine OAuth-Basis erzeugt, ohne echte Secrets zu sehen, und wie du das Ergebnis sicher prüfst.

Der praktische Standard ist OAuth 2.0 Authorization Code + PKCE, passend zur Richtung von OAuth 2.1. PKCE bedeutet: Beim Login wird ein Challenge-Wert gesendet, beim Token-Tausch beweist die App, dass sie den ursprünglichen Verifier besitzt. Weitere Grundlagen stehen in Claude Code Authentication, Claude Code API Development und Claude Code Getting Started.

Empfohlene Struktur

Nutze Claude Code für Routen, Sessions, Tests und Review-Checklisten. Füge keine echten client secrets, refresh tokens, Produktions-.env oder Screenshots aus Provider-Konsolen ein. Gib Variablennamen, erwartetes Verhalten und Fehlerszenarien vor.

BereichEmpfehlungGrund
FlowAuthorization Code + PKCEVermeidet Token-Leaks älterer Flows
CSRFstate speichern und prüfenStoppt gefälschte Callbacks
OIDC Replaynonce speichern und prüfenErkennt wiederverwendete Identity-Ergebnisse
Redirect URIExakter Allow-List-MatchVerhindert Zielaustausch
TokensServerseitige Session oder verschlüsselter StoreHält lange Tokens aus localStorage heraus
Claude Code InputDummy-Werte und SpezifikationKeine Secrets in Prompts oder Logs

Primärquellen: OAuth 2.1, RFC 7636 PKCE, RFC 9700 OAuth 2.0 Security BCP, OpenID Connect Core, OWASP OAuth2 Cheat Sheet, Claude Code Security.

Drei reale Einsatzfälle

Erstens: ein B2B-SaaS-Adminbereich. Nutzer melden sich über Google Workspace oder Microsoft Entra ID an, aber deine App ordnet sie intern Benutzer, Organisation und Rolle zu. OAuth beweist Identität; deine Autorisierung entscheidet Berechtigungen.

Zweitens: delegierter API-Zugriff. Kalender, Mail, Storage und CRM brauchen Zustimmung, minimale Scopes, verschlüsselte Refresh Tokens und eine saubere Trennung beim Disconnect.

Drittens: interne Tools und MCP-ähnliche Services. Statische API Keys sind am Anfang bequem, werden aber bei Widerruf und Audit schwierig. OAuth/OIDC macht Login, Consent, Ablauf und Identität klarer.

Mobile Apps und SPAs können kein client secret verbergen, daher ist PKCE Pflicht. Auch serverseitige Web-Apps profitieren, weil Reviews einfacher werden.

Lokal lauffähige Demo

Diese Demo braucht keine Google-, Microsoft- oder Auth0-Zugangsdaten. Ein Express-Prozess ist OAuth-Client und Mock Authorization Server zugleich. So siehst du state, nonce, PKCE S256, exakte Redirect-Validierung, Einmal-Codes und serverseitige Token-Speicherung.

Lege die Dateien in einem leeren Ordner an, führe npm install && npm start aus und öffne http://localhost:3000.

{
  "scripts": { "start": "node server.mjs" },
  "dependencies": { "express": "^4.19.2", "express-session": "^1.18.0" },
  "engines": { "node": ">=20" }
}
// server.mjs
import crypto from "node:crypto";
import express from "express";
import session from "express-session";

const app = express();
app.use(express.urlencoded({ extended: false }));
app.use(session({
  name: "oauth_demo_sid",
  secret: "dev-only-change-this-32-byte-secret",
  resave: false,
  saveUninitialized: false,
  cookie: { httpOnly: true, sameSite: "lax", secure: false, maxAge: 10 * 60 * 1000 },
}));

const client = {
  clientId: "claude-code-demo",
  redirectUri: "http://localhost:3000/callback",
  scope: "openid profile email",
};
const authorizationEndpoint = "http://localhost:3000/mock/authorize";
const tokenEndpoint = "http://localhost:3000/mock/token";
const registeredRedirectUris = new Set([client.redirectUri]);
const pendingCodes = new Map();

function randomUrlSafe(bytes = 32) {
  return crypto.randomBytes(bytes).toString("base64url");
}
function sha256Base64Url(value) {
  return crypto.createHash("sha256").update(value).digest("base64url");
}
function fail(res, status, message) {
  return res.status(status).type("text/plain").send(message);
}

app.get("/", (_req, res) => {
  res.type("html").send(`<h1>OAuth PKCE local demo</h1><p><a href="/auth/login">Start login</a></p>`);
});

app.get("/auth/login", (req, res) => {
  const state = randomUrlSafe();
  const nonce = randomUrlSafe();
  const codeVerifier = randomUrlSafe(48);
  const codeChallenge = sha256Base64Url(codeVerifier);
  req.session.oauth = { state, nonce, codeVerifier, createdAt: Date.now() };

  const params = new URLSearchParams({
    response_type: "code",
    client_id: client.clientId,
    redirect_uri: client.redirectUri,
    scope: client.scope,
    state,
    nonce,
    code_challenge: codeChallenge,
    code_challenge_method: "S256",
  });
  res.redirect(`${authorizationEndpoint}?${params}`);
});

app.get("/mock/authorize", (req, res) => {
  const p = req.query;
  const redirectUri = String(p.redirect_uri || "");
  if (p.response_type !== "code") return fail(res, 400, "response_type must be code");
  if (p.client_id !== client.clientId) return fail(res, 400, "unknown client_id");
  if (!registeredRedirectUris.has(redirectUri)) return fail(res, 400, "redirect_uri is not registered exactly");
  if (p.code_challenge_method !== "S256") return fail(res, 400, "PKCE S256 is required");
  if (!p.code_challenge || !p.state || !p.nonce) return fail(res, 400, "missing state, nonce, or PKCE challenge");

  const code = randomUrlSafe(24);
  pendingCodes.set(code, {
    clientId: client.clientId,
    redirectUri,
    codeChallenge: String(p.code_challenge),
    nonce: String(p.nonce),
    expiresAt: Date.now() + 60_000,
    used: false,
  });

  const redirect = new URL(redirectUri);
  redirect.searchParams.set("code", code);
  redirect.searchParams.set("state", String(p.state));
  res.redirect(redirect.toString());
});

app.get("/callback", async (req, res) => {
  const oauth = req.session.oauth;
  const code = String(req.query.code || "");
  const returnedState = String(req.query.state || "");
  if (!oauth) return fail(res, 400, "missing OAuth session");
  if (returnedState !== oauth.state) return fail(res, 403, "state mismatch: possible CSRF or mixed login attempt");

  const response = await fetch(tokenEndpoint, {
    method: "POST",
    headers: { "content-type": "application/x-www-form-urlencoded" },
    body: new URLSearchParams({
      grant_type: "authorization_code",
      code,
      redirect_uri: client.redirectUri,
      client_id: client.clientId,
      code_verifier: oauth.codeVerifier,
    }),
  });
  const tokens = await response.json();
  if (!response.ok) return fail(res, response.status, JSON.stringify(tokens, null, 2));
  if (tokens.nonce !== oauth.nonce) return fail(res, 403, "nonce mismatch: possible replay");

  req.session.oauth = undefined;
  req.session.tokenSet = {
    accessToken: tokens.access_token,
    refreshToken: tokens.refresh_token,
    expiresAt: Date.now() + tokens.expires_in * 1000,
  };
  res.redirect("/dashboard");
});

app.post("/mock/token", (req, res) => {
  const body = req.body;
  const record = pendingCodes.get(body.code);
  if (body.grant_type !== "authorization_code") return res.status(400).json({ error: "unsupported_grant_type" });
  if (!record || record.used || record.expiresAt < Date.now()) return res.status(400).json({ error: "invalid_grant" });
  if (body.client_id !== record.clientId) return res.status(400).json({ error: "invalid_client" });
  if (body.redirect_uri !== record.redirectUri) return res.status(400).json({ error: "invalid_redirect_uri" });
  if (sha256Base64Url(body.code_verifier || "") !== record.codeChallenge) return res.status(400).json({ error: "invalid_code_verifier" });

  record.used = true;
  res.json({ token_type: "Bearer", access_token: randomUrlSafe(32), refresh_token: randomUrlSafe(32), expires_in: 300, nonce: record.nonce });
});

app.get("/dashboard", (req, res) => {
  const tokenSet = req.session.tokenSet;
  if (!tokenSet) return res.redirect("/auth/login");
  const secondsLeft = Math.max(0, Math.floor((tokenSet.expiresAt - Date.now()) / 1000));
  res.type("html").send(`<h1>Logged in</h1><p>Access token is stored server-side, not in localStorage.</p><p>Expires in ${secondsLeft} seconds.</p>`);
});

app.listen(3000, () => console.log("Open http://localhost:3000"));

Wichtig: /auth/login speichert state, nonce und code_verifier in der Session; die Anfrage sendet nur code_challenge; /callback prüft state; /mock/token berechnet S256 neu; /dashboard liest Tokens aus der Server-Session statt aus localStorage.

Prompt für Claude Code

Implementiere OAuth 2.0 Authorization Code + PKCE in Express + TypeScript.
Anforderungen:
- Provider-Konfiguration nur als Umgebungsvariablennamen, keine echten Secrets.
- state, nonce und code_verifier in einer serverseitigen Session speichern.
- redirect_uri exakt gegen den konfigurierten Wert prüfen.
- Nur code_challenge_method=S256 erlauben.
- Access und refresh tokens nie in localStorage speichern.
- Refresh tokens nur verschlüsselt in der Datenbank oder serverseitig speichern.
- Tests für Erfolg, state mismatch, PKCE mismatch, abgelaufenen Code und Code-Wiederverwendung ergänzen.
- Am Ende eine Security-Review-Checkliste ausgeben.

Review-Prompt:

Prüfe diese OAuth-Implementierung gegen RFC 9700, RFC 7636 und OWASP OAuth2 Cheat Sheet.
Suche Secrets in Logs, Snapshots, Frontend-Bundles und Git-Diffs.
Klassifiziere Funde als High, Medium oder Low und schlage Patches vor.

Konkrete Fallen

Prüfe redirect URIs nicht per Prefix. https://app.example.com.evil.test/callback ist nicht deine App. Vergleiche exakt mit registrierten URIs.

Erzeuge state nicht ohne Prüfung. Speichere ihn beim Start, vergleiche ihn im Callback und lösche ihn danach. Für mehrere Tabs nutze kurzlebige Transaktionen pro state.

Erlaube PKCE plain nicht ohne zwingenden Grund. Nutze S256, logge den verifier nicht, lasse Codes schnell ablaufen und nur einmal verwenden.

Bei OIDC reicht nonce nicht. Prüfe JWT-Signatur, issuer, audience, Ablauf und nonce. ID Token steht für Identität, Access Token für API-Zugriff.

Lege lange Tokens nicht in localStorage. Nutze serverseitige Sessions, verschlüsselte Tabellen, kurze Cookie-Laufzeiten und Widerruf. Siehe Claude Code Cookie Management.

Checkliste für bezahlbare Qualität

  • Token-Laufzeiten und Refresh-Verhalten sind dokumentiert.
  • Das Team kann state, nonce und PKCE einfach erklären.
  • Redirect URIs, Scopes und Provider-Einstellungen sind dokumentiert.
  • Logs enthalten keine codes, verifier, tokens oder cookies.
  • Fehlermeldungen verraten keine internen Details.
  • Tests decken Erfolg, mismatch, Ablauf und Wiederverwendung ab.
  • Claude-Code-Diffs wurden gegen offizielle Quellen geprüft.

ClaudeCodeLab Training und Beratung kann daraus einen Team-Workshop machen: vorhandenen OAuth-Flow mitbringen, Risiken kartieren, Tests ergänzen und eine wiederverwendbare Review-Checkliste erhalten. Siehe ClaudeCodeLab training.

Praktische Verifikation

Die Demo deckt Login-Start, Mock-Autorisierung, Callback, Token-Tausch und Dashboard ab. Änderst du state, stoppt die App mit state mismatch. Ist der verifier falsch, kommt invalid_code_verifier. In früheren OAuth-Prototypen lagen die übersehenen Fehler meist bei mehreren Tabs, Zurück-Button, abgelaufenen Codes und versehentlichen Logs. Packe diese Fälle in den ersten Claude-Code-Prompt.

Fazit

OAuth-Qualität entsteht durch Schutz der ganzen Transaktion. Nutze Authorization Code + PKCE, prüfe state und nonce, matche Redirect URIs exakt und halte Tokens serverseitig. Claude Code beschleunigt die Arbeit, braucht aber klare Sicherheitsvorgaben und Review gegen Primärquellen.

#Claude Code #OAuth #authentication #security #TypeScript
Kostenlos

Kostenloses PDF: Claude-Code-Cheatsheet

E-Mail eintragen und eine Seite mit Befehlen, Review-Gewohnheiten und sicheren Workflows herunterladen.

Wir schützen Ihre Daten und senden keinen Spam.

Masa

Über den Autor

Masa

Engineer für praktische Claude-Code-Workflows und Team-Einführung.