RFC 9068 - JWT-Profil für OAuth 2.0-Zugriffstoken
- Status: Proposed Standard
- Veröffentlicht: October 2021
- Stream: IETF
- Errata: Keine Errata
Zusammenfassung (Abstract)
Diese Spezifikation definiert ein Profil (Profile) für die Ausstellung von OAuth 2.0-Zugriffstoken (Access Token) im JSON Web Token (JWT)-Format. Autorisierungsserver (Authorization Server) und Ressourcenserver (Resource Server) verschiedener Anbieter können dieses Profil nutzen, um Zugriffstoken auf interoperable Weise auszustellen und zu verwenden.
Status dieses Memorandums (Status of This Memo)
Dies ist ein Internet Standards Track-Dokument.
Dieses Dokument ist ein Produkt der Internet Engineering Task Force (IETF). Es repräsentiert den Konsens der IETF-Community. Es wurde einer öffentlichen Überprüfung unterzogen und zur Veröffentlichung von der Internet Engineering Steering Group (IESG) genehmigt. Weitere Informationen zu Internet-Standards sind in Abschnitt 2 von RFC 7841 verfügbar.
Informationen über den aktuellen Status dieses Dokuments, eventuelle Errata und Möglichkeiten zur Rückmeldung sind verfügbar unter: https://www.rfc-editor.org/info/rfc9068.
Inhaltsverzeichnis (Contents)
- 1. Introduction (Einführung)
- 1.1 Requirements Notation and Conventions (Anforderungsnotation und Konventionen)
- 1.2 Terminology (Terminologie)
- 2. JWT Access Token Header and Data Structure (JWT-Zugriffstoken-Header und Datenstruktur)
- 2.1 Header (Header)
- 2.2 Data Structure (Datenstruktur)
- 2.2.1 Authentication Information Claims (Authentifizierungsinformations-Claims)
- 2.2.2 Identity Claims (Identitäts-Claims)
- 2.2.3 Authorization Claims (Autorisierungs-Claims)
- 2.2.3.1 Claims for Authorization Outside of Delegation Scenarios (Claims für Autorisierung außerhalb von Delegierungsszenarien)
- 3. Requesting a JWT Access Token (Anforderung eines JWT-Zugriffstokens)
- 4. Validating JWT Access Tokens (Validierung von JWT-Zugriffstoken)
- 5. Security Considerations (Sicherheitserwägungen)
- 6. Privacy Considerations (Datenschutzerwägungen)
- 7. IANA Considerations (IANA-Erwägungen)
- 7.1 Media Type Registration (Medientyp-Registrierung)
- 7.1.1 Registry Content (Registry-Inhalt)
- 7.2 Claims Registration (Claims-Registrierung)
- 7.2.1 Registry Content (Registry-Inhalt)
- 7.2.1.1 Roles (Rollen)
- 7.2.1.2 Groups (Gruppen)
- 7.2.1.3 Entitlements (Berechtigungen)
- 7.2.1 Registry Content (Registry-Inhalt)
- 7.1 Media Type Registration (Medientyp-Registrierung)
- 8. References (Referenzen)
- 8.1 Normative References (Normative Referenzen)
- 8.2 Informative References (Informative Referenzen)
- Acknowledgements (Danksagungen)
- Author's Address (Adresse des Autors)
Urheberrechtshinweis (Copyright Notice)
Copyright (c) 2021 IETF Trust und die als Autoren des Dokuments identifizierten Personen. Alle Rechte vorbehalten.
Dieses Dokument unterliegt BCP 78 und den rechtlichen Bestimmungen des IETF Trust in Bezug auf IETF-Dokumente (https://trustee.ietf.org/license-info), die zum Zeitpunkt der Veröffentlichung dieses Dokuments in Kraft sind. Bitte lesen Sie diese Dokumente sorgfältig durch, da sie Ihre Rechte und Einschränkungen in Bezug auf dieses Dokument beschreiben. Aus diesem Dokument extrahierte Codekomponenten müssen den Text der vereinfachten BSD-Lizenz gemäß Abschnitt 4.e der rechtlichen Bestimmungen des Trust enthalten und werden ohne Gewährleistung wie in der vereinfachten BSD-Lizenz beschrieben bereitgestellt.
Ressourcen
- Offizieller Text: RFC 9068
- Offizielle Seite: RFC 9068 DataTracker
- Errata: RFC Editor Errata
RFC 9068 - JWT Profile for OAuth 2.0 Access Tokens
Grundlegende Informationen
- RFC-Nummer: 9068
- Titel: JSON Web Token (JWT) Profile for OAuth 2.0 Access Tokens
- Deutscher Titel: JWT-Profil für OAuth 2.0-Zugriffstoken
- Veröffentlichungsdatum: Oktober 2021
- Status: PROPOSED STANDARD (Vorgeschlagener Standard)
- Autoren: V. Bertocci (Auth0), B. Campbell (Ping Identity)
Zusammenfassung (Abstract)
Diese Spezifikation definiert ein Standardprofil für die Verwendung von OAuth 2.0-Zugriffstoken im JWT-Format. Sie legt erforderliche Claims, empfohlene Claims und Validierungsanforderungen für JWT-Zugriffstoken fest, sodass Ressourcenserver Zugriffstoken direkt validieren und parsen können, ohne jedes Mal einen Introspektionsendpunkt aufrufen zu müssen.
Übersicht über JWT-Zugriffstoken
Warum benötigen wir JWT-Zugriffstoken?
Probleme traditioneller Zugriffstoken:
Opake Token (Opaque Token):
- Format: Zufallszeichenkette (z.B.: "SlAV32hkKG...XPw")
- Merkmal: Kann nicht direkt geparst werden
- Validierung: Muss Introspektionsendpunkt aufrufen (RFC 7662)
Probleme:
❌ Jeder API-Aufruf erfordert Introspektion → zusätzliche Netzwerkanfragen
❌ Hohe Last auf Autorisierungsserver → Leistungsengpass
❌ Erhöhte Latenz → schlechte Benutzererfahrung
❌ Single Point of Failure → Ausfall des Autorisierungsservers beeinträchtigt alle APIs
Beispielablauf:
Client → [access_token] → Ressourcenserver
↓
Introspektionsendpunkt ← Autorisierungsserver
↓
Antwort (active: true)
↓
Validierung erfolgreich
Vorteile von JWT-Zugriffstoken:
JWT-Token:
- Format: Selbstenthaltendes JSON Web Token
- Merkmal: Kann direkt validiert und geparst werden
- Validierung: Signaturverifizierung mit öffentlichem Schlüssel
Vorteile:
✓ Keine Introspektion erforderlich → null zusätzliche Anfragen
✓ Reduzierte Last auf Autorisierungsserver → hohe Leistung
✓ Geringe Latenz → schnelle Antwort
✓ Offline-Validierung → Ausfall des Autorisierungsservers beeinträchtigt API nicht
✓ Enthält Kontextinformationen → scope, Benutzer-ID usw.
Beispielablauf:
Client → [JWT access_token] → Ressourcenserver
↓
1. Signatur verifizieren (mit öffentlichem Schlüssel)
2. Ablaufzeit prüfen
3. Audience verifizieren
↓
Validierung erfolgreich
↓
Scope/Benutzer-ID aus JWT lesen
JWT-Zugriffstoken vs. Opake Token
Vergleich:
Opake Token (Opaque Token):
Vorteile:
+ Kann jederzeit widerrufen werden (serverseitige Kontrolle)
+ Kleines Token (normalerweise 20-40 Zeichen)
+ Keine Informationsleckage (nicht parsbar)
Nachteile:
- Muss durch Introspektion validiert werden (Netzwerk-Overhead)
- Hoher Druck auf Autorisierungsserver
- Keine Offline-Validierung
Anwendungsfälle:
- Szenarien, die sofortigen Widerruf erfordern
- Hohe Sicherheitsanforderungen (Token soll nicht parsbar sein)
- Kurzlebige Token
JWT-Zugriffstoken:
Vorteile:
+ Selbstenthalten (keine Introspektion erforderlich)
+ Hohe Leistung (Offline-Validierung)
+ Enthält Kontext (scope, claims)
+ Erweiterbar
Nachteile:
- Großes Token (normalerweise 100-300+ Zeichen)
- Schwer zu widerrufen (erfordert zusätzliche Mechanismen)
- Informationen sichtbar (base64-Dekodierung lesbar)
Anwendungsfälle:
- Hochleistungs-APIs
- Microservice-Architektur
- Verteilte Systeme
- Token müssen Kontextinformationen enthalten
Hybridlösung:
- Kurzlebige JWT-Zugriffstoken (z.B. 15 Minuten)
- Langlebige opake Refresh-Token
→ Kombiniert Vorteile beider Ansätze
JWT-Zugriffstoken-Format
Erforderliche Claims (Required Claims)
{
"iss": "https://authorization-server.example.com",
"exp": 1639533600,
"aud": "https://api.example.com",
"sub": "user-123",
"client_id": "client-456",
"iat": 1639530000,
"jti": "token-789"
}
1. iss (Issuer - Aussteller):
"iss": "https://authorization-server.example.com"
Beschreibung:
- Kennung des Autorisierungsservers
- Muss HTTPS-URL sein
- Wird verwendet, um Verifizierungsschlüssel zu finden
Validierung:
const payload = jwt.decode(token);
if (payload.iss !== expectedIssuer) {
throw new Error('Invalid issuer');
}
2. exp (Expiration Time - Ablaufzeit):
"exp": 1639533600 // Unix-Zeitstempel
Beschreibung:
- Ablaufzeit des Tokens
- Unix-Zeitstempel (Sekunden)
- Muss Zukunftszeit sein
Validierung:
const now = Math.floor(Date.now() / 1000);
if (payload.exp <= now) {
throw new Error('Token expired');
}
Empfohlene Gültigkeitsdauer:
- Kurzfristig: 5-15 Minuten (hohe Sicherheit)
- Mittelfristig: 1 Stunde (ausgewogen)
- Langfristig: 24 Stunden (niedrige Sicherheit, nicht empfohlen)
3. aud (Audience - Zielgruppe):
"aud": "https://api.example.com"
oder
"aud": ["https://api.example.com", "https://api2.example.com"]
Beschreibung:
- Ziel-Ressourcenserver des Tokens
- Kann Zeichenkette oder Zeichenketten-Array sein
- Verhindert Verwendung des Tokens für unbeabsichtigte Ressourcen
Validierung:
const expectedAudience = 'https://api.example.com';
if (Array.isArray(payload.aud)) {
if (!payload.aud.includes(expectedAudience)) {
throw new Error('Invalid audience');
}
} else {
if (payload.aud !== expectedAudience) {
throw new Error('Invalid audience');
}
}
4. sub (Subject - Subjekt):
"sub": "user-123"
Beschreibung:
- Subjekt-Kennung des Tokens
- Normalerweise Benutzer-ID
- Muss im Bereich des Ausstellers eindeutig sein
- Zeichenkettentyp
Verwendung:
// Benutzer-ID aus Token abrufen
const userId = payload.sub;
const user = await getUserById(userId);
5. client_id (Client Identifier - Client-Kennung):
"client_id": "client-456"
Beschreibung:
- ID des Clients, der das Token anfordert
- Wird für Nachverfolgung und Audit verwendet
- Unterscheidet Anfragen verschiedener Clients
Verwendung:
// Client-Berechtigungen prüfen
const client = await getClient(payload.client_id);
if (!client.hasPermission('write')) {
throw new Error('Client not authorized');
}
6. iat (Issued At - Ausstellungszeit):
"iat": 1639530000 // Unix-Zeitstempel
Beschreibung:
- Ausstellungszeit des Tokens
- Unix-Zeitstempel (Sekunden)
- Wird verwendet, um Replay-Angriffe zu verhindern
Validierung:
const now = Math.floor(Date.now() / 1000);
const maxAge = 3600; // 1 Stunde
if (now - payload.iat > maxAge) {
throw new Error('Token too old');
}
7. jti (JWT ID - JWT-Kennung):
"jti": "token-789"
Beschreibung:
- Eindeutige Kennung des Tokens
- Wird verwendet, um Replay-Angriffe zu verhindern
- Kann zum Widerrufen von Token verwendet werden
Verwendung:
// Prüfen, ob Token widerrufen wurde
const isRevoked = await checkRevokedToken(payload.jti);
if (isRevoked) {
throw new Error('Token has been revoked');
}
Optionale Claims (Optional Claims)
8. scope (Scope - Geltungsbereich):
"scope": "read write profile"
Beschreibung:
- Autorisierter Berechtigungsbereich
- Durch Leerzeichen getrennte Zeichenkette
- Ressourcenserver führt basierend darauf Berechtigungskontrolle durch
Verwendung:
const scopes = payload.scope.split(' ');
if (!scopes.includes('write')) {
return res.status(403).json({ error: 'Insufficient scope' });
}
9. Benutzerdefinierte Claims:
{
"iss": "https://authorization-server.example.com",
"exp": 1639533600,
"aud": "https://api.example.com",
"sub": "user-123",
"client_id": "client-456",
// Benutzerdefinierte Claims
"email": "[email protected]",
"name": "John Doe",
"roles": ["admin", "editor"],
"tenant_id": "tenant-789",
"permissions": ["read:articles", "write:articles"]
}
Beschreibung:
- Beliebige benutzerdefinierte Claims können hinzugefügt werden
- Wird verwendet, um Kontextinformationen zu übertragen
- Reduziert Datenbankabfragen
Hinweis:
⚠️ Token-Größenbeschränkung (normalerweise < 8KB)
⚠️ Keine sensiblen Informationen einschließen (Token kann dekodiert werden)
⚠️ Netzwerkübertragungskosten berücksichtigen
Vollständiges JWT-Beispiel
Vollständiges JWT-Zugriffstoken:
eyJhbGciOiJSUzI1NiIsInR5cCI6ImF0K2p3dCIsImtpZCI6IjEyMyJ9.eyJpc3MiOiJodHRwczovL2F1dGhvcml6YXRpb24tc2VydmVyLmV4YW1wbGUuY29tIiwiZXhwIjoxNjM5NTMzNjAwLCJhdWQiOiJodHRwczovL2FwaS5leGFtcGxlLmNvbSIsInN1YiI6InVzZXItMTIzIiwiY2xpZW50X2lkIjoiY2xpZW50LTQ1NiIsImlhdCI6MTYzOTUzMDAwMCwianRpIjoidG9rZW4tNzg5Iiwic2NvcGUiOiJyZWFkIHdyaXRlIn0.signature
Aufschlüsselung:
Header (Kopfzeile):
{
"alg": "RS256",
"typ": "at+jwt", ← Typ: Zugriffstoken-JWT
"kid": "123"
}
Payload (Nutzdaten):
{
"iss": "https://authorization-server.example.com",
"exp": 1639533600,
"aud": "https://api.example.com",
"sub": "user-123",
"client_id": "client-456",
"iat": 1639530000,
"jti": "token-789",
"scope": "read write"
}
Signature (Signatur):
RSASSA-PKCS1-v1_5 using SHA-256
Signierung und Validierung
Empfohlene Algorithmen
RFC 9068 empfiehlt:
Prioritätsempfehlung:
1. RS256 (RSA mit SHA-256)
- Asymmetrische Verschlüsselung
- Validierung mit öffentlichem Schlüssel
- Am häufigsten verwendet
2. ES256 (ECDSA mit P-256 und SHA-256)
- Asymmetrische Verschlüsselung
- Kürzere Signatur
- Bessere Leistung
Nicht empfohlen:
❌ HS256 (HMAC mit SHA-256)
- Symmetrische Verschlüsselung
- Erfordert gemeinsamen Schlüssel
- Geringere Sicherheit (alle Ressourcenserver benötigen Schlüssel)
Warum asymmetrische Algorithmen empfohlen werden?
→ Autorisierungsserver signiert mit privatem Schlüssel
→ Ressourcenserver validiert mit öffentlichem Schlüssel
→ Öffentlicher Schlüssel kann öffentlich verteilt werden
→ Kein gemeinsamer Schlüssel erforderlich
JWT-Generierung (Autorisierungsserver)
const jose = require('jose');
class JWTAccessTokenIssuer {
constructor(privateKey, issuer, keyId) {
this.privateKey = privateKey;
this.issuer = issuer;
this.keyId = keyId;
}
async issueAccessToken(userId, clientId, audience, scope, expiresIn = 900) {
const now = Math.floor(Date.now() / 1000);
const payload = {
// Erforderliche Claims
iss: this.issuer,
exp: now + expiresIn, // Standard 15 Minuten
aud: audience,
sub: userId,
client_id: clientId,
iat: now,
jti: this.generateJti(),
// Optionale Claims
scope: scope
};
// JWT generieren
const jwt = await new jose.SignJWT(payload)
.setProtectedHeader({
alg: 'RS256',
typ: 'at+jwt', // Wichtig: Als Zugriffstoken kennzeichnen
kid: this.keyId
})
.sign(this.privateKey);
return jwt;
}
generateJti() {
return require('crypto').randomBytes(16).toString('hex');
}
}
// Verwendungsbeispiel
const { generateKeyPair } = require('jose');
const { privateKey, publicKey } = await generateKeyPair('RS256');
const issuer = new JWTAccessTokenIssuer(
privateKey,
'https://authorization-server.example.com',
'key-123'
);
const accessToken = await issuer.issueAccessToken(
'user-123', // userId
'client-456', // clientId
'https://api.example.com', // audience
'read write', // scope
900 // 15 Minuten
);
console.log('Access Token:', accessToken);
JWT-Validierung (Ressourcenserver)
const jose = require('jose');
class JWTAccessTokenValidator {
constructor(issuer, audience) {
this.issuer = issuer;
this.audience = audience;
this.jwksCache = null;
this.jwksCacheTime = 0;
}
async validate(token) {
try {
// Schritt 1: JWKS abrufen (öffentlicher Schlüsselsatz)
const jwks = await this.getJWKS();
// Schritt 2: JWT validieren
const { payload, protectedHeader } = await jose.jwtVerify(
token,
jwks,
{
issuer: this.issuer,
audience: this.audience,
typ: 'at+jwt' // Typ bestätigen
}
);
// Schritt 3: Zusätzliche Validierung
this.validatePayload(payload);
return {
valid: true,
payload,
header: protectedHeader
};
} catch (err) {
return {
valid: false,
error: err.message
};
}
}
async getJWKS() {
// JWKS 1 Stunde cachen
const now = Date.now();
if (this.jwksCache && (now - this.jwksCacheTime < 3600000)) {
return this.jwksCache;
}
// JWKS vom Autorisierungsserver abrufen
const jwksUrl = `${this.issuer}/.well-known/jwks.json`;
const jwks = jose.createRemoteJWKSet(new URL(jwksUrl));
this.jwksCache = jwks;
this.jwksCacheTime = now;
return jwks;
}
validatePayload(payload) {
// Erforderliche Claims prüfen
const required = ['iss', 'exp', 'aud', 'sub', 'client_id', 'iat', 'jti'];
for (const claim of required) {
if (!payload[claim]) {
throw new Error(`Missing required claim: ${claim}`);
}
}
// client_id-Format prüfen
if (typeof payload.client_id !== 'string') {
throw new Error('client_id must be a string');
}
// Weitere benutzerdefinierte Validierungen können hinzugefügt werden...
}
// Scope extrahieren
getScopes(payload) {
if (!payload.scope) return [];
return payload.scope.split(' ');
}
// Berechtigung prüfen
hasScope(payload, requiredScope) {
const scopes = this.getScopes(payload);
return scopes.includes(requiredScope);
}
}
// Verwendungsbeispiel
const validator = new JWTAccessTokenValidator(
'https://authorization-server.example.com',
'https://api.example.com'
);
// Zugriffstoken validieren
const result = await validator.validate(accessToken);
if (result.valid) {
console.log('Token is valid');
console.log('User ID:', result.payload.sub);
console.log('Client ID:', result.payload.client_id);
console.log('Scopes:', result.payload.scope);
} else {
console.error('Token is invalid:', result.error);
}
Express-Middleware-Implementierung
JWT-Validierungs-Middleware
const express = require('express');
const jose = require('jose');
// JWT-Validierungs-Middleware
function jwtAuthMiddleware(options = {}) {
const {
issuer,
audience,
requiredScopes = []
} = options;
const validator = new JWTAccessTokenValidator(issuer, audience);
return async (req, res, next) => {
try {
// Schritt 1: Token extrahieren
const token = extractToken(req);
if (!token) {
return res.status(401).json({
error: 'invalid_request',
error_description: 'Missing access token'
});
}
// Schritt 2: Token validieren
const result = await validator.validate(token);
if (!result.valid) {
return res.status(401).json({
error: 'invalid_token',
error_description: result.error
});
}
// Schritt 3: Scope prüfen
if (requiredScopes.length > 0) {
const hasAllScopes = requiredScopes.every(scope =>
validator.hasScope(result.payload, scope)
);
if (!hasAllScopes) {
return res.status(403).json({
error: 'insufficient_scope',
error_description: `Required scopes: ${requiredScopes.join(', ')}`
});
}
}
// Schritt 4: An Request-Objekt anhängen
req.auth = {
token: result.payload,
userId: result.payload.sub,
clientId: result.payload.client_id,
scopes: validator.getScopes(result.payload)
};
next();
} catch (err) {
console.error('Auth middleware error:', err);
res.status(500).json({
error: 'server_error',
error_description: 'Internal server error'
});
}
};
}
// Token-Extraktions-Hilfsfunktion
function extractToken(req) {
const authHeader = req.headers.authorization;
if (!authHeader) {
return null;
}
const parts = authHeader.split(' ');
if (parts.length !== 2 || parts[0] !== 'Bearer') {
return null;
}
return parts[1];
}
// Verwendungsbeispiel
const app = express();
// JWT-Validierung konfigurieren
const jwtAuth = jwtAuthMiddleware({
issuer: 'https://authorization-server.example.com',
audience: 'https://api.example.com'
});
// Öffentlicher Endpunkt (keine Authentifizierung erforderlich)
app.get('/api/public', (req, res) => {
res.json({ message: 'Public endpoint' });
});
// Geschützter Endpunkt (Authentifizierung erforderlich)
app.get('/api/protected', jwtAuth, (req, res) => {
res.json({
message: 'Protected endpoint',
user: req.auth.userId,
client: req.auth.clientId
});
});
// Endpunkt, der spezifische Scopes erfordert
app.post('/api/articles',
jwtAuthMiddleware({
issuer: 'https://authorization-server.example.com',
audience: 'https://api.example.com',
requiredScopes: ['write', 'articles']
}),
(req, res) => {
res.json({
message: 'Article created',
author: req.auth.userId
});
}
);
// Dynamische Scope-Prüfung
app.delete('/api/articles/:id', jwtAuth, async (req, res) => {
const article = await getArticle(req.params.id);
// Berechtigung prüfen: Administrator oder Autor
const isAdmin = req.auth.scopes.includes('admin');
const isAuthor = article.authorId === req.auth.userId;
if (!isAdmin && !isAuthor) {
return res.status(403).json({
error: 'insufficient_permissions',
error_description: 'You can only delete your own articles'
});
}
await deleteArticle(req.params.id);
res.json({ message: 'Article deleted' });
});
app.listen(3000, () => {
console.log('API server running on http://localhost:3000');
});
Scope-Validierungs-Decorator
// Erweiterte Scope-Validierung
class ScopeValidator {
// Alle Scopes erforderlich (UND-Logik)
static requireAll(...scopes) {
return (req, res, next) => {
const hasAll = scopes.every(scope =>
req.auth.scopes.includes(scope)
);
if (!hasAll) {
return res.status(403).json({
error: 'insufficient_scope',
error_description: `Required scopes: ${scopes.join(' AND ')}`
});
}
next();
};
}
// Beliebigen Scope erforderlich (ODER-Logik)
static requireAny(...scopes) {
return (req, res, next) => {
const hasAny = scopes.some(scope =>
req.auth.scopes.includes(scope)
);
if (!hasAny) {
return res.status(403).json({
error: 'insufficient_scope',
error_description: `Required scopes: ${scopes.join(' OR ')}`
});
}
next();
};
}
// Komplexer Ausdruck
static requireExpression(expression) {
return (req, res, next) => {
const scopes = req.auth.scopes;
// Beispiel: "(read AND write) OR admin"
const result = evaluateExpression(expression, scopes);
if (!result) {
return res.status(403).json({
error: 'insufficient_scope',
error_description: `Required: ${expression}`
});
}
next();
};
}
}
// Verwendung
app.get('/api/data',
jwtAuth,
ScopeValidator.requireAll('read', 'data'),
(req, res) => {
// Erfordert read UND data
}
);
app.post('/api/admin/users',
jwtAuth,
ScopeValidator.requireAny('admin', 'super_admin'),
(req, res) => {
// Erfordert admin ODER super_admin
}
);
Token-Widerrufsstrategien
Problem und Lösungen
JWT-Widerrufsproblem:
Problem:
JWT ist selbstenthalten, Ressourcenserver fragt Autorisierungsserver nicht ab
→ Token kann nicht sofort widerrufen werden
Szenarien:
1. Benutzer meldet sich ab
2. Passwort wird geändert
3. Berechtigungen werden widerrufen
4. Sicherheitsvorfall
Lösungen:
Lösung 1: Kurzlebige Token
// Kurzlebige Zugriffstoken ausstellen (5-15 Minuten)
const accessToken = await issuer.issueAccessToken(
userId,
clientId,
audience,
scope,
900 // 15 Minuten
);
Vorteile:
✓ Einfache Implementierung
✓ Automatisch schnelles Ablaufen
✓ Kleines Widerrufsfenster
Nachteile:
- Erfordert häufige Erneuerung
- Benutzererfahrung beeinträchtigt
Lösung 2: Widerrufsliste (Revocation List)
class TokenRevocationList {
constructor() {
this.revokedTokens = new Set();
// Produktionsumgebung sollte Redis oder ähnlichen verteilten Cache verwenden
}
// Token widerrufen
revoke(jti, expiresAt) {
this.revokedTokens.add(jti);
// Ablaufzeit festlegen (automatisch nach Token-Ablauf bereinigen)
setTimeout(() => {
this.revokedTokens.delete(jti);
}, (expiresAt * 1000) - Date.now());
}
// Prüfen, ob widerrufen
isRevoked(jti) {
return this.revokedTokens.has(jti);
}
}
const revocationList = new TokenRevocationList();
// In Validierungs-Middleware prüfen
async function checkRevocation(req, res, next) {
const jti = req.auth.token.jti;
if (revocationList.isRevoked(jti)) {
return res.status(401).json({
error: 'invalid_token',
error_description: 'Token has been revoked'
});
}
next();
}
// Verwendung
app.use('/api', jwtAuth, checkRevocation);
Lösung 3: Versionsnummer/Zeitstempel
// Benutzer-Tabelle Feld hinzufügen
{
userId: '123',
tokenVersion: 5, // Bei jeder Abmeldung/Passwortänderung erhöhen
// oder
tokensInvalidBefore: 1639530000 // Token vor dieser Zeit ungültig
}
// Versionsnummer beim Token-Generieren einschließen
const accessToken = await issuer.issueAccessToken(
userId,
clientId,
audience,
scope,
900,
{ token_version: user.tokenVersion } // Benutzerdefinierter Claim
);
// Bei Validierung prüfen
async function checkTokenVersion(req, res, next) {
const userId = req.auth.userId;
const tokenVersion = req.auth.token.token_version;
const user = await getUserById(userId);
if (tokenVersion < user.tokenVersion) {
return res.status(401).json({
error: 'invalid_token',
error_description: 'Token version outdated'
});
}
next();
}
Lösung 4: Hybridlösung (Empfohlen)
// Mehrere Strategien kombinieren
class HybridRevocationStrategy {
constructor() {
this.shortTermRevocations = new Set(); // Kürzlicher Widerruf (Redis)
this.userVersions = new Map(); // Benutzer-Token-Version (Datenbank)
}
async isValid(payload) {
// Prüfung 1: JTI in Widerrufsliste
if (this.shortTermRevocations.has(payload.jti)) {
return false;
}
// Prüfung 2: Token-Version
const userVersion = await this.getUserTokenVersion(payload.sub);
if (payload.token_version < userVersion) {
return false;
}
// Prüfung 3: Zeitstempel
const invalidBefore = await this.getUserTokensInvalidBefore(payload.sub);
if (payload.iat < invalidBefore) {
return false;
}
return true;
}
async revokeToken(jti, expiresAt) {
// Kurzfristiger Widerruf (bis Token abläuft)
this.shortTermRevocations.add(jti);
setTimeout(() => {
this.shortTermRevocations.delete(jti);
}, (expiresAt * 1000) - Date.now());
}
async revokeAllUserTokens(userId) {
// Benutzer-Token-Version erhöhen
const currentVersion = await this.getUserTokenVersion(userId);
await this.setUserTokenVersion(userId, currentVersion + 1);
}
}
Leistungsoptimierung
JWKS-Caching
class JWKSCache {
constructor(jwksUrl, cacheDuration = 3600000) {
this.jwksUrl = jwksUrl;
this.cacheDuration = cacheDuration;
this.cache = null;
this.cacheTime = 0;
}
async getJWKS() {
const now = Date.now();
// Cache prüfen
if (this.cache && (now - this.cacheTime < this.cacheDuration)) {
return this.cache;
}
// Neues JWKS abrufen
try {
const response = await fetch(this.jwksUrl);
const jwks = await response.json();
this.cache = jwks;
this.cacheTime = now;
return jwks;
} catch (err) {
// Bei Abruf-Fehler aber vorhandenem alten Cache, diesen weiter verwenden
if (this.cache) {
console.warn('JWKS fetch failed, using cached version');
return this.cache;
}
throw err;
}
}
invalidate() {
this.cache = null;
this.cacheTime = 0;
}
}
Validierungsergebnis-Caching
class TokenValidationCache {
constructor(cacheDuration = 60000) { // 1 Minute
this.cache = new Map();
this.cacheDuration = cacheDuration;
}
get(token) {
const entry = this.cache.get(token);
if (!entry) {
return null;
}
if (Date.now() > entry.expiresAt) {
this.cache.delete(token);
return null;
}
return entry.result;
}
set(token, result) {
this.cache.set(token, {
result,
expiresAt: Date.now() + this.cacheDuration
});
// Regelmäßige Bereinigung
this.cleanup();
}
cleanup() {
const now = Date.now();
for (const [token, entry] of this.cache.entries()) {
if (now > entry.expiresAt) {
this.cache.delete(token);
}
}
}
}
// Verwendung
const validationCache = new TokenValidationCache();
async function validateWithCache(token, validator) {
// Cache prüfen
let result = validationCache.get(token);
if (!result) {
// Token validieren
result = await validator.validate(token);
// Nur gültige Token cachen
if (result.valid) {
validationCache.set(token, result);
}
}
return result;
}
Best Practices
1. Token-Größenoptimierung
// ❌ Schlecht: Token zu groß
{
"iss": "https://authorization-server.example.com",
"exp": 1639533600,
"aud": "https://api.example.com",
"sub": "user-123",
"client_id": "client-456",
"iat": 1639530000,
"jti": "token-789",
"scope": "read write",
// Zu viele benutzerdefinierte Claims
"email": "[email protected]",
"full_name": "John Robert Smith Junior",
"profile_picture": "https://cdn.example.com/users/123/profile/large.jpg",
"address": {
"street": "123 Main Street",
"city": "New York",
"state": "NY",
"zip": "10001",
"country": "USA"
},
"preferences": { /* viele Daten */ }
}
// Ergebnis: Token > 2KB
// ✓ Gut: Token schlank
{
"iss": "https://auth.example.com", // Kurze Domain
"exp": 1639533600,
"aud": "https://api.example.com",
"sub": "user-123",
"client_id": "client-456",
"iat": 1639530000,
"jti": "token-789",
"scope": "read write",
// Nur notwendige benutzerdefinierte Claims einschließen
"roles": ["admin"]
}
// Ergebnis: Token < 500 Bytes
Strategie:
1. Kurze Aussteller-URL verwenden
2. Benutzerdefinierte Claims minimieren
3. Große Daten serverseitig speichern, Token nur Referenz einschließen
4. Rollen statt detaillierte Berechtigungen verwenden
2. Sicherheit
// ✓ Korrekte Sicherheitskonfiguration
const securityConfig = {
// Kurzlebige Token
accessTokenTTL: 900, // 15 Minuten
// Asymmetrischen Algorithmus verwenden
algorithm: 'RS256',
// typ-Header
tokenType: 'at+jwt',
// Spezifisches Audience
audience: 'https://api.example.com', // Keine Wildcards verwenden
// HTTPS erzwingen
requireHTTPS: true,
// Uhrzeit-Abweichungstoleranz
clockTolerance: 60 // 1 Minute
};
// ❌ Unsichere Konfiguration
const insecureConfig = {
accessTokenTTL: 86400, // 24 Stunden - zu lang!
algorithm: 'HS256', // Symmetrischer Algorithmus - nicht empfohlen!
tokenType: 'JWT', // Nicht spezifisch
audience: '*', // Wildcard - gefährlich!
requireHTTPS: false // HTTP - unsicher!
};
3. Fehlerbehandlung
// Umfassende Fehlerbehandlung
class JWTErrorHandler {
static handle(err, req, res, next) {
if (err.name === 'JWTExpired') {
return res.status(401).json({
error: 'invalid_token',
error_description: 'The access token expired',
error_uri: 'https://docs.example.com/errors/token-expired'
});
}
if (err.name === 'JWTClaimValidationFailed') {
return res.status(401).json({
error: 'invalid_token',
error_description: `Token validation failed: ${err.claim}`,
error_uri: 'https://docs.example.com/errors/invalid-token'
});
}
if (err.name === 'JWSSignatureVerificationFailed') {
return res.status(401).json({
error: 'invalid_token',
error_description: 'Token signature verification failed'
});
}
// Allgemeiner Fehler
console.error('JWT error:', err);
res.status(401).json({
error: 'invalid_token',
error_description: 'Token validation failed'
});
}
}
app.use(JWTErrorHandler.handle);
Zusammenfassung
Kernpunkte
✓ JWT-Zugriffstoken sind selbstenthalten
✓ Keine Introspektion erforderlich → hohe Leistung
✓ Offline-Validierung → skalierbar
✓ Enthält Kontext → reduziert Datenbankabfragen
✓ Standardisiertes Format → Interoperabilität
Erforderliche Claims:
- iss, exp, aud, sub, client_id, iat, jti
Empfohlene Praktiken:
✓ RS256- oder ES256-Algorithmus verwenden
✓ Kurzlebige Token (5-15 Minuten)
✓ typ-Header auf "at+jwt" setzen
✓ Spezifisches Audience
✓ Token-Größe minimieren
✓ Widerrufsstrategie implementieren
Leistungsoptimierung:
✓ JWKS-Caching
✓ Validierungsergebnis-Caching
✓ Asynchrone Validierung
Sicherheitsüberlegungen:
✓ HTTPS erzwingen
✓ Alle Claims validieren
✓ Widerrufsmechanismus implementieren
✓ Anomale Token überwachen
Referenzen
Verwandte RFCs:
- [RFC 9068] JWT Profile for OAuth 2.0 Access Tokens ← Dieses Dokument
- [RFC 7519] JSON Web Token (JWT)
- [RFC 7515] JSON Web Signature (JWS)
- [RFC 6749] OAuth 2.0 Authorization Framework
- [RFC 7662] OAuth 2.0 Token Introspection
Verwandte Bibliotheken:
- jose - JavaScript JWT-Bibliothek
- jsonwebtoken - Node.js JWT-Bibliothek
Zusammenfassung: RFC 9068 definiert das Standardformat für OAuth 2.0-Zugriffstoken unter Verwendung von JWT. Durch Selbstenthalten und Offline-Validierung werden API-Leistung und Skalierbarkeit erheblich verbessert. In Kombination mit kurzlebigen Token und geeigneten Widerrufsstrategien kann hohe Leistung bei gleichzeitiger Sicherheit gewährleistet werden!