---
title: "JWT nicht in LocalStorage speichern: Cookie Auth fuer SPAs 2026"
description: "Ein praktischer 2026 Guide, um JWT-Authentifizierung aus LocalStorage in HTTP-only Cookies mit CSRF-Schutz zu verschieben: fuer SPA, SSR, Uploads, WebSocket, Gateway und Mobile."
author: Aleksei Aleinikov
date: 2026-05-26
updated: 2026-06-27
lang: de
tags: [jwt, authentication, cookies, web-security, spa, owasp]
canonical: https://www.alekseialeinikov.com/de/blog/topics/security/jwt-nicht-in-localstorage-cookie-auth-fuer-spas-2026
source: alekseialeinikov.com
---

# JWT nicht in LocalStorage speichern: Cookie Auth fuer SPAs 2026

Wenn ein **JWT** in `localStorage` liegt, kann jedes injizierte Script es lesen und wegschicken. Das ist der unangenehme Teil vieler SPA-Auth-Setups: Der Token ist bequem fuer JavaScript, aber genau diese Bequemlichkeit macht ihn nach einer XSS-Luecke leicht stehlbar.

Der bessere Default in 2026 ist, sensitive Tokens aus JavaScript herauszuhalten und den Transport dem Browser ueber HTTP-only Cookies zu ueberlassen.

