Authentication

Session-Authentifizierung

Die Session-Authentifizierung hält die Wahrheit auf dem Server. Eine zufällige Session-ID wird in einem HttpOnly-Cookie übertragen, während die Identität und Berechtigungen des Benutzers in einem Store liegen, den Sie kontrollieren und jederzeit widerrufen können.

beginner14 min readUpdated 16. Sept. 2026
server.ts
ts
// server.ts
import express from "express";
import session from "express-session";
import { RedisStore } from "connect-redis";

const app = express();

app.use(
  session({
    store: new RedisStore({ client: redis }),
    secret: process.env.SESSION_SECRET!,
    resave: false,
    saveUninitialized: false,
    cookie: {
      httpOnly: true,
      secure: true,
      sameSite: "lax",
      maxAge: 1000 * 60 * 60 * 24, // 1 day
    },
  })
);

app.post("/login", async (req, res) => {
  const user = await verifyCredentials(req.body);
  if (!user) return res.status(401).json({ error: "invalid_credentials" });

  // Rotate the session id so a fixated cookie cannot be reused.
  req.session.regenerate((err) => {
    if (err) return res.status(500).json({ error: "session_error" });
    req.session.userId = user.id;
    res.json({ ok: true });
  });
});
Transport
Opake Session-ID in einem Cookie
Server-Status
Erforderlich (ein Session-Store)
Cookie-Flags
HttpOnly, Secure, SameSite
Typischer Store
Redis oder eine Datenbank
Übliche Laufzeit
30 Minuten bis 2 Wochen
Widerruf
Server-Datensatz löschen
CSRF-Risiko
Ja, Cookies werden automatisch gesendet
Spezifikation
RFC 6265 (Cookies)

Warum es wichtig ist

Was serverseitige Sessions bieten

Status lebt auf dem Server

Der Browser hält nur eine opake Kennung. Identität, Rollen und Berechtigungen bleiben in einem Store, den Sie vollständig kontrollieren, sodass keine sensiblen Daten gegenüber dem Client exponiert werden.

Sofortiger Widerruf

Das Löschen eines Session-Datensatzes meldet einen Benutzer sofort auf jedem Gerät ab, das diese Session teilt, ohne auf das Ablaufdatum eines Tokens warten zu müssen.

Cookies übernehmen den Transport

Der Browser fügt das Cookie automatisch hinzu, und HttpOnly hält es außerhalb der Reichweite von JavaScript, wodurch eine ganze Klasse von Token-Diebstählen ausgeschlossen wird.

Das Gesamtbild

Drei Kernkomponenten

Ein Datensatz in Ihrem Besitz, ein Cookie, das nur eine opake ID transportiert, und ein gemeinsamer Store, über den jeder Server den Datensatz finden kann.

Session-Datensatz

Speichern

Eine Zeile oder ein Schlüssel, der eine zufällige ID einem Benutzer, einem Ablaufdatum und serverseitigen Daten wie einem Warenkorb oder einer Flash-Nachricht zuordnet.

Cookie

Transport

Ein Set-Cookie-Header übergibt die ID an den Browser, welcher sie bei jeder passenden Anfrage zurücksendet, bis sie abläuft oder gelöscht wird.

Gemeinsamer Store

Skalierung

Das Auslagern von Sessions aus dem Prozessspeicher in Redis oder eine Datenbank ermöglicht es jeder Instanz, dieselbe Session zu lesen.

Ablauf

Wie aus einem Login eine Session wird

Jede Anfrage nach dem Login wiederholt denselben Lookup, und ein Logout ist schlicht ein Löschvorgang.

  1. 1

    Anmeldedaten prüfen

    POST /login prüft die übermittelte E-Mail und das Passwort gegen den gespeicherten Passwort-Hash.

  2. 2

    Session-Datensatz erstellen

    Der Server generiert eine zufällige ID und speichert eine Session, die die Benutzer-ID und ein Ablaufdatum im Session-Store enthält.

  3. 3

    Set-Cookie senden

    Die Antwort enthält die opake ID in einem HttpOnly, Secure, SameSite Cookie.

  4. 4

    Cookie zurückgeben

    Der Browser fügt das Cookie jeder späteren Anfrage an dieselbe Seite automatisch hinzu.

  5. 5

    Session laden

    Die middleware liest die ID, sucht sie im Store und fügt den authentifizierten Benutzer der Anfrage hinzu.

  6. 6

    Beim Logout zerstören

    Logout löscht den Datensatz aus dem Store und leert das Cookie in der Antwort.

Eine kurze Geschichte

Drei Jahrzehnte Cookies

  1. 1994

    Netscape führt Cookies ein

    Ein kleiner persistenter Token ermöglicht es einem zustandslosen HTTP-Server, sich zu merken, wer anfragt.

    94
  2. 1997

    Cookies erhalten eine Spezifikation

    RFC 2109 formalisiert Set-Cookie und die Attribute, die das Verhalten bis heute definieren.

    97
  3. 2011

    RFC 6265 konsolidiert die Regeln

    Die Cookie-Spezifikation wird basierend darauf neu geschrieben, was Browser tatsächlich tun, statt auf dem ursprünglichen Vorschlag.

    11
  4. 2016

    SameSite erscheint

    Ein neues Attribut erlaubt es Servern, Cookies von Cross-Site-Anfragen und der dadurch entstehenden CSRF-Angriffsfläche auszuschließen.

    16
  5. 2020

    Lax wird zum Standard

    Chrome behandelt Cookies ohne SameSite als Lax, was das Verhalten für viele Seiten stillschweigend ändert.

    20
  6. 2023

    Partitionierte Cookies

    CHIPS bindet ein Cookie an eine Top-Level-Seite, um Third-Party-Tracking in eingebetteten Inhalten zu unterbinden.

    23

Der vollständige Leitfaden

Session-Authentifizierung: Alles was Sie wissen müssen

