Authentifizierung beantwortet eine Frage: Wer bist du? Nicht was du darfst – das ist Autorisierung. Aber ohne zuverlässige Identität ist jede Berechtigungsprüfung wertlos.
Die Wahl des Auth-Modells beeinflusst Security, Developer Experience und Betriebsaufwand. Dieser Artikel hilft dir, zwischen OAuth2, API Keys und mTLS zu entscheiden – und erklärt, warum JWT nicht immer die richtige Wahl ist.
Zielbild
Nach diesem Artikel kannst du:
- Zwischen OAuth2/OIDC, API Keys und mTLS begründet wählen
- JWT vs. opaque Tokens für deinen Use Case bewerten
- Token Lifetimes und Rotation sinnvoll konfigurieren
- CORS und CSRF für Browser-Clients korrekt einrichten
- Ein AuthN-Modell dokumentieren, das Security und DX balanciert
Kernfragen
Bevor du weiterliest, versuche diese Fragen für dein Projekt zu beantworten:
- Wer sind die Clients? Browser, Mobile Apps, Server, Partner?
- OAuth2 oder API Keys? Oder beides für verschiedene Use Cases?
- JWT oder opaque Tokens? Was sind die Trade-offs?
- Wie lange leben Tokens? Access Token? Refresh Token?
- Wie rotiert ihr Keys? JWKS? Manuelle Rotation?
Auth-Modelle im Überblick
Drei Hauptansätze haben sich etabliert, jeder mit eigenen Stärken.
OAuth2 / OpenID Connect
OAuth2 ist ein Autorisierungs-Framework, OIDC erweitert es um Authentifizierung.
Flows:
| Flow | Use Case |
|---|---|
| Authorization Code + PKCE | Browser, Mobile Apps (User-Login) |
| Client Credentials | Server-to-Server (Machine-to-Machine) |
| Device Code | Smart TVs, CLIs ohne Browser |
| Refresh Token Grant (RFC) | Lange Sessions ohne Re-Login |
Vorteile:
- Standardisiert, gut dokumentiert
- Delegierte Authentifizierung (User loggt sich beim IdP ein)
- Scopes für granulare Berechtigungen
- Token Revocation möglich
Nachteile:
- Komplexer zu implementieren
- Abhängigkeit vom Auth Server
- Mehr Roundtrips bei Token-Refresh
Wann verwenden:
- User-facing APIs (Browser, Mobile)
- Multi-Tenant-Systeme
- Wenn delegierte Auth nötig ist (Social Login, SSO)
API Keys
Einfache, langlebige Credentials für Server-to-Server-Kommunikation.
GET /v1/orders HTTP/1.1
Authorization: Bearer sk_live_abc123xyz
Oder als Header:
X-API-Key: sk_live_abc123xyz
Vorteile:
- Einfach zu implementieren und zu nutzen
- Kein Token-Refresh nötig
- Gut für Server-to-Server
Nachteile:
- Kein eingebauter Ablauf (manuelles Rotieren)
- Schwer zu revoken ohne Client-Änderung
- Keine User-Identität (nur Client/Tenant)
Wann verwenden:
- Server-to-Server-Integration
- Partner-APIs
- Einfache Webhooks
- Wenn OAuth2 Overkill ist
mTLS (Mutual TLS)
Beide Seiten authentifizieren sich mit Zertifikaten.
Vorteile:
- Sehr sicher (Zertifikate statt Secrets)
- Keine Credentials im Request
- Gut für Zero-Trust-Architekturen
Nachteile:
- Komplex zu betreiben (PKI, Zertifikatsmanagement)
- Schwieriger für externe Partner
- Nicht für Browser geeignet
Wann verwenden:
- Interne Service-to-Service-Kommunikation
- Hochsichere Umgebungen
- Wenn PKI bereits existiert
Entscheidungsmatrix
| Kriterium | OAuth2/OIDC | API Keys | mTLS |
|---|---|---|---|
| Browser-Clients | Ja | Nein | Nein |
| Mobile Apps | Ja | Eingeschränkt | Nein |
| Server-to-Server | Ja | Ja | Ja |
| User-Identität | Ja | Nein | Nein |
| Komplexität | Hoch | Niedrig | Hoch |
| Betriebsaufwand | Mittel | Niedrig | Hoch |
Empfehlung: OAuth2 für User-facing, API Keys für einfache Server-Integration, mTLS für interne Services.
JWT vs. Opaque Tokens
Die Wahl zwischen JWT und opaque Tokens ist eine der häufigsten Fragen.
JWT (JSON Web Token)
Self-contained Token mit Payload:
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiJ1c2VyXzEyMyIsInNjb3BlIjoicmVhZCB3cml0ZSIsImV4cCI6MTcwNjUyMjQwMH0.
signature
Decoded:
{
"sub": "user_123",
"iss": "https://auth.example.com",
"aud": "https://api.example.com",
"scope": "read write",
"exp": 1706522400,
"iat": 1706518800
}
Vorteile:
- Stateless: API muss Auth Server nicht fragen
- Payload enthält Claims (User-ID, Scopes)
- Skaliert gut (kein zentraler Token-Store)
Nachteile:
- Nicht sofort revozierbar (läuft erst ab)
- Größer als opaque Tokens
- Payload ist lesbar (Base64, nicht verschlüsselt)
Opaque Tokens
Zufälliger String ohne Bedeutung:
at_7f3b8c9d2e1a4f5b6c7d8e9f0a1b2c3d
API muss den Token serverseitig prüfen (Introspection oder Token-Store):
POST /oauth/introspect
Content-Type: application/x-www-form-urlencoded
token=at_7f3b8c9d2e1a4f5b6c7d8e9f0a1b2c3d
Vorteile:
- Sofort revozierbar
- Kleiner
- Keine sensitiven Daten im Token
Nachteile:
- Stateful: jeder Request braucht serverseitige Prüfung
- Auth Server wird Bottleneck
- Mehr Latenz
Wann was verwenden?
| Szenario | Empfehlung |
|---|---|
| Hohe Last, Skalierung kritisch | JWT |
| Sofortige Revocation nötig | Opaque |
| Sensitive Claims (PII) | Opaque |
| Microservices, viele APIs | JWT |
| Single API, einfacher Betrieb | Opaque |
| Kurze Token-Lifetime (< 15 min) | JWT |
| Lange Token-Lifetime | Opaque |
Hybrid-Ansatz: Kurze JWTs (5-15 min) + Refresh Tokens (opaque). So bekommst du Stateless-Vorteile mit kontrollierbarer Revocation.
Token Lifetimes
Token-Lifetimes balancieren Security (kurz) und UX (lang).
Access Tokens
| Kontext | Empfohlene Lifetime |
|---|---|
| Browser (SPA) | 5-15 Minuten |
| Mobile App | 15-60 Minuten |
| Server-to-Server | 1-24 Stunden |
| Hochsicher | 5 Minuten |
Warum kurz?
- Kompromittierte Tokens sind nur kurz gültig
- Erzwingt regelmäßige Re-Validierung
- Begrenzt Schaden bei Token-Leak
Refresh Tokens
| Kontext | Empfohlene Lifetime |
|---|---|
| Browser | 1-7 Tage |
| Mobile App | 30-90 Tage |
| Server | Nicht nötig (Client Credentials) |
Refresh Token Rotation:
Bei jedem Refresh ein neues Refresh Token ausgeben:
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&refresh_token=rt_old123
Response:
{
"access_token": "at_new456",
"refresh_token": "rt_new789",
"expires_in": 900
}
Warum Rotation?
- Kompromittiertes Refresh Token wird bei nächster Nutzung ungültig
- Erkennung von Token-Diebstahl (zwei Clients mit gleichem Token)
Absolute vs. Sliding Expiration
Absolute: Token läuft nach fester Zeit ab, egal wie aktiv der User ist.
Sliding: Lifetime verlängert sich bei Aktivität.
Empfehlung: Absolute Expiration für Access Tokens, Sliding für Sessions.
Key Management und Rotation
Signing Keys für JWTs müssen sicher verwaltet und regelmäßig rotiert werden.
JWKS (JSON Web Key Set)
Öffentliche Keys werden über einen Endpoint bereitgestellt:
GET /.well-known/jwks.json
Response:
{
"keys": [
{
"kty": "RSA",
"kid": "key_2024_01",
"use": "sig",
"alg": "RS256",
"n": "0vx7agoebG...",
"e": "AQAB"
},
{
"kty": "RSA",
"kid": "key_2023_12",
"use": "sig",
"alg": "RS256",
"n": "abc123...",
"e": "AQAB"
}
]
}
Key Rotation
- Neuen Key generieren und zu JWKS hinzufügen
- Neue Tokens mit neuem Key signieren (
kidim Header) - Grace Period (z.B. 24h): beide Keys aktiv
- Alten Key entfernen nach Grace Period
{
"alg": "RS256",
"typ": "JWT",
"kid": "key_2024_01"
}
Regel: API validiert gegen alle Keys im JWKS, nicht nur den neuesten.
Rotation Schedule
| Key-Typ | Rotation |
|---|---|
| Signing Keys (JWT) | 90 Tage |
| API Keys | Bei Kompromittierung |
| TLS-Zertifikate | 90 Tage (Let's Encrypt) |
CORS für Browser-Clients
Cross-Origin Resource Sharing ist kritisch für SPAs.
Minimale CORS-Policy
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 86400
CORS-Regeln
| Regel | Erklärung |
|---|---|
| Keine Wildcards mit Credentials | * + credentials: include funktioniert nicht |
| Origin-Whitelist | Explizite Liste erlaubter Origins |
| Preflight cachen | Access-Control-Max-Age reduziert OPTIONS-Requests |
| Credentials explizit | Access-Control-Allow-Credentials: true wenn nötig |
Preflight-Requests
Browser senden OPTIONS vor komplexen Requests:
OPTIONS /v1/orders HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Authorization, Content-Type
Response:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 86400
CSRF-Schutz
Cross-Site Request Forgery ist relevant, wenn Cookies verwendet werden.
Wann ist CSRF ein Risiko?
| Auth-Methode | CSRF-Risiko |
|---|---|
| Bearer Token (Header) | Nein |
| Cookie-basiert | Ja |
| API Key (Header) | Nein |
Warum? Browser senden Cookies automatisch. Header nicht.
CSRF-Schutz-Strategien
1. SameSite Cookies:
Set-Cookie: session=abc123; SameSite=Strict; Secure; HttpOnly
| SameSite | Verhalten |
|---|---|
| Strict | Nie cross-site gesendet |
| Lax | Nur bei Top-Level-Navigation |
| None | Immer (erfordert Secure) |
2. CSRF-Tokens:
<input type="hidden" name="_csrf" value="token123">
Server prüft Token bei POST/PUT/DELETE.
3. Double-Submit Cookie:
Cookie + Header müssen übereinstimmen:
Cookie: csrf=abc123
X-CSRF-Token: abc123
Empfehlung: Bearer Tokens im Authorization-Header statt Cookies. Dann ist CSRF in der Praxis mitigiert, solange Tokens nicht als Cookies gespeichert werden (XSS bleibt ein separates Risiko).
Anonymous Access
Manchmal brauchen APIs öffentliche Endpoints ohne Auth.
Regeln für Anonymous Access
- Explizit markieren: Dokumentiere, welche Endpoints anonym sind
- Minimieren: So wenig wie möglich
- Rate Limiting: Aggressiver als für authentifizierte Requests
- Keine sensitiven Daten: Nur öffentliche Informationen
Beispiele
| Endpoint | Anonymous | Begründung |
|---|---|---|
GET /health |
Ja | Monitoring |
GET /v1/products |
Ja | Öffentlicher Katalog |
POST /v1/auth/login |
Ja | Login-Flow |
GET /v1/users/me |
Nein | User-Daten |
POST /v1/orders |
Nein | Geschäftsaktion |
Regeln & Anti-Patterns
Do
- OAuth2 für User-facing APIs, API Keys für Server-to-Server
- Kurze Access Token Lifetimes (5-15 min für Browser)
- Refresh Token Rotation aktivieren
- JWKS für Key Distribution, regelmäßige Rotation
- CORS-Whitelist statt Wildcards
- Bearer Tokens statt Cookies (vermeidet CSRF)
- Anonymous Access minimieren und explizit dokumentieren
Don't
- Langlebige JWTs ohne Refresh-Mechanismus
- API Keys in Browser-Clients
Access-Control-Allow-Origin: *mit Credentials- CSRF-Schutz vergessen bei Cookie-Auth
- Signing Keys nie rotieren
- Sensitive Claims in JWT-Payload ohne Verschlüsselung
- Passwords im Token (auch nicht gehasht)
Artefakt: AuthN-ADR
# ADR: Authentifizierungsmodell
## Status
Accepted
## Kontext
Die API wird von verschiedenen Clients genutzt:
- Browser (SPA)
- Mobile Apps
- Partner-Server
Wir brauchen ein Auth-Modell, das Security, DX und Betriebsaufwand balanciert.
## Entscheidung
### Auth-Methoden
| Client-Typ | Auth-Methode |
|------------------|----------------------------------|
| Browser (SPA) | OAuth2 Authorization Code + PKCE |
| Mobile App | OAuth2 Authorization Code + PKCE |
| Partner-Server | API Keys |
| Interne Services | mTLS (optional) |
### Token-Typ
- **Access Tokens:** JWT (RS256)
- **Refresh Tokens:** Opaque
### Token Lifetimes
| Token | Lifetime |
|------------------------|-------------------------|
| Access Token (Browser) | 15 Minuten |
| Access Token (Mobile) | 30 Minuten |
| Access Token (Server) | 1 Stunde |
| Refresh Token | 7 Tage (Rotation aktiv) |
### JWT Claims
```json
{
"iss": "https://auth.example.com",
"aud": "https://api.example.com",
"sub": "user_abc123",
"tenant_id": "tenant_xyz",
"scope": "read:orders write:orders",
"exp": 1706522400,
"iat": 1706521500
}
```
### Key Rotation
- Algorithmus: RS256
- JWKS Endpoint: `/.well-known/jwks.json`
- Rotation: alle 90 Tage
- Grace Period: 24 Stunden
### API Keys
- Format: `sk_live_` + 32 Zeichen
- Scopes: pro Key konfigurierbar
- Rotation: manuell, bei Kompromittierung
### CORS
Erlaubte Origins:
- `https://app.example.com`
- `https://admin.example.com`
### Anonymous Endpoints
- `GET /health`
- `GET /v1/products`
- `POST /v1/auth/login`
- `POST /v1/auth/token`
## Konsequenzen
### Positiv
- Standardisiertes Auth-Modell
- Kurze Token-Lifetimes begrenzen Schaden
- Refresh Rotation erkennt Token-Diebstahl
### Negativ
- OAuth2-Komplexität für simple Use Cases
- JWKS-Caching nötig für Performance
## Flow-Diagramm
### Browser/Mobile (Authorization Code + PKCE)
```mermaid
sequenceDiagram
participant User
participant App
participant AuthServer as Auth Server
participant API
User->>App: Login starten
App->>AuthServer: Authorization Request (PKCE)
AuthServer-->>User: Login
User-->>AuthServer: Credentials
AuthServer-->>App: Authorization Code
App->>AuthServer: Token Exchange (Code → Access + Refresh)
AuthServer-->>App: Access + Refresh Token
App->>API: API Request + Access Token
alt Token abgelaufen
App->>AuthServer: Refresh Token
AuthServer-->>App: Neuer Access Token
App->>API: API Request + Access Token
end
```
### Partner-Server (API Key)
```mermaid
sequenceDiagram
participant Server
participant API
Server->>API: API Request + API Key
API->>API: Validate Key
API-->>Server: Response
```
## Checkliste
Bevor du zum nächsten Artikel gehst, prüfe:
- [ ] Auth-Modell ist gewählt (OAuth2, API Keys, mTLS)
- [ ] OAuth2-Flows sind dokumentiert
- [ ] Token-Typ ist entschieden (JWT vs. opaque)
- [ ] Token Lifetimes sind definiert
- [ ] Refresh Token Rotation ist aktiviert
- [ ] JWKS-Endpoint existiert und Keys rotieren
- [ ] CORS ist konfiguriert (keine Wildcards mit Credentials)
- [ ] CSRF-Schutz ist implementiert (falls Cookies)
- [ ] Anonymous Endpoints sind explizit dokumentiert
- [ ] AuthN-ADR ist geschrieben
## Wie es weitergeht
Im nächsten Teil geht es um Autorisierung: RBAC vs. ABAC, wie du BOLA
verhinderst, und warum Least Privilege schwieriger ist als gedacht.
Alle Teile der Serie: [Serie: API-Design](/serien/api-design/)