Zum Hauptinhalt springen

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:


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!