JWT-Authentifizierung: Wie JSON Web Tokens funktionieren

· 12 Min. Lesezeit

Inhaltsverzeichnis

Was ist ein JSON Web Token?

Ein JSON Web Token (JWT) ist eine kompakte, URL-sichere Methode, um Ansprüche zwischen zwei Parteien darzustellen. JWTs sind zum De-facto-Standard für Authentifizierung und Autorisierung in modernen Webanwendungen, APIs und Microservices-Architekturen geworden.

Wenn sich ein Benutzer anmeldet, generiert der Server ein JWT mit Benutzerinformationen und sendet es an den Client. Der Client fügt dieses Token dann in nachfolgende Anfragen ein, um seine Identität nachzuweisen. Stellen Sie es sich wie einen digitalen Reisepass vor, der Ihre Anmeldedaten enthält und verifiziert werden kann, ohne jedes Mal bei der ausstellenden Behörde nachzufragen.

Im Gegensatz zur traditionellen sitzungsbasierten Authentifizierung, bei der der Server den Sitzungsstatus im Speicher oder in einer Datenbank verwaltet, sind JWTs zustandslos. Alle Informationen, die zur Verifizierung des Tokens benötigt werden, sind im Token selbst enthalten. Diese architektonische Entscheidung bringt mehrere Vorteile:

Sie können jedes JWT mit unserem JWT-Decoder-Tool inspizieren und dekodieren, um seinen Inhalt zu sehen, ohne Code schreiben zu müssen. Dies ist besonders nützlich beim Debuggen von Authentifizierungsproblemen oder beim Verstehen, welche Daten Ihre Tokens enthalten.

Profi-Tipp: JWTs sind standardmäßig signiert, nicht verschlüsselt. Jeder kann ein JWT dekodieren und lesen. Speichern Sie niemals sensible Informationen wie Passwörter, Kreditkartennummern oder Sozialversicherungsnummern in JWT-Payloads.

JWT-Struktur erklärt

Ein JWT besteht aus drei verschiedenen Teilen, die durch Punkte (.) getrennt sind: dem Header, dem Payload und der Signatur. So sieht ein echtes JWT aus:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiZW1haWwiOiJqb2huQGV4YW1wbGUuY29tIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE1MTYyNDI2MjJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Lassen Sie uns jede Komponente aufschlüsseln, um zu verstehen, wie sie zusammenarbeiten, um ein sicheres, verifizierbares Token zu erstellen.

1. Header

Der Header enthält typischerweise zwei kritische Felder, die definieren, wie das Token verarbeitet werden soll:

{
  "alg": "HS256",
  "typ": "JWT"
}

Dieses JSON-Objekt wird dann Base64Url-kodiert, um den ersten Teil des JWT zu bilden. Die Kodierung macht es URL-sicher und kompakt für die Übertragung in HTTP-Headern.

2. Payload

Der Payload enthält Claims – Aussagen über den Benutzer und zusätzliche Metadaten. Hier speichern Sie die tatsächlichen Daten, die Sie übertragen möchten:

{
  "sub": "1234567890",
  "name": "John Doe",
  "email": "[email protected]",
  "role": "admin",
  "iat": 1516239022,
  "exp": 1516242622
}

Claims werden in drei Kategorien unterteilt:

Registrierte Claims (standardisiert und empfohlen):

Öffentliche Claims sind benutzerdefinierte Claims, die in der IANA JSON Web Token Registry definiert werden sollten oder kollisionsresistente Namen (wie URLs) verwenden.

Private Claims sind benutzerdefinierte Claims, auf die sich die Parteien geeinigt haben, wie role, permissions oder department.

3. Signatur

Die Signatur macht JWTs sicher und manipulationssicher. Sie wird erstellt, indem der kodierte Header, der kodierte Payload, ein geheimer Schlüssel genommen und der im Header angegebene Algorithmus angewendet wird:

HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)

Die Signatur erfüllt zwei kritische Zwecke:

  1. Integritätsprüfung: Stellt sicher, dass das Token seit seiner Ausstellung nicht verändert wurde
  2. Authentifizierung: Beweist, dass das Token von jemandem mit Zugriff auf den geheimen Schlüssel erstellt wurde

