Tutorial · Fortgeschritten
JWT Authentication erklärt: sichere APIs mit Tokens
JSON Web Tokens (JWT) sind der Standard, um APIs zustandslos abzusichern. Dieses Tutorial erklärt Aufbau und Login-Flow und zeigt eine sichere Umsetzung mit Node.js – mit kurzlebigem Access-Token und langlebigem Refresh-Token.
Warum JWT?
Bei der klassischen Session-Authentifizierung speichert der Server für jeden eingeloggten Nutzer eine Session – das kostet Speicher und erschwert das Skalieren über mehrere Server. Ein JWT dreht das um: Nach dem Login bekommt der Client ein signiertes Token, das er bei jeder Anfrage mitschickt. Der Server muss nichts speichern – er prüft nur die Signatur.
Ein JWT ist damit selbsttragend (enthält alle nötigen Infos) und fälschungssicher: Ohne das geheime Signatur-Secret lässt sich der Inhalt nicht unbemerkt verändern.
Signiert heißt nicht verschlüsselt. Header und Payload eines Standard-JWT sind nur Base64url-kodiert und für jeden lesbar. Lege also niemals Passwörter oder Geheimnisse in die Payload.
Aufbau eines JWT
Ein JWT besteht aus drei durch Punkte getrennten Teilen: header.payload.signature.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 ← Header (Base64url)
.eyJzdWIiOjQyLCJlbWFpbCI6ImFAYi5kZSIsImlhdCI6MTc1MTkwMDAwMCwiZXhwIjoxNzUxOTAwOTAwfQ ← Payload
.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c ← Signatur
- Header – welcher Algorithmus signiert wurde, z. B.
{ "alg": "HS256", "typ": "JWT" }. - Payload – die „Claims“, also Nutzdaten. Übliche Felder:
sub(Subject/User-ID),iat(issued at),exp(Ablauf). - Signatur – Hash aus Header, Payload und Secret. Ändert ein Angreifer die Payload, passt die Signatur nicht mehr und der Server lehnt das Token ab.
Auf jwt.io kannst du ein Token einfügen und siehst sofort Header und Payload dekodiert.
Der Flow: Login bis geschützte Route
- Der Nutzer sendet E-Mail + Passwort an
/auth/login. - Der Server prüft die Daten und stellt bei Erfolg ein Access-Token (kurzlebig) und ein Refresh-Token (langlebig) aus.
- Für jede geschützte Anfrage schickt der Client das Access-Token im Header:
Authorization: Bearer <token>. - Der Server verifiziert die Signatur und gewährt (oder verweigert) Zugriff.
- Läuft das Access-Token ab, holt der Client über das Refresh-Token ein neues – ganz ohne erneuten Login.
Implementierung mit Node.js & Express
Wir nutzen jsonwebtoken (aktuell Version 9.0.3) zum Signieren/Verifizieren und
bcrypt zum sicheren Hashen der Passwörter. cookie-parser liest das
Refresh-Cookie aus.
npm install express jsonwebtoken bcrypt cookie-parser
# Zwei starke, GETRENNTE Secrets erzeugen und als Umgebungsvariablen setzen:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
Grundgerüst & Registrierung
const express = require("express");
const jwt = require("jsonwebtoken");
const bcrypt = require("bcrypt");
const cookieParser = require("cookie-parser");
const app = express();
app.use(express.json());
app.use(cookieParser());
// NIE hart kodieren – immer aus der Umgebung laden:
const ACCESS_SECRET = process.env.ACCESS_SECRET;
const REFRESH_SECRET = process.env.REFRESH_SECRET;
// Demo-"Datenbank" im Speicher
const users = []; // { id, email, passwortHash }
// Access-Token: kurzlebig (15 Minuten)
function createAccessToken(user) {
return jwt.sign(
{ sub: user.id, email: user.email },
ACCESS_SECRET,
{ expiresIn: "15m" }
);
}
// Registrierung – Passwort wird gehasht, nie im Klartext gespeichert
app.post("/auth/register", async (req, res) => {
const { email, passwort } = req.body;
if (!email || !passwort) {
return res.status(400).json({ error: "email und passwort nötig" });
}
const passwortHash = await bcrypt.hash(passwort, 12); // 12 = Kostenfaktor
const user = { id: users.length + 1, email, passwortHash };
users.push(user);
res.status(201).json({ id: user.id, email: user.email });
});
Login: Tokens ausstellen
app.post("/auth/login", async (req, res) => {
const { email, passwort } = req.body;
const user = users.find(u => u.email === email);
// Gleiche Fehlermeldung für "User fehlt" und "Passwort falsch",
// damit Angreifer nicht erkennen, welche E-Mails existieren.
if (!user || !(await bcrypt.compare(passwort, user.passwortHash))) {
return res.status(401).json({ error: "Ungültige Anmeldedaten" });
}
const accessToken = createAccessToken(user);
const refreshToken = jwt.sign(
{ sub: user.id },
REFRESH_SECRET,
{ expiresIn: "7d" }
);
// Refresh-Token als httpOnly-Cookie: für JavaScript unsichtbar (schützt vor XSS)
res.cookie("refreshToken", refreshToken, {
httpOnly: true,
secure: true, // nur über HTTPS
sameSite: "strict", // schützt vor CSRF
maxAge: 7 * 24 * 60 * 60 * 1000
});
// Access-Token bekommt der Client im Body (hält ihn z. B. im Speicher)
res.json({ accessToken });
});
Middleware & geschützte Route
function auth(req, res, next) {
const header = req.headers.authorization || "";
const token = header.startsWith("Bearer ") ? header.slice(7) : null;
if (!token) {
return res.status(401).json({ error: "Kein Token" });
}
try {
// Algorithmus FEST vorgeben – verhindert "alg"-Verwirrungsangriffe
const payload = jwt.verify(token, ACCESS_SECRET, { algorithms: ["HS256"] });
req.user = payload;
next();
} catch (err) {
return res.status(401).json({ error: "Token ungültig oder abgelaufen" });
}
}
// Nur mit gültigem Access-Token erreichbar
app.get("/me", auth, (req, res) => {
res.json({ id: req.user.sub, email: req.user.email });
});
Refresh: neues Access-Token ohne Login
app.post("/auth/refresh", (req, res) => {
const token = req.cookies.refreshToken;
if (!token) {
return res.status(401).json({ error: "Kein Refresh-Token" });
}
try {
const payload = jwt.verify(token, REFRESH_SECRET, { algorithms: ["HS256"] });
const user = users.find(u => u.id === payload.sub);
if (!user) throw new Error("unbekannt");
res.json({ accessToken: createAccessToken(user) });
} catch (err) {
return res.status(401).json({ error: "Refresh-Token ungültig" });
}
});
app.listen(3000, () => console.log("Auth-Server auf http://localhost:3000"));
Sicherheit: worauf es ankommt
| Access-Token | Refresh-Token | |
|---|---|---|
| Lebensdauer | kurz (5–15 Min) | lang (Tage bis Wochen) |
| Zweck | API-Anfragen autorisieren | neues Access-Token holen |
| Aufbewahrung | im Speicher der App | httpOnly-Cookie |
| Bei Diebstahl | läuft schnell ab | rotieren & widerrufen |
- Access-Token kurz halten (5–15 Min). Das begrenzt den Schaden, falls eines abhandenkommt.
- Refresh-Token als
httpOnly-,Secure-,SameSite-Cookie ausliefern – so kommt JavaScript nicht heran (Schutz gegen XSS und CSRF). - Getrennte Secrets für Access- und Refresh-Token, immer aus Umgebungsvariablen / einem Secret-Manager – niemals im Code.
- Algorithmus pinnen: bei
verifyimmeralgorithms: ["HS256"]angeben. Sonst drohen „alg“-Verwirrungsangriffe (z. B.alg: none). - Refresh-Token-Rotation: bei jeder Nutzung ein neues ausstellen und das alte entwerten. Taucht ein bereits genutztes wieder auf, die ganze Token-Familie widerrufen.
- Minimale Payload: nur das Nötigste hineinschreiben – keine sensiblen Daten.
JWT macht Authentifizierung zustandslos und skalierbar. Das Muster „kurzlebiges Access-Token + langlebiges, gut geschütztes Refresh-Token“ verbindet Bequemlichkeit mit Sicherheit – solange Secrets und Algorithmus sauber gehandhabt werden.
Hat dir das Tutorial geholfen?
Die Inhalte sind kostenlos. Über einen Kaffee freue ich mich sehr. ☕