Was Session-Authentifizierung ist

Die Session-Authentifizierung ist die älteste und nach wie vor am häufigsten verwendete Methode, um einen Benutzer im Web angemeldet zu halten. Die Idee dahinter ist simpel: Der Server erinnert sich an dich und der Browser bewahrt eine Quittung auf.

Wenn du dich einloggst, überprüft der Server deine Anmeldedaten und erstellt eine Session – einen Datensatz, den er selbst speichert. Dieser Datensatz kann deine User-ID, den Zeitpunkt der Erstellung, das Ablaufdatum und Zustände wie einen Warenkorb enthalten. Der Server sendet dann eine Session-ID an deinen Browser: eine lange, zufällige Zeichenfolge, die genau diesen einen Datensatz identifiziert. Der Browser speichert die ID in einem Cookie und sendet sie bei jeder Anfrage zurück. Eine middleware auf dem Server liest die ID aus, lädt den Datensatz und weiß nun, wer du bist.

Das entscheidende Merkmal ist, dass der Browser einen opaken Identifikator hält und nicht deine Identität. Es ist wie eine Garderobenmarke, nicht der Mantel selbst. Wenn die ID durchsickert, kann ein Angreifer deine Identität übernehmen, bis du oder der Server den Datensatz ungültig machen – aber die ID selbst verrät nichts, kann nicht dekodiert und nicht bearbeitet werden, um zusätzliche Berechtigungen zu erhalten. Alles Relevante existiert hinter der Abfrage des Servers.

Dies ist das Gegenteil eines eigenständigen Tokens wie einem JWT, bei dem die Anmeldedaten signierte Claims tragen und der Server diesen vertraut, ohne einen Datenbank-Roundtrip durchführen zu müssen. Die Session-Authentifizierung entscheidet sich für eine Abfrage im Austausch gegen mehr Kontrolle. Dieser Kompromiss ist das Thema dieses Guides.

Der Session-Lifecycle

Jede Session folgt demselben Ablauf: Sie wird beim Login erstellt, per Cookie übertragen, pro Request geladen und beim Logout zerstört.

Erstellung. Ein erfolgreicher POST /login ist der einzige Ort, an dem eine Session entstehen sollte. Generieren Sie eine kryptografisch zufällige ID – mindestens 128 Bit aus einem CSPRNG, niemals einen Zähler oder einen erratbaren Wert – und speichern Sie einen Datensatz, der über diese ID referenziert wird. Legen Sie ein Ablaufdatum fest. Wenn die App eine „Angemeldet halten“-Funktion unterstützt, wählen Sie bewusst eine längere Lebensdauer, anstatt dies dem Zufall zu überlassen.

Transport. Senden Sie die ID mit einem Set-Cookie Header. Das HttpOnly Flag schützt sie vor dem Zugriff durch JavaScript, Secure beschränkt die Übertragung auf HTTPS und SameSite limitiert, wann sie an Cross-Site-Requests angehängt wird. Ohne diese Flags ist die Session-ID anfällig für XSS und Network Sniffing.

Lookup. Bei jedem folgenden Request liest eine middleware das Cookie aus, sucht die ID im Store und verknüpft entweder den Benutzer mit dem Request oder lehnt den Request ab. Eine fehlende oder abgelaufene ID bedeutet einen anonymen Request, keinen Fehler, sodass öffentliche Seiten weiterhin funktionieren.

Zerstörung. Ein Logout löscht den Datensatz aus dem Store und leert das Cookie. Das Löschen des Datensatzes ist das, was die Session tatsächlich beendet; das Leeren des Cookies ist eine Höflichkeit, die verhindert, dass der Browser eine ungültige ID sendet. Ein serverseitiger Expiry-Job sollte zudem verwaiste Sessions bereinigen, damit der Store nicht unendlich anwächst.

Dieser Lifecycle ist bewusst langweilig, und genau das ist ein Feature. Es gibt einen Ort, der Sessions erstellt, einen Ort, der sie lädt, und einen Ort, der sie zerstört – das ist leicht zu auditieren und einfach zu testen.

Passwörter sind die erste Hürde

Eine Session kann nur so vertrauenswürdig sein wie der Login, der sie erstellt hat. Daher verdient die Passwortspeicherung Beachtung, noch bevor überhaupt ein Cookie zum Einsatz kommt.

Speichern Sie niemals ein Passwort im Klartext. Speichern Sie einen langsamen, gesalzenen Hash, der von einem eigens dafür entwickelten Algorithmus wie Argon2id, bcrypt oder scrypt erzeugt wurde. Diese sind bewusst rechenintensiv, wodurch ein Datenbank-Leak von einem sofortigen Credential-Dump in einen langwierigen und kostspieligen Cracking-Prozess verwandelt wird. Ein Allzweck-Hash wie SHA-256 ist hierfür das falsche Werkzeug: Er ist schnell, und genau das ist es, was ein Angreifer will.

import argon2 from "argon2";

export async function hashPassword(password: string): Promise<string> {
  return argon2.hash(password, { type: argon2.argon2id });
}

export async function verifyPassword(
  password: string,
  hash: string
): Promise<boolean> {
  try {
    return await argon2.verify(hash, password);
  } catch {
    return false;
  }
}

Die Verifizierung muss in konstanter Zeit (constant-time) erfolgen oder, noch besser, an den Vergleichsmechanismus des Algorithmus selbst delegiert werden, damit keine Informationen über das Timing preisgegeben werden. Am wichtigsten ist es, bei einer unbekannten E-Mail-Adresse und einem falschen Passwort den gleichen Fehler zurückzugeben. Die Meldung „Benutzer nicht gefunden“ liefert einem Angreifer ein kostenloses Oracle zur Account-Enumeration. Eine generische invalid_credentials-Antwort, bei der in beiden Fällen ein vergleichbarer Rechenaufwand betrieben wird, verrät hingegen nichts.

