Baustelle: Work in Progress · noindex

Tutorial · Fortgeschritten

JWT Authentication erklärt: sichere APIs mit Tokens

Node.js Security Lesezeit ~11 Min Stand: Juli 2026

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.

Wichtig

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.

JWT (gekürzt)
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9   ← Header (Base64url)
.eyJzdWIiOjQyLCJlbWFpbCI6ImFAYi5kZSIsImlhdCI6MTc1MTkwMDAwMCwiZXhwIjoxNzUxOTAwOTAwfQ   ← Payload
.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c   ← Signatur
Tool-Tipp

Auf jwt.io kannst du ein Token einfügen und siehst sofort Header und Payload dekodiert.

Der Flow: Login bis geschützte Route

  1. Der Nutzer sendet E-Mail + Passwort an /auth/login.
  2. Der Server prüft die Daten und stellt bei Erfolg ein Access-Token (kurzlebig) und ein Refresh-Token (langlebig) aus.
  3. Für jede geschützte Anfrage schickt der Client das Access-Token im Header: Authorization: Bearer <token>.
  4. Der Server verifiziert die Signatur und gewährt (oder verweigert) Zugriff.
  5. 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.

bash
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

javascript — auth.js
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

javascript — auth.js
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

javascript — auth.js
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

javascript — auth.js
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-TokenRefresh-Token
Lebensdauerkurz (5–15 Min)lang (Tage bis Wochen)
ZweckAPI-Anfragen autorisierenneues Access-Token holen
Aufbewahrungim Speicher der ApphttpOnly-Cookie
Bei Diebstahlläuft schnell abrotieren & widerrufen
Zusammengefasst

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.