Security & Auth

OAuth2 & OIDC mit Claude Code: Authentication 2026

6. Mai 2026 · 12 min Lesezeit · Von SpockyMagicAI

OAuth2 Authorization Code Flow mit PKCE, Refresh Tokens, OIDC ID-Token, Auth0 und Keycloak Integration — Claude Code als Authentication-Experte.

Inhalt dieses Artikels

  1. OAuth2 Grundlagen: Flows, Scopes & Tokens
  2. PKCE – Proof Key for Code Exchange
  3. OIDC ID Token: JWT-Struktur & Claims
  4. Refresh Token Rotation & Token-Strategien
  5. Auth0 Integration mit Next.js
  6. Keycloak Self-Hosted: Docker & Node.js

Authentication ist 2026 komplexer denn je — und gleichzeitig kritischer. Mit Claude Code als KI-Assistenten lassen sich OAuth2-Flows und OIDC-Implementierungen schneller entwickeln, debuggen und sicher gestalten. In diesem Artikel zeigen wir, wie man die wichtigsten Authentication-Patterns professionell umsetzt — von PKCE über Token-Rotation bis hin zu kompletten Auth0- und Keycloak-Integrationen.

Ob du eine Single Page Application absicherst, eine API mit Client Credentials schützt oder eine Self-Hosted-Lösung mit Keycloak betreibst — die Grundprinzipien bleiben gleich: kein Vertrauen ohne Verifikation, sichere Token-Speicherung und regelmäßige Rotation.

1. OAuth2 Grundlagen: Flows, Scopes & Tokens

OAuth2 ist kein Authentifizierungsprotokoll — es ist ein Autorisierungsframework. Wer das vergisst, baut gefährliche Systeme. OAuth2 delegiert den Zugriff auf Ressourcen, ohne Passwörter weiterzugeben. Für echte Authentifizierung braucht es OIDC (OpenID Connect), das OAuth2 erweitert.

Die vier OAuth2 Grant Types im Überblick

Grant Type Anwendungsfall Sicherheitslevel Empfohlen
Authorization Code + PKCE Web Apps, SPAs, Mobile Sehr hoch ✅ Ja
Client Credentials Machine-to-Machine (M2M) Hoch (kein User) ✅ Für M2M
Device Flow TV, CLI, IoT Mittel ✅ Für Devices
Implicit (veraltet) Alte SPAs Niedrig ❌ Nein
Password (veraltet) Legacy-Systeme Sehr niedrig ❌ Absolut nicht

Authorization Code Flow — der Standard 2026

Browser/Client Authorization Server Resource Server │ │ │ │── GET /authorize? │ │ response_type=code │ │ client_id=<id> │ │ redirect_uri=<uri> │ │ scope=openid profile email │ │ code_challenge=<S256> ────>│ │ │ │ [User Login + Consent] │ │ │ │<── 302 redirect_uri?code=AUTH_CODE │ │ │── POST /token │ │ grant_type=authorization_code│ │ code=AUTH_CODE │ │ code_verifier=VERIFIER ───>│ │ │ │<── access_token + refresh_token + id_token │ │ │ │── GET /api/resource │ │ │ Authorization: Bearer <AT> ─────────────────────────────>│ │ │ │ │<───────────────────────────────────────────── 200 OK + Data

Scopes: Was darf der Token?

Scopes sind kommagetrennte Berechtigungsanfragen. Der Authorization Server kann gewährte Scopes einschränken — der Client muss das prüfen.

Typische Scope-Hierarchie

Token-Typen und ihre Lebensdauer

// Token-Lebensdauer Best Practices 2026
// Access Token — kurzlebig, stateless (JWT) oder opaque accessToken: { format: "JWT", // oder "opaque" (Reference Token) lifetime: "15min", // EMPFEHLUNG: 5-30 Minuten storage: "memory", // NIEMALS localStorage! algorithm: "RS256" // Asymmetrisch > HS256 }, // Refresh Token — langlebig, opaque, rotiert refreshToken: { format: "opaque", // NIEMALS als JWT lifetime: "30days", // Absolutes Maximum storage: "httpOnly cookie", // SameSite=Strict rotation: true // Rotation bei jedem Use }, // ID Token — OIDC, NUR für Authentifizierung idToken: { format: "JWT", lifetime: "1hour", usage: "auth-only", // NIEMALS als API Bearer Token! validate: ["iss", "aud", "exp", "nonce"] }