Ein erfolgreicher Login ist zudem der richtige Zeitpunkt, um über Rate Limiting, Lockouts nach wiederholten Fehlversuchen und Multi-Faktor-Challenges nachzudenken. All dies geschieht, bevor der Session-Datensatz erstellt wird.

Was in einer Session gespeichert werden sollte

Ein Session-Datensatz sollte ein Zeiger sein, keine Fotokopie. Die Versuchung, das gesamte User-Objekt hineinzustopfen, ist groß und meistens ein Fehler.

Speichern Sie die User ID und lassen Sie den Request den Rest laden. Wenn Sie den User aus Performance-Gründen cachen, geben Sie ihm eine kurze Lebensdauer und invalidieren Sie ihn bei Änderungen. Eine veraltete Kopie in einer langlebigen Session führt nämlich zu Bugs, die schwer zu reproduzieren sind: Ein Admin, dessen Rechte herabgestuft wurden, behält seine alte Rolle, bis er sich erneut anmeldet.

Angemessene Dinge, die serverseitig gespeichert werden können, sind die User ID, eine Rolle oder Tenant ID (sofern diese klein ist und sich selten ändert), ein CSRF-Token, ein „Remember Me“-Flag sowie kurzlebiger UI-State, wie etwa eine Flash-Message oder ein Redirect-Ziel nach dem Login. Vermeiden Sie es, große Objekte, Secrets, Tokens für andere Dienste oder irgendetwas zu speichern, das nicht in einem Debugging-Dump erscheinen sollte.

declare module "express-session" {
  interface SessionData {
    userId: string;
    tenantId: string;
    csrfToken: string;
    flash?: { type: "info" | "error"; message: string };
  }
}

Wenn der Session-Payload über einige Kilobyte anwächst, ist das ein Zeichen dafür, dass die Daten in die Datenbank gehören – keyed by the user und bei Bedarf geladen. Kleine Sessions lassen sich schneller serialisieren, sind günstiger zu speichern und leichter zu handhaben.

Aufbau des middleware-Stacks

Die Session-Verwaltung lässt sich am besten als eine kleine, geordnete Kette darstellen: Cookie parsen, Session laden, Benutzer zuordnen und anschließend geschützte Routen absichern. Jedes Element erledigt genau eine Aufgabe, was das gesamte System testbar macht.

import cookieParser from "cookie-parser";
import session from "express-session";

app.use(cookieParser());
app.use(
  session({
    name: "__Host-sid",
    secret: process.env.SESSION_SECRET!,
    store,
    resave: false,          // do not rewrite unchanged sessions
    saveUninitialized: false, // do not create a session for anonymous visitors
    cookie: {
      httpOnly: true,
      secure: process.env.NODE_ENV === "production",
      sameSite: "lax",
      path: "/",
      maxAge: 1000 * 60 * 60 * 12,
    },
  })
);
app.use(loadUser());        // attach req.user when a session exists
app.use(csrf());            // issue and verify CSRF tokens
app.use("/api", routes);    // handlers can call requireAuth() themselves

Zwei Optionen sorgen meist für die größte Verwirrung. resave: false verhindert, dass die middleware die Session bei jeder Anfrage zurück in den Store schreibt, selbst wenn sich nichts geändert hat, was einen Netzwerk-Roundtrip einspart. saveUninitialized: false verhindert, dass für jeden anonymen Besucher eine Session erstellt wird; so wird vermieden, dass der Store mit leeren Datensätzen gefüllt wird und ein Cookie gesetzt wird, bevor der Benutzer überhaupt eine Aktion ausgeführt hat. Beides sind Einstellungen, die man fast immer aktivieren möchte.

Die Reihenfolge ist entscheidend. Der Cookie-Parser muss vor der Session-middleware laufen, die Session-middleware vor allem, was req.session ausliest, und der User-Loader vor jeder Route, die req.user prüft. Guard-middleware kann global oder pro Route implementiert werden; die Zuweisung pro Route sorgt dafür, dass öffentliche Endpunkte standardmäßig öffentlich bleiben.

SameSite im Detail

SameSite ist das Attribut, das am häufigsten falsch verstanden wird. Daher lohnt es sich, genau zu verstehen, was die einzelnen Werte tatsächlich erlauben.

Strict sendet das Cookie niemals bei einer Anfrage, deren Site sich von der unterscheidet, die das Cookie gesetzt hat. Wenn ein Nutzer in einer E-Mail auf einen Link zu Ihrer App klickt, kommt die Anfrage ohne Session an. Der Nutzer erscheint also beim ersten Navigationsschritt möglicherweise als ausgeloggt und erst nach einem Refresh als eingeloggt. Dies ist die sicherste Einstellung und eignet sich gut für hochsensible Admin-Tools.

Lax sendet das Cookie bei Top-Level-Navigationen unter Verwendung sicherer Methoden, jedoch nicht bei cross-site POSTs, Bildern oder iframes. Dies deckt den häufigen Fall ab, einem Link zu folgen und eingeloggt zu bleiben, während der klassische CSRF-Form-Post blockiert wird. Es ist der vernünftige Standard für die meisten Web-Apps und der Browser-Standard in modernem Chrome, wenn das Attribut weggelassen wird.

None sendet das Cookie bei jeder cross-site Anfrage und erfordert Secure. Dies ist nur für echte cross-site Flows notwendig, wie etwa einen eingebetteten Checkout oder ein Widget, das von einem anderen Origin bereitgestellt wird. Jede Verwendung von None vergrößert Ihre CSRF-Angriffsfläche. Fügen Sie daher Token auf Anwendungsebene hinzu und stellen Sie sicher, dass das Secure-Flag gesetzt ist, da der Browser das Cookie andernfalls komplett ablehnen wird.