![Dieses Gesicht, wenn man merkt, dass die App Tokens jahrelang fuer injiziertes JavaScript sichtbar gemacht hat.](https://www.alekseialeinikov.com/blog/1_ldHCmzpjsA_NbFP8XHPwbg.webp)

Wenn derselbe Token in einem HTTP-only Cookie liegt, passieren drei nuetzliche Dinge:

- JavaScript kann ihn nicht lesen, daher verliert injizierter Code den einfachsten Weg zum Token-Diebstahl.
- Der Browser sendet ihn automatisch mit passenden Requests.
- Cookie-Flags wie `Secure`, `HttpOnly` und `SameSite` schuetzen zusaetzlich gegen Sniffing und einige CSRF-Muster.

Das entfernt nicht jedes Authentifizierungsrisiko. Fuer unsafe Methods brauchst du weiterhin CSRF-Schutz, kurze Access-Token-Laufzeiten, Refresh-Rotation und sauberes Logout-Verhalten. Aber es ist eine staerkere Basis als Bearer Tokens im Browser Storage.

## Server Side: Tokens mit Node.js ausstellen

**Hier ist ein minimales Express-Setup.**

```bash
npm install express cookie-parser jsonwebtoken
```

```js
import express from 'express';
import cookieParser from 'cookie-parser';

const app = express();

app.use(express.json());
app.use(cookieParser());
```

Fuer Produktion definiere ich Cookie-Optionen meistens zentral:

```js
const isProduction = process.env.NODE_ENV === 'production';

const authCookie = {
  httpOnly: true,
  secure: isProduction,
  sameSite: 'strict',
  path: '/',
};
```

`httpOnly` haelt den Token von JavaScript fern. `secure` sorgt dafuer, dass das Cookie in Produktion nur ueber HTTPS gesendet wird. `sameSite: 'strict'` ist ein konservativer Default fuer Same-Site-Apps.

Wenn Frontend und API auf unterschiedlichen Sites liegen, brauchst du eventuell `sameSite: 'none'` plus `secure: true`, und CORS muss sauber konfiguriert werden.

## Login Endpoint

**Nach erfolgreicher Credential-Pruefung erzeugst du einen Access Token und einen Refresh Token.** Der Access Token sollte kurzlebig sein. Der Refresh Token darf laenger leben, sollte aber trotzdem rotiert und widerrufbar sein.

```js
import jwt from 'jsonwebtoken';

app.post('/api/token', async (req, res) => {
  const { username, password } = req.body;

  // 1. Credentials pruefen.
  // 2. User laden.
  // 3. Inaktive oder gesperrte Accounts ablehnen.

  const accessToken = jwt.sign(
    { sub: username },
    process.env.ACCESS_SECRET,
    { expiresIn: '5m' },
  );

  const refreshToken = jwt.sign(
    { sub: username },
    process.env.REFRESH_SECRET,
    { expiresIn: '1d' },
  );

  res
    .cookie('access_token', accessToken, {
      ...authCookie,
      maxAge: 5 * 60 * 1000,
    })
    .cookie('refresh_token', refreshToken, {
      ...authCookie,
      maxAge: 24 * 60 * 60 * 1000,
    })
    .json({ user: username, status: 'active' });
});
```

Der Client bekommt nur harmlose User-Informationen im JSON. Die sensitiven Tokens bleiben in Cookies, die Frontend-JavaScript nicht lesen kann.

Es ist weiterhin okay, oeffentlichen UI-State wie `username`, Theme oder Feature Flags in `localStorage` zu speichern. Die wichtige Regel lautet: keine Bearer Tokens dort speichern.

## Refresh Endpoint

**Der Refresh Endpoint liest den Refresh Token aus den Cookies.** Wenn der Token gueltig ist, stellt er ein neues Paar aus.

```js
app.post('/api/token/refresh', (req, res) => {
  const refreshToken = req.cookies.refresh_token;

  if (!refreshToken) {
    return res.sendStatus(401);
  }

  jwt.verify(refreshToken, process.env.REFRESH_SECRET, (err, decoded) => {
    if (err) {
      return res.sendStatus(403);
    }

    const newAccess = jwt.sign(
      { sub: decoded.sub },
      process.env.ACCESS_SECRET,
      { expiresIn: '5m' },
    );

    const newRefresh = jwt.sign(
      { sub: decoded.sub },
      process.env.REFRESH_SECRET,
      { expiresIn: '1d' },
    );

    res
      .cookie('access_token', newAccess, {
        ...authCookie,
        maxAge: 5 * 60 * 1000,
      })
      .cookie('refresh_token', newRefresh, {
        ...authCookie,
        maxAge: 24 * 60 * 60 * 1000,
      })
      .json({ ok: true });
  });
});
```

In einem echten Produktionssystem solltest du auch eine Refresh-Token-ID serverseitig speichern, damit alte Refresh Tokens widerrufen, Reuse erkannt und Sessions nach Logout invalidiert werden koennen.

## CSRF-Schutz bleibt notwendig

**HTTP-only Cookies reduzieren XSS-basierten Token-Diebstahl, entfernen aber kein CSRF-Risiko.** Der Browser sendet Cookies automatisch, auch bei manchen unerwuenschten Cross-Site-Requests.

Fuer unsafe Methods wie `POST`, `PUT`, `PATCH` und `DELETE` brauchst du einen CSRF Token.

Ein gaengiges Muster:

1. Der Server setzt ein nicht-HTTP-only CSRF Cookie oder liefert einen CSRF Token ueber einen Endpoint.
2. Der Client liest diesen CSRF-Wert.
3. Der Client sendet ihn in einem `X-CSRF-Token` Header zurueck.
4. Der Server prueft, ob der Header zum erwarteten Wert passt.

```js
app.get('/api/csrf-token', (req, res) => {
  const token = crypto.randomUUID();

  res
    .cookie('csrf_token', token, {
      secure: isProduction,
      sameSite: 'strict',
      path: '/',
    })
    .set('X-CSRF-Token', token)
    .json({ ok: true });
});
```

Du kannst das manuell umsetzen, Framework-Unterstuetzung nutzen oder ein gepflegtes CSRF-Package verwenden. Die Kernidee bleibt gleich: Unsafe Requests muessen beweisen, dass sie von deinem Frontend-Code kommen und nicht von einem zufaelligen Form Post einer anderen Site.

## React Client: Login mit Cookies und CSRF

**Der Client behandelt JWTs nicht mehr manuell.** Er bittet den Browser, Cookies mitzuschicken, indem `credentials: 'include'` gesetzt wird.

```js
async function getCSRF() {
  const response = await fetch('/api/csrf-token', {
    credentials: 'include',
  });

  return response.headers.get('x-csrf-token');
}
```

Diesen Token nutzt du dann beim Login:

```js
const loginUser = async (event) => {
  event.preventDefault();

  const csrf = await getCSRF();

  const response = await fetch('/api/token', {
    method: 'POST',
    credentials: 'include',
    headers: {
      'Content-Type': 'application/json',
      'X-CSRF-Token': csrf,
    },
    body: JSON.stringify({
      username: event.target.username.value,
      password: event.target.password.value,
    }),
  });

  if (response.ok) {
    const data = await response.json();
    localStorage.setItem('username', data.user);
    return;
  }

  alert('Wrong login or password');
};
```

Die Trennung ist klar:

- Cookies halten sensitive Tokens,
- `localStorage` haelt nur harmlose Anzeigeinformationen,
- der CSRF Token wird fuer unsafe Requests als Header gesendet.

## Access Token automatisch erneuern

**Wenn Access Tokens nach fuenf Minuten ablaufen, erneuere sie kurz vor Ablauf.**

```js
useEffect(() => {
  const timer = setInterval(async () => {
    const csrf = await getCSRF();

    await fetch('/api/token/refresh', {
      method: 'POST',
      credentials: 'include',
      headers: {
        'Content-Type': 'application/json',
        'X-CSRF-Token': csrf,
      },
    });
  }, 4.5 * 60 * 1000);

  return () => clearInterval(timer);
}, []);
```

Nutzer koennen weiterarbeiten, ohne staendig "session expired" Popups zu sehen, waehrend der Access Token kurzlebig bleibt.

Du kannst auch reaktiv refreshen: Wenn ein API Call `401` zurueckgibt, rufe einmal `/api/token/refresh` auf und wiederhole danach den urspruenglichen Request.

## Fuenf praktische Szenarien

**Cookie-basierte Auth veraendert einige typische Workflows.**

## 1. Grosse Dateien hochladen

**Weil Cookies automatisch mitgesendet werden, bleibt Upload-Code sauber.**

```js
const uploadFile = async (file) => {
  const data = new FormData();
  data.append('file', file);

  await fetch('/api/upload', {
    method: 'POST',
    body: data,
    credentials: 'include',
  });
};
```

Trotzdem brauchst du CSRF-Schutz fuer Uploads, wenn sie Serverzustand veraendern.

## 2. Server-Side Rendering mit Next.js

**Waehrend SSR ist der Access Token in den Request Cookies verfuegbar.**

```js
export async function getServerSideProps({ req }) {
  const token = req.cookies.access_token;

  // Daten im Namen des Nutzers laden.

  return {
    props: {},
  };
}
```

So kannst du private Daten vorladen und fertiges HTML zurueckgeben, ohne den Token fuer Browser-JavaScript sichtbar zu machen.

## 3. Realtime Chat ueber WebSocket

**Bei Same-Site-WebSocket-Verbindungen senden Browser normalerweise Cookies waehrend des WebSocket-Handshakes mit.**

Auf dem Server solltest du das Cookie validieren und den `Origin` Header pruefen, bevor die Verbindung akzeptiert wird.

Wenn deine Infrastruktur Cookies entfernt oder du Cross-Site-WebSocket-Auth brauchst, packe keine langlebigen JWTs in die URL. URLs koennen in Logs landen. Nutze lieber ein kurzlebiges, einmaliges Connection Ticket.

## 4. API Gateway vor Microservices

**Ein API Gateway kann das Cookie lesen, den Token pruefen und vertrauenswuerdige Identitaetsinformationen an interne Services weitergeben.**

Eine andere Option ist, den Token nach der Validierung in einen internen `Authorization` Header zu kopieren.

Die inneren Services bleiben unabhaengig von Browser-Details. Sie vertrauen entweder dem Gateway oder pruefen einen service-seitigen Token.

## 5. Mobile Builds mit React Native

**React Native ist anders.** Mobile Apps haben keine browser-erzwungenen HTTP-only Cookies im gleichen Sinn.

Nutze stattdessen den sicheren Speicher der Plattform:

- Keychain auf iOS,
- Keystore auf Android,
- einen gepflegten Secure-Storage-Wrapper.

Das ist das naechste mobile Aequivalent: Refresh Credentials verschluesselt und von anderen Apps isoliert speichern.

## Takeaways

HTTP-only Cookies sind kein magischer Schild, aber sie sind ein besserer Default fuer SPA-Authentifizierung als Bearer Tokens in `localStorage`.

Die praktische Checkliste:

- JWTs nicht in `localStorage` speichern,
- HTTP-only Cookies fuer Access und Refresh Tokens nutzen,
- `Secure`, `HttpOnly` und `SameSite` bewusst setzen,
- unsafe Methods mit CSRF Tokens schuetzen,
- Access Tokens kurzlebig halten,
- Refresh Tokens rotieren,
- Sessions serverseitig widerrufen, wenn Nutzer sich ausloggen,
- Origins fuer WebSocket-Flows validieren.

Der Wechsel ist groesstenteils Wiring. Der Gewinn ist deutlich: weniger Wege fuer injiziertes JavaScript, eine Nutzeridentitaet zu stehlen, saubererer Client-Code und ein Security-Modell, das besser dazu passt, wie Browser wirklich funktionieren.
