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
openid — Pflicht für OIDC, liefert ID Token
profile — Name, Benutzername, Profilbild
email — E-Mail + Verifikationsstatus
offline_access — Refresh Token wird ausgestellt
read:users write:users — Anwendungsspezifische Scopes
admin — Privilegierte Operationen (sparsam vergeben!)
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
- code_verifier — ein zufälliger, kryptographisch sicherer String (43–128 Zeichen, Base64url-codiert)
- 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!)
- Signatur prüfen via JWKS-Endpoint (
/.well-known/jwks.json)
iss muss exakt mit dem konfigurierten Aussteller übereinstimmen
aud muss die eigene Client-ID enthalten
exp prüfen — Token nicht abgelaufen (max. 5s Toleranz für Clock-Skew)
iat darf nicht in der Zukunft liegen
nonce validieren — exakt der gesendete Wert (Replay-Schutz)
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
- Sliding Expiry + Rotation: Jeder Use verlängert die Lebensdauer. Ideal für aktive User.
- Absolute Expiry: Feste maximale Lebensdauer, unabhängig von Use. Sicherster Ansatz.
- Refresh Token Families: Tokens einer Anmeldung als Familie — Diebstahl invalidiert alle.
// 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?
| Speicherort | XSS-Sicher | CSRF-Sicher | Empfehlung |
| 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
| Kriterium | Keycloak | Auth0 |
| 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