Ein wichtiger Detailpunkt: SameSite vergleicht Sites, nicht Origins, und die Definition einer Site ist die registrierbare Domain. app.example.com und api.example.com gelten als dieselbe Site. Eine kompromittierte Subdomain ist daher durch SameSite überhaupt nicht geschützt. Dies ist ein weiterer Grund, warum das __Host--Cookie-Präfix und eine strikte Subdomain-Hygiene wichtig sind.

Eigenbau oder Nutzung einer Library

Es ist verlockend, Sessions von Hand zu implementieren: Ein Cookie setzen, einen Map führen, diesen abfragen. Zum Lernen ist das hervorragend. Für die Produktion ist es meist ein Fehler, da die entscheidenden Details genau jene sind, die man leicht übersieht.

Eine ausgereifte Library wie express-session für Node, das Session-Framework von Django oder der Session-Store von Rails bietet signierte IDs, sichere Cookie-Standardwerte, austauschbare Stores, ID-Regenerierung und die Handhabung von Ablaufzeiten. Sie sollten jeden dieser Mechanismen dennoch verstehen – genau dafür ist dieser Guide da –, aber Sie sollten nicht die einzige Person sein, die sich Gedanken darüber gemacht hat.

Falls Sie dennoch eine eigene Lösung bauen, sieht die Mindest-Checkliste so aus: Eine CSPRNG-ID von mindestens 128 Bit, ein Constant-Time-Lookup, HttpOnly, Secure und SameSite Cookies, ID-Rotation beim Login, serverseitiges Expiry und ein Pfad zum Löschen beim Logout. Wenn einer dieser Punkte fehlt, haben Sie eine Sicherheitslücke und kein Session-System.

Testen der Session-Authentifizierung

Die Authentifizierung ist sicherheitskritisch und verdient daher Tests, die nicht nur den „Happy Path“, sondern gezielt auch Negativfälle prüfen.

import request from "supertest";
import app from "../app.js";

test("protected route rejects anonymous requests", async () => {
  await request(app).get("/api/me").expect(401);
});

test("login sets an HttpOnly session cookie", async () => {
  const res = await request(app)
    .post("/login")
    .send({ email: "[email protected]", password: "correct-horse" })
    .expect(200);

  const cookie = res.headers["set-cookie"][0];
  expect(cookie).toContain("HttpOnly");
  expect(cookie).toContain("SameSite=Lax");
});

test("logout invalidates the session", async () => {
  const agent = request.agent(app);
  await agent.post("/login").send(credentials).expect(200);
  await agent.get("/api/me").expect(200);
  await agent.post("/logout").expect(204);
  await agent.get("/api/me").expect(401);
});

Die Verwendung eines Agents, der Cookies beibehält, ermöglicht es einem einzelnen Test, das Verhalten eines echten Browsers zu simulieren. Überprüfen Sie, dass sich die Session-ID nach dem Login ändert, dass eine abgelaufene Session abgelehnt wird und dass manipulierte Cookies ignoriert werden. Diese Tests finden Regressionen, die bei einem manuellen Durchklicken übersehen würden.

Die Session-ID ist nur so sicher wie das Cookie, das sie transportiert. Diese Attribute machen den Unterschied zwischen einem soliden Design und einer Sicherheitslücke.

  • HttpOnly — das Cookie ist für document.cookie unsichtbar. Dies ist das wichtigste Flag, da es den Diebstahl von Sessions via XSS neutralisiert. Setzen Sie es immer.
  • Secure — der Browser sendet das Cookie nur über HTTPS. Ohne dieses Flag kann ein Angreifer im Netzwerk die ID auslesen. Setzen Sie es in der Production immer und beachten Sie, dass Secure Cookies bei einfachem HTTP verworfen werden, was die lokale Entwicklung beeinflusst.
  • SameSite — steuert das Senden über verschiedene Seiten hinweg. Strict sendet das Cookie niemals cross-site, was am sichersten ist, aber eingehende Links unterbricht, die den Nutzer sonst eingeloggt halten würden. Lax sendet es bei Top-Level-Navigationen, wie etwa beim Klicken auf einen Link, und ist für die meisten Apps der richtige Standard. None sendet es überall und erfordert Secure.
  • Path — beschränkt das Cookie auf ein URL-Präfix. Path=/ ist typisch. Die Beschränkung auf /app reduziert die Angriffsfläche geringfügig, hilft aber selten genug, um ein unerwartetes Verhalten zu rechtfertigen.
  • Domain — steuert, welche Hosts das Cookie erhalten. Wenn man dieses Attribut weglässt, bleibt das Cookie auf dem exakten Host, was die sicherere Wahl ist. Das Setzen einer Parent-Domain teilt die Session über Subdomains hinweg, was den Schadensradius einer kompromittierten Subdomain vergrößert.
  • Max-Age und Expires — legen fest, wann das Cookie verworfen werden soll. Ein Session-Cookie ohne diese Attribute bleibt so lange bestehen, bis der Browser geschlossen wird, was auf Mobilgeräten oft nicht den Erwartungen der Nutzer entspricht. Ein explizites Max-Age ist klarer.
  • __Host--Präfix — wenn ein Cookie __Host-sid genannt wird, erzwingt dies Secure, Path=/ und kein Domain, was verhindert, dass eine Subdomain Ihr Session-Cookie überschreibt.
HTTP/1.1 200 OK
Set-Cookie: __Host-sid=s%3A9f2c...; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=86400

Auswahl eines Session-Stores

Die Entscheidung, wo Sessions gespeichert werden, hat den größten Einfluss darauf, wie Ihre App skaliert.

In-process memory ist in vielen Frameworks der Standard und eignet sich gut für eine App mit einer einzigen Instanz oder für einen Prototyp. Die Daten gehen beim Neustart verloren und können nicht geteilt werden, weshalb dieser Ansatz scheitert, sobald Sie mehr als einen Prozess betreiben.

