Zum Hauptinhalt springen

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)​


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​



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!