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!