Redis ist die Standardwahl für die Produktion. Es ist schnell, unterstützt nativ das Ablaufdatum von Keys und wird von jeder Instanz gemeinsam genutzt. Sessions sind kleine Key-Value-Datensätze, was genau der Stärke von Redis entspricht. Ein managed Redis ist kostengünstig und nimmt Ihnen die betriebliche Last ab.

Eine relationale Datenbank funktioniert gut, wenn Sie bereits PostgreSQL oder MySQL einsetzen und möchten, dass Sessions auch bei einem Redis-Ausfall erhalten bleiben. Sie erhalten Persistenz und eine einfache Überprüfung mittels SQL, allerdings auf Kosten einer langsameren Abfrage, sofern die Tabelle nicht klein ist und über die ID indiziert wurde.

Eine dedizierte Session-Tabelle ist in Frameworks wie Django und Rails üblich. Der Store ist eine Tabelle mit einem Session-Key, einem serialisierten Payload und einer Spalte für das Ablaufdatum. Ein Index auf dem Key und ein periodischer Cleanup-Job sind die einzigen erforderlichen Wartungsmaßnahmen.

CREATE TABLE sessions (
  id         text PRIMARY KEY,
  user_id    bigint NOT NULL REFERENCES users (id) ON DELETE CASCADE,
  data       jsonb NOT NULL DEFAULT '{}'::jsonb,
  expires_at timestamptz NOT NULL,
  created_at timestamptz NOT NULL DEFAULT now()
);

CREATE INDEX sessions_expires_idx ON sessions (expires_at);

Speicher skaliert nicht

Es ist wichtig, klar zu benennen, warum In-Memory-Sessions in der Produktion ein Bug sind, da das daraus resultierende Versagen sporadisch auftritt und verwirrend ist.

Stellen Sie sich zwei App-Instanzen hinter einem Load Balancer ohne gemeinsamen Store vor. Ein Benutzer loggt sich ein und landet auf Instanz A, welche die Session im eigenen Speicher ablegt. Die nächste Anfrage wird an Instanz B geroutet, die keinen Eintrag zu dieser ID hat – für den Benutzer sieht es also so aus, als wäre er ausgeloggt. Wenn er sich erneut einloggt, springt er möglicherweise zwischen den beiden Instanzen hin und her. Der Benutzer erlebt zufällige Logouts; die Logs zeigen eine Session, die auf einem Node existiert, auf dem anderen jedoch nicht.

Man kann dies mit Sticky Sessions übertünchen, bei denen der Load Balancer einen Client an eine bestimmte Instanz bindet. Das hilft zwar, ist aber fragil: Ein Deployment, ein Absturz oder ein Autoscaling-Event löscht den Node und damit jede darauf befindliche Session. Sticky Sessions behindern zudem die horizontale Skalierung und erschweren Canary Releases.

Die ehrliche Lösung ist ein gemeinsamer Store. Sobald Sessions in Redis oder einer Datenbank liegen, kann jede Instanz jede Anfrage bedienen, Deployments werden für angemeldete Benutzer unsichtbar und Sie können die Kapazität beliebig erweitern. Nutzen Sie den Speicher nur für die lokale Entwicklung und machen Sie den Store konfigurierbar, damit die Produktion ihn niemals versehentlich verwendet.

Session Fixation und Rotation

Session Fixation ist ein Angriff, bei dem der Angreifer eine Session-ID erwirkt, die das Opfer anschließend verwendet. Der Angreifer wartet dann, bis sich das Opfer authentifiziert, und nutzt die nun privilegierte Session aus. Die klassische Methode ist ein manipulierter Link wie ?sid=attacker-known-id auf einer Seite, die eine vom Client bereitgestellte Session-ID akzeptiert.

Die Verteidigung ist einfach und zwingend erforderlich: Regenerieren Sie die Session-ID jedes Mal, wenn sich die Berechtigungsstufe ändert. Das bedeutet beim Login, nach einem Passwort-Reset, beim Wechsel in eine Admin-Ansicht und nach jeder Step-up-Authentifizierung. Die ID vor dem Login wird verworfen, wodurch die Kopie des Angreifers wertlos wird.

req.session.regenerate((err) => {
  if (err) return next(err);
  // A brand new id now identifies this session.
  req.session.userId = user.id;
  res.json({ ok: true });
});

Die Rotation bietet einen zweiten Vorteil: Sie verhindert die Wiederverwendung von Session-IDs über einen längeren Zeitraum. Eine ID, die über Monate hinweg gültig ist, ist ein weitaus attraktiveres Ziel als eine, die bei jedem Login gewechselt wird. Einige Frameworks führen die Rotation zudem über einen Timer durch, was das Zeitfenster einschränkt, in dem eine geleakte ID nutzbar ist.

Akzeptieren Sie niemals eine Session-ID aus einem Query-String, einem Request-Body oder einem benutzerdefinierten Header. Die einzige Quelle sollte das Cookie sein, das der Server selbst gesetzt hat – und erst nachdem validiert wurde, dass die ID im Store existiert.

Ablauf- und Idle-Timeouts

Sessions sollten nicht ewig bestehen bleiben. Dabei sind zwei Zeitintervalle entscheidend, und gute Systeme setzen beide ein.

Ein Idle-Timeout lässt eine Session nach einer Zeit der Inaktivität ablaufen – bei sensiblen Anwendungen typischerweise nach 15 bis 60 Minuten. Jede Anfrage, die vor Ablauf des Timeouts eingeht, aktualisiert das Ablaufdatum. So bleibt ein aktiver Nutzer angemeldet, während ein vergessenes Laptop den Zugriff verliert. Dies ist die primäre Verteidigung gegen gestohlene Cookies auf einem Gerät.