Client Credentials Flow — Machine-to-Machine

// m2m-auth.ts — Service-to-Service Authentication
import axios from 'axios'; interface TokenCache { accessToken: string; expiresAt: number; } class M2MAuthClient { private cache: TokenCache | null = null; private readonly tokenUrl: string; private readonly clientId: string; private readonly clientSecret: string; constructor(domain: string, clientId: string, clientSecret: string) { this.tokenUrl = `https://${domain}/oauth/token`; this.clientId = clientId; this.clientSecret = clientSecret; } async getAccessToken(audience: string): Promise<string> { // Cache-Check mit 60s Puffer vor Ablauf if (this.cache && this.cache.expiresAt > Date.now() + 60000) { return this.cache.accessToken; } const response = await axios.post(this.tokenUrl, { grant_type: 'client_credentials', client_id: this.clientId, client_secret: this.clientSecret, audience, }, { headers: { 'Content-Type': 'application/json' } }); const { access_token, expires_in } = response.data; this.cache = { accessToken: access_token, expiresAt: Date.now() + (expires_in * 1000), }; return access_token; } }

Achtung: Client Credentials Flow darf NIEMALS im Browser verwendet werden — der Client Secret wäre öffentlich sichtbar. Dieser Flow gehört ausschließlich in serverseitige Dienste.

2. PKCE – Proof Key for Code Exchange

PKCE (RFC 7636) ist seit 2019 Pflichtstandard für öffentliche Clients und seit 2025 auch für vertrauliche Clients empfohlen. Es verhindert Authorization Code Interception Attacks — selbst wenn ein Angreifer den Authorization Code abfängt, kann er ihn ohne den geheimen code_verifier nicht einlösen.

Wie PKCE funktioniert

Die zwei Hauptkomponenten

  1. code_verifier — ein zufälliger, kryptographisch sicherer String (43–128 Zeichen, Base64url-codiert)
  2. code_challenge — SHA-256 Hash des code_verifier, Base64url-codiert (S256-Methode)

Der Client sendet den code_challenge beim Authorization Request, behält den code_verifier lokal. Beim Token Exchange sendet er den Verifier — der Server prüft: SHA256(verifier) == challenge.

// pkce.ts — PKCE Implementation von Grund auf
import { subtle } from 'crypto'; /** * Erzeugt einen kryptographisch sicheren code_verifier * Länge: 128 Bytes → ~171 Zeichen Base64url (RFC 7636: min 43, max 128 Zeichen) */ export function generateCodeVerifier(): string { const array = new Uint8Array(64); crypto.getRandomValues(array); return base64UrlEncode(Array.from(array).map(b => String.fromCharCode(b)).join('')); } /** * Erzeugt code_challenge via S256 (SHA-256) * Formel: BASE64URL(SHA256(ASCII(code_verifier))) */ export async function generateCodeChallenge(verifier: string): Promise<string> { const encoder = new TextEncoder(); const data = encoder.encode(verifier); const digest = await subtle.digest('SHA-256', data); const hashArray = Array.from(new Uint8Array(digest)); return base64UrlEncode(hashArray.map(b => String.fromCharCode(b)).join('')); } function base64UrlEncode(input: string): string { return btoa(input) .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=/g, ''); // Kein Padding! } // ─── Verwendung ─────────────────────────────────────────────────────────── export async function startAuthFlow(config: OAuthConfig) { const verifier = generateCodeVerifier(); const challenge = await generateCodeChallenge(verifier); const state = crypto.randomUUID(); // CSRF-Schutz const nonce = crypto.randomUUID(); // Replay-Schutz (OIDC) // Sicher in sessionStorage speichern (nur für diese Session) sessionStorage.setItem('pkce_verifier', verifier); sessionStorage.setItem('oauth_state', state); sessionStorage.setItem('oidc_nonce', nonce); const params = new URLSearchParams({ response_type: 'code', client_id: config.clientId, redirect_uri: config.redirectUri, scope: 'openid profile email offline_access', state, nonce, code_challenge: challenge, code_challenge_method: 'S256', }); window.location.href = `${config.authorizationEndpoint}?${params.toString()}`; }

Callback-Handler mit State-Validierung

// callback.ts — Authorization Code einlösen
export async function handleCallback(callbackUrl: string, config: OAuthConfig) { const params = new URL(callbackUrl).searchParams; const code = params.get('code'); const state = params.get('state'); const error = params.get('error'); if (error) throw new Error(`OAuth Error: ${error} - ${params.get('error_description')}`); // State validieren — CSRF-Angriff verhindern const savedState = sessionStorage.getItem('oauth_state'); if (!state || state !== savedState) { throw new Error('State mismatch — möglicher CSRF-Angriff!'); } const verifier = sessionStorage.getItem('pkce_verifier'); if (!verifier || !code) throw new Error('Fehlende PKCE-Parameter'); // Token Exchange — IMMER server-side in Production! const tokenResponse = await fetch(config.tokenEndpoint, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'authorization_code', client_id: config.clientId, redirect_uri: config.redirectUri, code: code, code_verifier: verifier, // Hier der Beweis! }), }); if (!tokenResponse.ok) { const err = await tokenResponse.json(); throw new Error(`Token-Exchange fehlgeschlagen: ${err.error_description}`); } // Cleanup — einmalig verwendet sessionStorage.removeItem('pkce_verifier'); sessionStorage.removeItem('oauth_state'); return await tokenResponse.json(); }

Best Practice: Auch bei vertraulichen Clients (mit Client Secret) sollte PKCE zusätzlich verwendet werden. Double-Layer-Security schadet nie. Auth0 und Okta verlangen PKCE seit 2024 für alle neuen Anwendungen.

3. OIDC ID Token: JWT-Struktur & Claims

OIDC — OpenID Connect — ist die Schicht über OAuth2, die echte Authentifizierung ermöglicht. Kern ist der ID Token: ein JWT, das kryptographisch signiert ist und beweist, wer der Benutzer ist und wann er sich authentifiziert hat.

Anatomie eines ID Tokens

Ein JWT besteht aus drei Base64url-codierten Teilen, getrennt durch Punkte: Header.Payload.Signature

// ID Token Payload — dekodiert (echte Struktur)
// Header { "alg": "RS256", // Algorithmus — IMMER asymmetrisch verwenden "typ": "JWT", "kid": "abc123def456" // Key ID — für JWKS-Lookup } // Payload — Standard OIDC Claims { // Pflicht-Claims (RFC 7519) "iss": "https://auth.beispiel.de", // Aussteller "sub": "auth0|64a1b2c3d4e5f6789", // Subject (User ID) "aud": "meine-app-client-id", // Zielgruppe "exp": 1746568800, // Ablauf (Unix Timestamp) "iat": 1746565200, // Ausgestellt am // OIDC-spezifische Claims "nonce": "a7f3c1b2-replay-schutz", // Replay-Angriff verhindern "auth_time": 1746565200, // Zeitpunkt der Authentifizierung "acr": "urn:mace:incommon:iap:silver", // Auth Context Class "amr": ["pwd", "otp"], // Auth Methods (Passwort + OTP = MFA!) // Profile Claims (scope=profile) "name": "Max Mustermann", "given_name": "Max", "family_name": "Mustermann", "picture": "https://example.com/avatar.jpg", "locale": "de-DE", "zoneinfo": "Europe/Berlin", // Email Claims (scope=email) "email": "max@beispiel.de", "email_verified": true, // Custom Claims — IMMER mit Namespace! "https://beispiel.de/roles": ["admin", "editor"], "https://beispiel.de/org_id": "org_12345" }

ID Token validieren — Schritt für Schritt

Validierungsreihenfolge (NIEMALS überspringen!)

  1. Signatur prüfen via JWKS-Endpoint (/.well-known/jwks.json)
  2. iss muss exakt mit dem konfigurierten Aussteller übereinstimmen
  3. aud muss die eigene Client-ID enthalten
  4. exp prüfen — Token nicht abgelaufen (max. 5s Toleranz für Clock-Skew)
  5. iat darf nicht in der Zukunft liegen
  6. nonce validieren — exakt der gesendete Wert (Replay-Schutz)
  7. alg auf Whitelist prüfen — NIEMALS none akzeptieren!
// token-validator.ts — Robuste ID-Token-Validierung
import * as jose from 'jose'; interface OIDCConfig { issuer: string; jwksUri: string; clientId: string; allowedAlgorithms: string[]; } export async function validateIdToken( idToken: string, nonce: string, config: OIDCConfig ) { const JWKS = jose.createRemoteJWKSet(new URL(config.jwksUri)); const { payload, protectedHeader } = await jose.jwtVerify(idToken, JWKS, { issuer: config.issuer, audience: config.clientId, algorithms: config.allowedAlgorithms, // z.B. ['RS256', 'ES256'] clockTolerance: '5s', }); // Nonce validieren (jose prüft das nicht automatisch) if (payload.nonce !== nonce) { throw new Error('Ungültiger Nonce — möglicher Replay-Angriff!'); } // alg=none explizit verbieten if (protectedHeader.alg === 'none') { throw new Error('alg=none verboten — unsignierter Token!'); } return payload; } // UserInfo Endpoint — frische Claims abrufen export async function fetchUserInfo(accessToken: string, userInfoEndpoint: string) { const response = await fetch(userInfoEndpoint, { headers: { Authorization: `Bearer ${accessToken}` }, }); if (!response.ok) throw new Error(`UserInfo Fehler: ${response.status}`); return response.json(); }

OIDC Discovery Endpoint

Jeder OIDC-Provider veröffentlicht seine Konfiguration unter /.well-known/openid-configuration. Nutze diesen Endpoint statt hardcodierter URLs:

// Beispiel: https://auth.beispiel.de/.well-known/openid-configuration { "issuer": "https://auth.beispiel.de", "authorization_endpoint": "https://auth.beispiel.de/authorize", "token_endpoint": "https://auth.beispiel.de/oauth/token", "userinfo_endpoint": "https://auth.beispiel.de/userinfo", "jwks_uri": "https://auth.beispiel.de/.well-known/jwks.json", "end_session_endpoint": "https://auth.beispiel.de/v2/logout", "scopes_supported": ["openid", "profile", "email", "offline_access"], "response_types_supported": ["code"], "grant_types_supported": ["authorization_code", "client_credentials"], "token_endpoint_auth_methods_supported": ["client_secret_post", "private_key_jwt"] }

4. Refresh Token Rotation & Token-Strategien

Refresh Token Rotation ist der wichtigste Sicherheitsmechanismus gegen gestohlene Refresh Tokens. Das Prinzip: Bei jeder Verwendung eines Refresh Tokens wird ein neuer ausgestellt und der alte invalidiert. Wird ein alter Token erneut verwendet, deutet das auf einen Angriff hin — und alle Tokens der Session werden sofort gesperrt.

Rotation-Modelle im Vergleich

// token-rotation.ts — Rotation-Handler mit Theft-Detection
import { redis } from './redis-client'; import { signToken, verifyToken } from './jwt-utils'; const RT_PREFIX = 'rt:'; // Redis-Key-Präfix const FAMILY_PREFIX = 'rtf:'; // Familie-Invalidierung const RT_TTL_SECS = 2592000; // 30 Tage interface RefreshTokenPayload { userId: string; familyId: string; // Verbindet alle Tokens einer Session tokenId: string; // Einmaliger Token-Identifier issuedAt: number; } export async function rotateRefreshToken(oldToken: string): Promise<{ accessToken: string; refreshToken: string; }> { let payload: RefreshTokenPayload; try { payload = await verifyToken<RefreshTokenPayload>(oldToken); } catch { throw new Error('Ungültiger Refresh Token'); } const { userId, familyId, tokenId } = payload; // Familie gesperrt? → Theft-Detection angeschlagen const familyRevoked = await redis.get(`${FAMILY_PREFIX}${familyId}`); if (familyRevoked) { // Alle aktiven Sessions des Users sperren! await revokeAllUserSessions(userId); throw new Error('Token-Diebstahl erkannt — alle Sessions gesperrt'); } // Token noch gültig (nicht bereits verwendet)? const isValid = await redis.get(`${RT_PREFIX}${tokenId}`); if (!isValid) { // Bereits verwendeter Token → Familie sperren! await redis.set(`${FAMILY_PREFIX}${familyId}`, 'revoked', 'EX', RT_TTL_SECS); throw new Error('Bereits verwendeter Refresh Token — Rotation-Konflikt!'); } // Alten Token aus Redis löschen (einmalig!) await redis.del(`${RT_PREFIX}${tokenId}`); // Neuen Refresh Token ausstellen const newTokenId = crypto.randomUUID(); const newRefreshToken = await signToken({ userId, familyId, // Familie bleibt gleich! tokenId: newTokenId, issuedAt: Date.now(), }, RT_TTL_SECS + 's'); // Neuen Token in Redis registrieren await redis.set(`${RT_PREFIX}${newTokenId}`, userId, 'EX', RT_TTL_SECS); // Neuen Access Token ausstellen (kurzlebig) const accessToken = await signToken({ userId, type: 'access' }, '15m'); return { accessToken, refreshToken: newRefreshToken }; }

Token Storage — wo und wie?

SpeicherortXSS-SicherCSRF-SicherEmpfehlung
Memory (JS Variable) ✅ Ja ✅ Ja ✅ Access Token
HttpOnly Cookie (SameSite=Strict) ✅ Ja ✅ Ja (Strict) ✅ Refresh Token
localStorage ❌ Nein ✅ Ja ❌ NIEMALS Tokens!
sessionStorage ❌ Nein ✅ Ja ⚠️ Nur PKCE Verifier

Kritisch: Refresh Tokens in localStorage oder sessionStorage sind ein häufiges Angriffsziel. Jedes XSS-Snippet kann diese Tokens stehlen. Verwende immer HttpOnly-Cookies für Refresh Tokens in Web-Anwendungen.

5. Auth0 Integration mit Next.js

Auth0 ist 2026 einer der meistgenutzten Identity Provider. Mit dem offiziellen @auth0/nextjs-auth0 SDK lässt sich eine vollständige Authentication-Infrastruktur in wenigen Stunden aufbauen — inklusive PKCE, Refresh Token Rotation und rollenbasierter Zugriffskontrolle.

Installation und Konfiguration

// Terminal — Abhängigkeiten installieren
npm install @auth0/nextjs-auth0 npm install jose # JWT-Validierung # .env.local AUTH0_SECRET='super-geheimes-32-zeichen-secret' # openssl rand -hex 32 AUTH0_BASE_URL='https://meine-app.de' AUTH0_ISSUER_BASE_URL='https://mein-tenant.eu.auth0.com' AUTH0_CLIENT_ID='abc123...' AUTH0_CLIENT_SECRET='secret...' AUTH0_AUDIENCE='https://api.meine-app.de' AUTH0_SCOPE='openid profile email offline_access'
// app/api/auth/[auth0]/route.ts — Dynamic Route Handler
import { handleAuth, handleLogin, handleLogout, handleCallback } from '@auth0/nextjs-auth0'; import { NextRequest } from 'next/server'; export const GET = handleAuth({ // Login mit custom Parametern login: handleLogin({ authorizationParams: { audience: process.env.AUTH0_AUDIENCE, scope: 'openid profile email offline_access read:data', prompt: 'select_account', // Account-Auswahl erzwingen }, returnTo: '/dashboard', }), // Logout mit id_token_hint für vollständige Abmeldung logout: handleLogout(async (req: NextRequest) => { const { getSession } = await import('@auth0/nextjs-auth0'); const session = await getSession(); return { returnTo: process.env.AUTH0_BASE_URL, logoutParams: { id_token_hint: session?.idToken }, }; }), // Callback — nach erfolgreicher Auth0-Authentifizierung callback: handleCallback({ async afterCallback(req, session) { // Custom Claims in Session einfügen const roles = session.idToken?.['https://meine-app.de/roles'] ?? []; session.user.roles = roles; return session; }, }), });

API-Route Protection & Rollenprüfung

// app/api/admin/users/route.ts — Geschützte API Route
import { withApiAuthRequired, getSession } from '@auth0/nextjs-auth0'; import { NextResponse } from 'next/server'; export const GET = withApiAuthRequired(async function(req) { const session = await getSession(); const user = session!.user; // Rollen-Check — Namespace aus Auth0 Action const roles: string[] = user['https://meine-app.de/roles'] ?? []; if (!roles.includes('admin')) { return NextResponse.json( { error: 'Unzureichende Berechtigung' }, { status: 403 } ); } // Access Token für API-Calls an eigene Services const accessToken = session!.accessToken; const apiResponse = await fetch('https://api.meine-app.de/users', { headers: { Authorization: `Bearer ${accessToken}` }, }); const users = await apiResponse.json(); return NextResponse.json({ users, requestedBy: user.sub }); });

Auth0 Action — Custom Claims bei Login

// Auth0 Dashboard → Actions → Login Flow → Custom Action
/** * Auth0 Action: Rollen aus eigenem DB-System injizieren * Trigger: Post Login */ exports.onExecutePostLogin = async (event, api) => { const NAMESPACE = 'https://meine-app.de'; // Rollen aus externem System laden const rolesResponse = await fetch(`https://api.meine-app.de/auth/roles/${event.user.user_id}`, { headers: { Authorization: `Bearer ${event.secrets.INTERNAL_API_KEY}` }, }); const { roles } = await rolesResponse.json(); // Custom Claims mit Namespace injizieren (RFC-konform) api.idToken.setCustomClaim(`${NAMESPACE}/roles`, roles); api.accessToken.setCustomClaim(`${NAMESPACE}/roles`, roles); api.accessToken.setCustomClaim(`${NAMESPACE}/org_id`, event.user.app_metadata?.org_id); };

Tipp: Auth0 Actions ersetzen die alten Rules und Hooks. Nutze Actions für alle custom Claims, Webhook-Calls und MFA-Logik. Rules sind veraltet und werden 2026 abgeschaltet.

6. Keycloak Self-Hosted: Docker & Node.js

Keycloak ist die führende Open-Source Identity-Lösung für Self-Hosted-Szenarien. Version 24+ bietet native Quarkus-basierte Performance, erweiterte Admin REST API und deutlich verbesserte Startup-Zeiten. Ideal für Unternehmen mit Datenschutzanforderungen oder eigener Infrastruktur.

Docker Setup — Production-ready

// docker-compose.yml — Keycloak + PostgreSQL
version: '3.9' services: postgres: image: postgres:16-alpine restart: unless-stopped environment: POSTGRES_DB: keycloak POSTGRES_USER: keycloak POSTGRES_PASSWORD: ${DB_PASSWORD} volumes: - postgres_data:/var/lib/postgresql/data networks: [keycloak-net] healthcheck: test: ["CMD-SHELL", "pg_isready -U keycloak"] interval: 10s timeout: 5s retries: 5 keycloak: image: quay.io/keycloak/keycloak:24.0 restart: unless-stopped command: start --optimized environment: KC_DB: postgres KC_DB_URL: jdbc:postgresql://postgres/keycloak KC_DB_USERNAME: keycloak KC_DB_PASSWORD: ${DB_PASSWORD} KC_HOSTNAME: auth.meine-firma.de KC_HOSTNAME_STRICT: 'true' KC_HTTP_ENABLED: 'false' # HTTPS-only in Production KC_PROXY: edge # Hinter Reverse Proxy KC_FEATURES: token-exchange,admin-fine-grained-authz KEYCLOAK_ADMIN: admin KEYCLOAK_ADMIN_PASSWORD: ${KC_ADMIN_PASSWORD} ports: - "127.0.0.1:8080:8080" # Nur lokal — Caddy/Nginx davor depends_on: postgres: { condition: service_healthy } networks: [keycloak-net] volumes: postgres_data: networks: keycloak-net:

Realm & Client Konfiguration via Admin API

// keycloak-setup.ts — Realm + Client programmatisch anlegen
const KC_BASE = 'https://auth.meine-firma.de'; const REALM = 'meine-app'; // 1. Admin-Token holen async function getAdminToken(): Promise<string> { const res = await fetch(`${KC_BASE}/realms/master/protocol/openid-connect/token`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'password', client_id: 'admin-cli', username: process.env.KC_ADMIN_USER!, password: process.env.KC_ADMIN_PASS!, }), }); const data = await res.json(); return data.access_token; } // 2. Realm anlegen async function createRealm(adminToken: string) { await fetch(`${KC_BASE}/admin/realms`, { method: 'POST', headers: { Authorization: `Bearer ${adminToken}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ realm: REALM, enabled: true, displayName: 'Meine Anwendung', accessTokenLifespan: 900, // 15 Minuten ssoSessionMaxLifespan: 2592000, // 30 Tage refreshTokenMaxReuse: 0, // Rotation: keine Wiederverwendung passwordPolicy: 'length(12) and digits(1) and specialChars(1)', bruteForceProtected: true, failureFactor: 5, // Sperre nach 5 Fehlversuchen }), }); } // 3. OIDC-Client konfigurieren async function createClient(adminToken: string) { await fetch(`${KC_BASE}/admin/realms/${REALM}/clients`, { method: 'POST', headers: { Authorization: `Bearer ${adminToken}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ clientId: 'frontend-app', name: 'Frontend Application', publicClient: true, // SPA = public client standardFlowEnabled: true, directAccessGrantsEnabled: false, // Password Grant verboten pkceCodeChallengeMethod: 'S256', // PKCE erzwingen redirectUris: [ 'https://app.meine-firma.de/callback', 'http://localhost:3000/callback', ], webOrigins: ['https://app.meine-firma.de'], protocol: 'openid-connect', }), }); }

Node.js Integration mit keycloak-connect

// keycloak-middleware.ts — Express/Fastify Middleware
import Keycloak from 'keycloak-connect'; import session from 'express-session'; import RedisStore from 'connect-redis'; import { redis } from './redis-client'; const keycloakConfig = { 'realm': 'meine-app', 'auth-server-url': 'https://auth.meine-firma.de', 'resource': 'backend-service', 'bearer-only': true, // API-Modus: kein Redirect, nur Bearer prüfen 'verify-token-audience': true, 'confidential-port': 0, }; const memoryStore = new RedisStore({ client: redis }); const keycloak = new Keycloak({ store: memoryStore }, keycloakConfig); // Express App Setup app.use(session({ secret: process.env.SESSION_SECRET!, resave: false, saveUninitialized: false, store: memoryStore, cookie: { secure: true, httpOnly: true, sameSite: 'strict' }, })); app.use(keycloak.middleware()); // Geschützte Route — nur authentifizierte User app.get('/api/profile', keycloak.protect(), (req, res) => { const token = (req as any).kauth?.grant?.access_token; res.json({ userId: token?.content?.sub, email: token?.content?.email, roles: token?.content?.realm_access?.roles ?? [], }); } ); // Rollenbasierter Zugriff — nur Admin app.delete('/api/users/:id', keycloak.protect('realm:admin'), // Realm-Rolle "admin" erforderlich async (req, res) => { await userService.delete(req.params.id); res.json({ success: true }); } );

Keycloak vs. Auth0 — Wann welche Lösung?

Entscheidungsmatrix

KriteriumKeycloakAuth0
Datenschutz/DSGVO✅ Volle Kontrolle⚠️ US-Server (SCC nötig)
Setup-Aufwand⚠️ Hoch✅ Gering
Kosten✅ Open Source⚠️ Ab $23/Mo
Enterprise Features✅ Komplett✅ Komplett
Developer Experience⚠️ Komplex✅ Exzellent
Hosting-Aufwand⚠️ Selbst hosten✅ SaaS

Empfehlung 2026: Startups und kleine Teams sollten mit Auth0 starten — schneller Time-to-Market, weniger Ops-Overhead. Unternehmen mit DSGVO-kritischen Daten oder >1000 MAU sollten Keycloak evaluieren. Keycloak lohnt sich ab ~500 MAU aus Kostensicht.

Authentication-Systeme mit KI beschleunigen

Claude Code hilft dir dabei, OAuth2-Flows zu implementieren, Sicherheitslücken zu finden und komplexe OIDC-Konfigurationen zu debuggen — in Minuten statt Stunden.

Jetzt kostenlos testen →
🤖

SpockyMagicAI Team

Publiziert am 6. Mai 2026 · Security & Auth