Wenn jemand versucht, den Header oder Payload zu ändern, schlägt die Signaturprüfung fehl und das Token wird abgelehnt.

Wie JWT-Authentifizierung funktioniert

Das Verstehen des vollständigen Authentifizierungsablaufs ist für die korrekte Implementierung von JWTs unerlässlich. Hier ist der schrittweise Prozess:

Schritt 1: Benutzeranmeldung

Der Benutzer übermittelt seine Anmeldedaten (Benutzername und Passwort) an den Authentifizierungsendpunkt. Der Server validiert diese Anmeldedaten gegen die Datenbank.

Schritt 2: Token-Generierung

Wenn die Anmeldedaten gültig sind, generiert der Server ein JWT mit Benutzerinformationen und Claims. Der Server signiert das Token mit einem geheimen Schlüssel (für symmetrische Algorithmen) oder einem privaten Schlüssel (für asymmetrische Algorithmen).

Schritt 3: Token-Übermittlung

Der Server sendet das JWT zurück an den Client, typischerweise im Response-Body. Der Client speichert dieses Token für zukünftige Anfragen.

Schritt 4: Authentifizierte Anfragen

Für nachfolgende Anfragen fügt der Client das JWT im Authorization-Header mit dem Bearer-Schema ein:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Schritt 5: Token-Verifizierung

Der Server empfängt die Anfrage, extrahiert das JWT aus dem Header und verifiziert die Signatur mit dem geheimen oder öffentlichen Schlüssel. Wenn gültig, extrahiert der Server Benutzerinformationen aus dem Payload und verarbeitet die Anfrage.

Schritt 6: Token-Ablauf

Wenn das Token abläuft, muss der Client ein neues Token erhalten, entweder durch erneute Authentifizierung oder durch Verwendung eines Refresh-Tokens (im nächsten Abschnitt behandelt).

Schneller Tipp: Validieren Sie immer den exp-Claim auf der Serverseite, auch wenn Sie den Ablauf auf dem Client prüfen. Clientseitige Prüfungen können umgangen werden, aber serverseitige Validierung ist maßgeblich.

Access Tokens vs. Refresh Tokens

Ein robustes JWT-Authentifizierungssystem verwendet typischerweise zwei Arten von Tokens, die zusammenarbeiten: Access Tokens und Refresh Tokens. Das Verständnis des Unterschieds ist entscheidend für den Aufbau sicherer Anwendungen.

Access Tokens

Access Tokens sind kurzlebige JWTs (typischerweise 15 Minuten bis 1 Stunde), die Zugriff auf geschützte Ressourcen gewähren. Sie enthalten Benutzeridentität und Berechtigungen, die zur Autorisierung von Anfragen benötigt werden.

Eigenschaften:

Refresh Tokens

Refresh Tokens sind langlebige Tokens (Tage bis Monate), die ausschließlich zum Erhalt neuer Access Tokens verwendet werden. Sie werden sicher gespeichert und nur an den Token-Refresh-Endpunkt gesendet.

Eigenschaften:

Warum beide verwenden?

Dieser Dual-Token-Ansatz balanciert Sicherheit und Benutzererfahrung:

Aspekt Nur Access Token Access + Refresh Tokens
Sicherheit Niedriger (langlebige Tokens gefährdet) Höher (kurzlebige Access Tokens)
Benutzererfahrung Besser (keine erneute Authentifizierung) Besser (nahtlose Token-Aktualisierung)
Widerruf Schwierig (zustandslos) Möglich (Refresh Tokens in DB)
Netzwerk-Overhead Niedriger Etwas höher (Refresh-Anfragen)
Implementierungskomplexität Einfach Moderat

Token-Refresh-Ablauf

  1. Client erkennt, dass Access Token abgelaufen ist oder bald abläuft
  2. Client sendet Refresh Token an /auth/refresh-Endpunkt
  3. Server validiert Refresh Token gegen Datenbank
  4. Server generiert neues Access Token (und optional neues Refresh Token)
  5. Client empfängt und speichert neue Tokens
  6. Client wiederholt ursprüngliche Anfrage mit neuem Access Token