Eine absolute Laufzeit begrenzt das maximale Alter einer Session unabhängig von der Aktivität, üblicherweise auf 8 bis 24 Stunden (bei Hochrisiko-Apps kürzer). Sie erzwingt eine periodische Re-Authentifizierung und begrenzt die Zeitspanne, in der eine kompromittierte Session missbraucht werden kann, selbst wenn sie aktiv gehalten wird.

Speichern Sie das Ablaufdatum zusammen mit der Session und prüfen Sie es bei jedem Abruf, nicht nur im Cookie. Das Max-Age eines Cookies ist lediglich ein clientseitiger Hinweis, den ein Angreifer ignorieren kann; das serverseitige Ablaufdatum ist die eigentliche Regel. Löschen Sie beim Logout den Datensatz vollständig, anstatt ihn nur als abgelaufen zu markieren, damit der Store nicht mit toten Einträgen überläuft.

const session = await store.get(id);
if (!session || session.expiresAt < Date.now()) {
  await store.delete(id);
  return next(); // anonymous
}

Da Browser Cookies automatisch anhängen, kann eine Seite einer anderen Origin Ihren Browser dazu veranlassen, eine authentifizierte Anfrage ohne Ihr Wissen zu senden. Das ist Cross-Site Request Forgery: Eine bösartige Seite sendet ein Formular an https://bank.example/transfer und Ihr Session-Cookie wird mitgeschickt, wodurch die Anfrage legitim erscheint.

SameSite ist die erste Verteidigungslinie. SameSite=Lax verhindert, dass Cookies bei Cross-Site-POST-Anfragen gesendet werden, was den klassischen Angriff blockiert. Aber SameSite allein ist keine vollständige Strategie: Ältere Browser, bestimmte Subdomain-Konfigurationen und legitime Cross-Site-Flows benötigen weiterhin Schutz auf Anwendungsebene.

Zwei Patterns decken den Rest ab:

  • Synchronizer Token. Der Server speichert ein zufälliges Token in der Session und rendert es in Formulare oder stellt es in einem Response-Header bereit. Unsichere Anfragen müssen das Token zurückgeben, und der Server vergleicht diese in Constant Time. Da die Seite eines Angreifers das Token nicht lesen kann, kann sie keine gültige Anfrage fälschen.
  • Double-Submit Cookie. Der Server setzt einen Zufallswert in einem Non-HttpOnly-Cookie und der Client sendet denselben Wert in einem Header. Der Server prüft, ob die beiden übereinstimmen. Dies ist zustandslos und einfach zu implementieren, hängt jedoch davon ab, dass der Angreifer keine Cookies für Ihre Domain setzen kann – kombinieren Sie es daher mit dem __Host--Präfix.
const sent = req.get("x-csrf-token") ?? "";
const expected = req.session.csrfToken;
const ok =
  sent.length === expected.length &&
  crypto.timingSafeEqual(Buffer.from(sent), Buffer.from(expected));
if (!ok) return res.status(403).json({ error: "csrf_failed" });

Wenden Sie den CSRF-Schutz auf jede zustandsändernde Methode an — POST, PUT, PATCH, DELETE — und befreien Sie GET, HEAD und OPTIONS davon, da diese niemals den Zustand ändern dürfen. Prüfen Sie bei unsicheren Anfragen zudem den Origin-Header als einfaches zusätzliches Signal.

Signierte gegenüber verschlüsselten Cookies

Ein Cookie kann selbst Daten transportieren und nicht nur eine ID, sofern man es absichert. Hierfür gibt es zwei gängige Mechanismen, die unterschiedliche Probleme lösen.

Ein signiertes Cookie fügt einen keyed message authentication code hinzu, sodass der Server Manipulationen erkennen kann. Ein Benutzer kann role=member nicht in role=admin ändern, da die Signatur dann nicht mehr übereinstimmt. Eine Signierung verbirgt jedoch nichts: Die Payload ist base64-kodiert und für jeden, der das Cookie besitzt, lesbar. Signierung bietet Integrität, aber keine Vertraulichkeit.

Ein verschlüsseltes Cookie (oft als „sealed cookie“ bezeichnet) verbirgt die Payload und authentifiziert sie gleichzeitig. Der Server kann so eine kleine Menge an Session-Daten direkt im Cookie speichern, wodurch der Lookup im Store entfällt, während die Daten für den Client undurchsichtig bleiben. Frameworks wie cookie-session und die verschlüsselten Cookies von Rails verfolgen diesen Ansatz.

Der Trade-off ist derselbe wie bei JWTs: Ein eigenständiges Cookie lässt sich schwerer widerrufen und vergrößert bei jedem Aufruf die Request-Größe. Ein sinnvoller Mittelweg ist ein Hybrid-Ansatz: Nutzen Sie eine serverseitige Session für Identität und Berechtigungen und ein kurzlebiges signiertes Cookie für kleine, unkritische Flags. Egal wofür Sie sich entscheiden: Speichern Sie niemals Secrets, Rollen, die Sie nicht erneut prüfen, oder große Objekte in einem Cookie.

Sessions über Server hinweg skalieren

Sobald der Store gemeinsam genutzt wird, bleiben Performance und Betrieb die wichtigsten Themen.

Halten Sie Sessions klein. Eine Session sollte Identifikatoren enthalten, keine vollständigen Objekte. Wenn Sie eine Kopie des User-Dokuments speichern, muss jede Profiländerung jede einzelne Session aktualisieren, und veraltete Daten führen zu schwer nachvollziehbaren Bugs. Speichern Sie userId und laden Sie den User bei Bedarf oder cachen Sie diesen kurzzeitig.

Effizient lesen. Ein Redis GET pro Request dauert nur Mikrosekunden, ist aber bei hohem Volumen dennoch ein Netzwerk-Hop. Viele Stacks cachen die Session für die Lebensdauer eines einzelnen Requests, damit mehrere middlewares nicht jeweils den Store abfragen müssen.

