Die JWT-Authentifizierung (JSON Web Token) ist der Standard zum Sichern von REST-APIs in Node.js-Anwendungen. Im Jahr 2026 sind die Implementierungsmuster gut etabliert – aber viele Tutorials überspringen wichtige Sicherheitsdetails wie die Rotation von Aktualisierungstoken, die ordnungsgemäße Verwaltung von Geheimnissen und die Ungültigmachung von Token. Dieser Leitfaden behandelt die vollständige, produktionsreife Implementierung.
📋 Table of Contents
So funktioniert die JWT-Authentifizierung
- Der Benutzer meldet sich mit Anmeldeinformationen an → der Server validiert sie
- Der Server gibt zwei Token aus:Zugriffstoken (kurzlebig, 15 Min.) undAktualisierungstoken (langlebig, 7 Tage)
- Der Client sendet für jede Anfrage ein Zugriffstoken im Autorisierungsheader
- Wenn das Zugriffstoken abläuft, sendet der Client ein Aktualisierungstoken, um ein neues Zugriffstoken
- zu erhalten Beim Abmelden wird das Aktualisierungstoken serverseitig ungültig gemacht
Einrichtung
npm install express jsonwebtoken bcryptjs cookie-parser ioredis
npm install -D @types/jsonwebtoken @types/bcryptjs
Token-Dienst
// services/tokenService.js
const jwt = require('jsonwebtoken');
const Redis = require('ioredis');
const redis = new Redis(process.env.REDIS_URL);
const ACCESS_SECRET = process.env.JWT_ACCESS_SECRET;
const REFRESH_SECRET = process.env.JWT_REFRESH_SECRET;
// CRITICAL: Use different secrets for access and refresh tokens
function generateAccessToken(payload) {
return jwt.sign(payload, ACCESS_SECRET, {
expiresIn: '15m',
issuer: 'myapp',
audience: 'myapp-users',
});
}
function generateRefreshToken(payload) {
return jwt.sign(payload, REFRESH_SECRET, {
expiresIn: '7d',
issuer: 'myapp',
audience: 'myapp-users',
jwtid: crypto.randomUUID(), // unique ID for invalidation
});
}
function verifyAccessToken(token) {
return jwt.verify(token, ACCESS_SECRET, {
issuer: 'myapp',
audience: 'myapp-users',
});
}
async function verifyRefreshToken(token) {
const payload = jwt.verify(token, REFRESH_SECRET, {
issuer: 'myapp',
audience: 'myapp-users',
});
// Check token hasn't been revoked
const isRevoked = await redis.get(`revoked:${payload.jti}`);
if (isRevoked) throw new Error('Token revoked');
return payload;
}
async function revokeRefreshToken(token) {
try {
const payload = jwt.decode(token);
if (payload?.jti && payload?.exp) {
const ttl = payload.exp - Math.floor(Date.now() / 1000);
if (ttl > 0) {
await redis.setex(`revoked:${payload.jti}`, ttl, '1');
}
}
} catch (err) {
// Ignore decode errors on logout
}
}
module.exports = {
generateAccessToken, generateRefreshToken,
verifyAccessToken, verifyRefreshToken, revokeRefreshToken
};
Authentifizierungsrouten
// routes/auth.js
const express = require('express');
const bcrypt = require('bcryptjs');
const router = express.Router();
const { generateAccessToken, generateRefreshToken,
verifyRefreshToken, revokeRefreshToken } = require('../services/tokenService');
const { findUserByEmail } = require('../db/users');
// POST /auth/login
router.post('/login', async (req, res) => {
const { email, password } = req.body;
if (!email || !password) {
return res.status(400).json({ error: 'Email and password required' });
}
const user = await findUserByEmail(email);
if (!user || !(await bcrypt.compare(password, user.hashedPassword))) {
// Same error for missing user and wrong password (prevent user enumeration)
return res.status(401).json({ error: 'Invalid credentials' });
}
const payload = { sub: user.id, email: user.email, role: user.role };
const accessToken = generateAccessToken(payload);
const refreshToken = generateRefreshToken(payload);
// Store refresh token in httpOnly cookie (not accessible to JS)
res.cookie('refreshToken', refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days in ms
});
res.json({ accessToken, user: { id: user.id, email: user.email, role: user.role } });
});
// POST /auth/refresh
router.post('/refresh', async (req, res) => {
const refreshToken = req.cookies.refreshToken;
if (!refreshToken) return res.status(401).json({ error: 'No refresh token' });
try {
const payload = await verifyRefreshToken(refreshToken);
// Refresh token rotation: revoke old, issue new
await revokeRefreshToken(refreshToken);
const newRefreshToken = generateRefreshToken({
sub: payload.sub, email: payload.email, role: payload.role
});
const newAccessToken = generateAccessToken({
sub: payload.sub, email: payload.email, role: payload.role
});
res.cookie('refreshToken', newRefreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000,
});
res.json({ accessToken: newAccessToken });
} catch (err) {
res.clearCookie('refreshToken');
res.status(401).json({ error: 'Invalid or expired refresh token' });
}
});
// POST /auth/logout
router.post('/logout', async (req, res) => {
const refreshToken = req.cookies.refreshToken;
if (refreshToken) await revokeRefreshToken(refreshToken);
res.clearCookie('refreshToken');
res.json({ message: 'Logged out successfully' });
});
module.exports = router;
Authentifizierungs-Middleware
// middleware/authenticate.js
const { verifyAccessToken } = require('../services/tokenService');
function authenticate(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Access token required' });
}
const token = authHeader.split(' ')[1];
try {
const payload = verifyAccessToken(token);
req.user = payload; // { sub, email, role, iat, exp }
next();
} catch (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Access token expired', code: 'TOKEN_EXPIRED' });
}
res.status(401).json({ error: 'Invalid access token' });
}
}
// Role-based authorization
function authorize(...roles) {
return (req, res, next) => {
if (!roles.includes(req.user?.role)) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
}
module.exports = { authenticate, authorize };
Routen schützen
const { authenticate, authorize } = require('./middleware/authenticate');
// Public route
app.get('/health', (req, res) => res.json({ status: 'ok' }));
// Authenticated route
app.get('/api/profile', authenticate, (req, res) => {
res.json({ userId: req.user.sub, email: req.user.email });
});
// Role-restricted route
app.delete('/api/users/:id', authenticate, authorize('admin'), async (req, res) => {
await deleteUser(req.params.id);
res.json({ deleted: true });
});
Kritische Sicherheitscheckliste
- Speichern Sie niemals Token in localStorage – anfällig für XSS. Verwenden Sie httpOnly-Cookies für Aktualisierungstoken.
- Verwenden Sie separate Geheimnisse für Zugriffs- und Aktualisierungstoken. Wenn das Zugriffsgeheimnis preisgegeben wird, bleiben die Aktualisierungstoken sicher.
- Kurzes Ablaufdatum des Zugriffstokens (maximal 15 Minuten). Langlebige Zugriffstoken können ohne eine Sperrliste nicht ungültig gemacht werden.
- Token-Rotation aktualisieren – Bei jeder Aktualisierung wird ein neues Aktualisierungstoken ausgegeben und das alte widerrufen. Erkennt Token-Diebstahl.
- Nur HTTPS in der Produktion – JWTs während der Übertragung müssen verschlüsselt werden.
- Iss- und aud-Ansprüche validieren – verhindert, dass Token von einem Dienst in einem anderen verwendet werden.
Häufig gestellte Fragen
F: Sitzungen vs. JWT – was ist besser?
A: Sitzungen (serverseitiger Status) lassen sich leichter sofort ungültig machen. JWTs sind zustandslos und skalieren horizontal ohne einen gemeinsamen Sitzungsspeicher. Verwenden Sie Sitzungen für einfachere Apps. JWT für APIs, die von Mobil-/SPA-Clients oder Mikrodiensten genutzt werden.
F: Wie mache ich ein JWT vor Ablauf ungültig?
A: Zwei Ansätze: (1) Führen Sie eine Redis-Denylist der widerrufenen Token-JTIs – überprüft bei jeder Anfrage. (2) Verkürzen Sie den Ablauf des Zugriffstokens, sodass das Schadensfenster klein ist. Option 1 ist sicherer; Option 2 ist einfacher.
F: Wo soll das Zugriffstoken auf dem Client gespeichert werden?
A: Im Speicher (JavaScript-Variable) – nicht localStorage oder sessionStorage. Speicherspeicher bedeutet, dass er bei der Seitenaktualisierung verloren geht und ein Aktualisierungstokenaufruf erforderlich ist, um einen neuen zu erhalten – das ist das beabsichtigte Verhalten.
F: Sollte ich RS256 oder HS256 verwenden?
A: HS256 (symmetrisches, gemeinsames Geheimnis) für Dienste, die Sie vollständig kontrollieren. RS256 (asymmetrische, öffentliche/private Schlüssel), wenn Dritte Token verifizieren müssen, ohne Ihr Geheimnis zu kennen – wie Microservices oder Partner.
F: Wie gehe ich mit „Token abgelaufen“ im Frontend um?
A: 401-Antworten mit einemcode: "TOKEN_EXPIRED"abfangen Header, rufen Sie automatisch/auth/refreshauf , holen Sie sich ein neues Zugriffstoken und wiederholen Sie die ursprüngliche Anfrage. Die meisten HTTP-Clients (axios, ky) unterstützen Anforderungsinterceptoren für dieses Muster.
Fazit
Die Produktions-JWT-Authentifizierung erfordert mehr als nurjwt.sign() undjwt.verify(). Die vollständige Implementierung umfasst: httpOnly-Cookies für Aktualisierungstoken, Tokenrotation bei jeder Aktualisierung, Redis-gestützten Widerruf für die Abmeldung, separate Geheimnisse für jeden Tokentyp und rollenbasierte Autorisierungs-Middleware. Diese Architektur skaliert horizontal (zustandslose Zugriffstoken), behandelt Token-Diebstahl (Rotation erkennt Wiederverwendung) und unterstützt die ordnungsgemäße Abmeldung (Sperrliste). Beginnen Sie mit dieser Grundlage und Sie müssen Ihr Authentifizierungssystem später nicht umgestalten.
🔗 Share this article
✍️ Leave a Comment