Profi-Tipp: Implementieren Sie Refresh-Token-Rotation – geben Sie jedes Mal ein neues Refresh Token aus, wenn eines verwendet wird, und invalidieren Sie das alte. Dies begrenzt den Schaden, wenn ein Refresh Token kompromittiert wird.

Node.js-Implementierungsleitfaden

Lassen Sie uns ein vollständiges JWT-Authentifizierungssystem mit Node.js, Express und der jsonwebtoken-Bibliothek erstellen. Diese Implementierung umfasst sowohl Access- als auch Refresh-Tokens.

Installation

npm install express jsonwebtoken bcrypt dotenv

Umgebungskonfiguration

Erstellen Sie eine .env-Datei mit Ihren geheimen Schlüsseln:

ACCESS_TOKEN_SECRET=your-super-secret-access-key-change-this
REFRESH_TOKEN_SECRET=your-super-secret-refresh-key-change-this
ACCESS_TOKEN_EXPIRY=15m
REFRESH_TOKEN_EXPIRY=7d

Token-Generierung

const jwt = require('jsonwebtoken');

function generateAccessToken(user) {
  return jwt.sign(
    { 
      userId: user.id,
      email: user.email,
      role: user.role 
    },
    process.env.ACCESS_TOKEN_SECRET,
    { expiresIn: process.env.ACCESS_TOKEN_EXPIRY }
  );
}

function generateRefreshToken(user) {
  return jwt.sign(
    { userId: user.id },
    process.env.REFRESH_TOKEN_SECRET,
    { expiresIn: process.env.REFRESH_TOKEN_EXPIRY }
  );
}

Login-Endpunkt

const bcrypt = require('bcrypt');

app.post('/auth/login', async (req, res) => {
  try {
    const { email, password } = req.body;
    
    // Benutzer in Datenbank finden
    const user = await User.findOne({ email });
    if (!user) {
      return res.status(401).json({ error: 'Ungültige Anmeldedaten' });
    }
    
    // Passwort verifizieren
    const validPassword = await bcrypt.compare(password, user.password);
    if (!validPassword) {
      return res.status(401).json({ error: 'Ungültige Anmeldedaten' });
    }
    
    // Tokens generieren
    const accessToken = generateAccessToken(user);
    const refreshToken = generateRefreshToken(user);
    
    // Refresh Token in Datenbank speichern
    await RefreshToken.create({
      token: refreshToken,
      userId: user.id,
      expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
    });
    
    res.json({
      accessToken,
      refreshToken,
      user: {
        id: user.id,
        email: user.email,
        name: user.name
      }
    });
  } catch (error) {
    res.status(500).json({ error: 'Serverfehler' });
  }
});

Authentifizierungs-Middleware

function authenticateToken(req, res, next) {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1];
  
  if (!token) {
    return res.status(401).json({ error: 'Access Token erforderlich' });
  }
  
  jwt.verify(token, process.env.ACCESS_TOKEN_SECRET, (err, user) => {
    if (err) {
      return res.status(403).json({ error: 'Ungültiges oder abgelaufenes Token' });
    }
    
    req.user = user;
    next();
  });
}

// Verwendung
app.get('/api/protected', authenticateToken, (req, res) => {
  res.json({ 
    message: 'Geschützte Daten',
    user: req.user 
  });
});

Token-Refresh-Endpunkt

app.post('/auth/refresh', async (req, res) => {
  const { refreshToken } = req.body;
  
  if (!refreshToken) {
    return res.status(401).json({ error: 'Refresh Token erforderlich' });
  }
  
  try {
    // Prüfen, ob Refresh Token in Datenbank existiert
    const storedToken = await RefreshToken.findOne({ token: refreshToken });
    if (!storedToken) {
      return res.status(403).json({ error: 'Ungültiges Refresh Token' });
    }
    
    // Token-Signatur verifizieren
    jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET, async (err, user) => {
      if (err) {
        return res.status(403).json({ error: 'Ungültiges Refresh Token' });
      }
      
      // Benutzerdaten abrufen
      const userData = await User.findById(user.userId);
      
      // Neues Access Token generieren
      const accessToken = generateAccessToken(userData);
      
      res.json({ accessToken });