Ausfälle des Stores handhaben. Entscheiden Sie, ob ein Redis-Ausfall alle Benutzer ausloggen sollte oder ob geschützte Routen im Sinne eines “Fail Closed”-Prinzips gesperrt werden sollen. Fail Closed ist sicherer: Behandeln Sie einen nicht erreichbaren Store bei authentifizierten Endpunkten als “keine Session” und geben Sie einen klaren Fehler zurück, anstatt stillschweigend Zugriff zu gewähren.

Aufräumen. Setzen Sie eine TTL für Redis-Keys und einen periodischen DELETE FROM sessions WHERE expires_at < now() für Datenbank-Stores. Ohne Bereinigung wächst der Store unbegrenzt an und die Abfragen werden langsamer.

Vermeiden Sie Sticky Sessions, wenn Sie einen Shared Store haben. Sie erhöhen die betriebliche Komplexität ohne Mehrwert und erschweren Zero-Downtime-Deployments. Wenn Sie wirklich keinen gemeinsamen Store nutzen können, sind Sticky Sessions eine Notlösung, kein Design.

Sessions versus JWTs

Dieser Vergleich taucht ständig auf, und die ehrliche Antwort ist, dass beide Ansätze überlappende Probleme mit unterschiedlichen Trade-offs lösen.

Aspekt Server-side session JWT
Speicherort des Zustands Server-Store Innerhalb des Tokens
Widerruf (Revocation) Sofort möglich Schwierig bis zum Ablauf
Lookup pro Request Erforderlich Nicht notwendig
Sensible Daten im Client Niemals Vermeiden; Token ist lesbar
Skalierung Benötigt Shared Store Per Design zustandslos (stateless)
Browser CSRF-Risiko Ja Nur bei Cookie-Basis
Best Fit First-Party Web-Apps APIs, Services, Mobile

Sessions gewinnen in puncto Kontrolle. Man kann ein einzelnes Gerät sperren, aktive Sessions auflisten, einen globalen Logout erzwingen und Berechtigungen ändern, die bereits beim nächsten Request greifen. JWTs gewinnen durch ihre Zustandslosigkeit: Jeder Service kann ein Token verifizieren, ohne auf einen gemeinsamen Store zugreifen zu müssen. Das ist besonders attraktiv für Microservices und APIs, bei denen Aussteller und Verifizierer unterschiedliche Systeme sind.

Eine gängige und sinnvolle Architektur nutzt beides. Der Browser erhält ein HttpOnly Session-Cookie, das auf eine server-side session verweist. Diese Session kann ein kurzlebiges JWT enthalten, das für Aufrufe interner Services genutzt wird. Für den Nutzer fühlt es sich wie ein normaler Login an, während die internen Aufrufe zustandslos bleiben. Für einen tieferen Einblick in die andere Seite lies den JWT Guide.

Auditierung und Observability

Sessions stellen eine Sicherheitsgrenze dar, und Grenzen müssen beobachtbar sein. Loggen Sie die relevanten Ereignisse, aber niemals Geheimnisse.

Protokollieren Sie erfolgreiche und fehlgeschlagene Logins zusammen mit der User ID, einer groben Quelle wie der IP und dem User Agent sowie einem Zeitstempel. Protokollieren Sie die Erstellung, Rotation und Zerstörung von Sessions. Loggen Sie niemals die Session ID selbst, das Passwort oder das CSRF Token; eine Session ID in einer Log-Datei ist gleichbedeutend mit einem Credential in einer Log-Datei. Wenn Sie eine Korrelation herstellen müssen, loggen Sie stattdessen einen kurzen Hash der ID.

logger.info({
  event: "session.created",
  userId: user.id,
  ip: req.ip,
  userAgent: req.get("user-agent"),
});

Geben Sie Nutzern die Möglichkeit, ihre aktiven Sessions einzusehen und zu widerrufen. Eine „Sessions“-Seite, die Geräte mit einem Sign-out-Button auflistet, macht einen vermuteten Kompromiss zu einem Fix mit einem Klick – und es ist ein Feature, das Nutzer zunehmend erwarten. Alarmieren Sie Administratoren bei „Impossible Travel“, einem sprunghaften Anstieg fehlgeschlagener Logins oder einer hohen Anzahl an Sessions, die von einer einzigen IP erstellt wurden.

Best Practices

  • Generieren Sie Session-IDs mit einem kryptografisch sicheren Zufallsgenerator, niemals mit einem Zähler oder einem vom Benutzer bereitgestellten Wert.
  • Setzen Sie HttpOnly, Secure, SameSite=Lax und ein explizites Max-Age für das Session-Cookie; ziehen Sie das __Host--Präfix in Betracht.
  • Generieren Sie die Session-ID beim Login und nach jeder Änderung der Berechtigungen neu, um Session Fixation zu verhindern.
  • Verwenden Sie in der Produktion einen gemeinsamen Store wie Redis oder eine Datenbank; verlassen Sie sich niemals auf den Prozessspeicher über verschiedene Instanzen hinweg.
  • Erzwingen Sie sowohl ein Idle-Timeout als auch eine absolute Lebensdauer auf dem Server, nicht nur im Cookie.
  • Löschen Sie den Datensatz beim Logout und bereinigen Sie abgelaufene Sessions in regelmäßigen Abständen.
  • Fügen Sie jedem zustandsändernden Route einen CSRF-Schutz hinzu und verifizieren Sie den Origin-Header.
  • Halten Sie Session-Payloads klein; speichern Sie IDs und laden Sie den Rest bei Bedarf.
  • Implementieren Sie ein „Fail Closed“-Verhalten, wenn der Session-Store bei geschützten Routen nicht erreichbar ist.
  • Fordern Sie für sensible Aktionen, wie das Ändern eines Passworts oder das Hinzufügen einer Zahlungsmethode, eine erneute Authentifizierung an.

Häufige Fehler

  • Speichern von Sessions im Prozessspeicher bei gleichzeitigem Betrieb von mehr als einer Instanz.
  • Vergessen von HttpOnly, wodurch die Session-ID für XSS anfällig wird.
  • Verwendung von SameSite=None ohne Secure, sodass Browser das Cookie stillschweigend verwerfen.
  • Wiederverwendung der Session-ID nach dem Login anstatt einer Rotation.
  • Akzeptieren einer Session-ID aus einem Query-Parameter oder dem Request-Body.
  • Speichern von Rollen und Berechtigungen im Cookie und blindes Vertrauen ohne erneute Prüfung.
  • Setzen eines Cookies Max-Age, ohne den entsprechenden serverseitigen Datensatz jemals ablaufen zu lassen.
  • Rückgabe eines 200-Status mit einem Error-Body bei einer nicht authentifizierten Anfrage anstatt eines 401-Status.
  • Verzicht auf CSRF-Schutz mit der Begründung, dass “SameSite das bereits erledigt”.
  • Unkontrolliertes Wachstum des Session-Stores durch fehlende TTL oder fehlende Cleanup-Jobs.

Wie geht es weiter?

Die Session-Authentifizierung vermittelt die Grundlagen, auf denen jedes andere Authentifizierungsschema aufbaut: ein Credential, ein Lookup, ein Ablaufdatum und eine Möglichkeit zum Widerruf. Der natürliche Vergleich dazu ist JWT, bei dem die Claims beim Client mitgeführt werden und der Widerruf die eigentliche Herausforderung darstellt. Wenn Sie Third-Party-Logins oder delegierten Zugriff benötigen, ist OAuth 2.0 der Standard. Um das Cookie selbst zu verstehen – Set-Cookie, SameSite und die Header-Regeln – lesen Sie den HTTP-Guide. Und um die middleware in einen echten Server einzubinden, behandelt der Node.js-Guide die zugrunde liegende Runtime.

In der Praxis

Login, laden, logout, verteidigen

Vier Dateien, die zusammen eine vollständige Session-Schicht bilden.

routes/login.ts
import { Router } from "express";
import { verifyPassword } from "../auth/passwords.js";

const router = Router();

router.post("/login", async (req, res) => {
  const { email, password } = req.body;
  const user = await db.user.findByEmail(email);
  if (!user || !(await verifyPassword(password, user.passwordHash))) {
    return res.status(401).json({ error: "invalid_credentials" });
  }

  // Regenerate to prevent session fixation.
  req.session.regenerate((err) => {
    if (err) return res.status(500).json({ error: "session_error" });
    req.session.userId = user.id;
    req.session.role = user.role;
    res.json({ id: user.id, email: user.email });
  });
});

export default router;

Wo die Anmeldedaten liegen

Eine Session-ID in einem HttpOnly-Cookie kann nicht von JavaScript gelesen werden. Ein JWT im localStorage ist für jedes Skript auf der Seite lesbar, sodass ein einzelnes XSS zur vollständigen Kontoübernahme führen kann.

Bevorzugt
res.cookie("sid", sessionId, {
  httpOnly: true,
  secure: true,
  sameSite: "lax",
  path: "/",
  maxAge: 86_400_000,
});
Vermeiden
const res = await fetch("/login", { method: "POST", body });
const { token } = await res.json();

// Any injected script can read this.
localStorage.setItem("token", token);

Cross-Site-Verhalten

SameSite=Lax sendet das Cookie bei Top-Level-Navigationen und Same-Site-Anfragen, was für die meisten Apps richtig ist. 'None' ist nur erforderlich, wenn ein echter Cross-Site-Flow benötigt wird, und muss dann mit Secure und CSRF-Schutz kombiniert werden.

Bevorzugt
res.cookie("sid", id, {
  httpOnly: true,
  secure: true,
  sameSite: "lax",
});
Vermeiden
res.cookie("sid", id, {
  httpOnly: true,
  sameSite: "none",
  // missing Secure: the browser will drop it
});

Abwägungen

Lohnt sich serverseitiger Session-Status?

Sessions tauschen ein wenig Infrastruktur gegen sofortige Kontrolle ein. Für First-Party-Web-Apps ist das meist die richtige Entscheidung.

Strengths

  • Sofortiger Widerruf

    Das Löschen eines Session-Datensatzes beendet den Zugriff sofort. Es gibt kein Zeitfenster, in dem ein gestohlener Token weiter funktioniert, bis er abläuft.

  • Keine sensiblen Daten im Client

    Das Cookie enthält eine zufällige ID, keine Claims oder Rollen. Ein Benutzer kann seine eigenen Berechtigungen nicht manipulieren, da der Server die Wahrheit besitzt.

  • Natürlich für Browser

    Cookies, Redirects und serverseitig gerenderte Seiten funktionieren ohne Token-Refresh-Zyklus. Ein Login ist eine einzige Anfrage.

Trade-offs

  • Ein gemeinsamer Store ist nötig

    Der Speicher im Prozess funktioniert auf einem Server, bricht aber zusammen, sobald zwei laufen. Redis oder eine Datenbank ist in der Produktion praktisch obligatorisch.

  • CSRF wird zum Problem

    Da der Browser das Cookie automatisch sendet, müssen Sie CSRF-Token und korrekte SameSite-Einstellungen hinzufügen. Token-Authentifizierung in einem Header hat dieses Problem nicht.

  • Jede Anfrage greift auf den Store zu

    Ein Session-Lookup pro Anfrage erhöht Latenz und Last. Caching oder kurzlebige signierte Session-Cookies können dies reduzieren, allerdings auf Kosten der Aktualität.

Häufig gestellte Fragen

Häufig gestellte Fragen

Keep learning

Related topics from the roadmap.

$ Lernen Sie jetzt

Bereit, Session Auth zu lernen?

Unser interaktives Tutorial führt Sie Schritt für Schritt durch Session Auth — mit Quizzen und echtem Code, den Sie im Browser ausführen können.