JWT Authentication: Security Best Practices in 2026

Secure your APIs with JWT best practices: RS256 vs HS256, HttpOnly cookies, token rotation, refresh patterns, and common vulnerabilities to avoid in 2026.

E
ECOSIRE Research and Development Team
|19. März 202610 Min. Lesezeit2.2k Wörter|

JWT-Authentifizierung: Best Practices für die Sicherheit im Jahr 2026

JSON-Web-Tokens gibt es überall – aber die meisten Implementierungen weisen mindestens eine kritische Sicherheitslücke auf. Die Angriffsfläche ist größer, als es aussieht: Algorithmus-Verwirrungsangriffe, Token-Diebstahl über XSS, fehlende Ablaufvalidierung und unsachgemäße Geheimnisverwaltung gehören zu den häufigsten Schwachstellen in Produktionssystemen. Um JWT richtig zu machen, geht es nicht darum, eine Bibliothek aufzurufen und weiterzumachen; Es erfordert bewusste Entscheidungen auf jeder Ebene.

Dieser Leitfaden deckt den gesamten JWT-Sicherheitslebenszyklus ab – von der Auswahl des Signaturalgorithmus und der Tokenstruktur bis hin zu Speicherung, Rotation, Widerruf und realen NestJS-Implementierungsmustern – damit Sie eine Authentifizierung erstellen können, die einem Angriff tatsächlich standhält.

Wichtige Erkenntnisse

  • Für verteilte Systeme immer RS256 (asymmetrisch) verwenden; HS256 nur, wenn der API-Server sowohl Aussteller als auch Verifizierer ist – Speichern Sie Token in HttpOnly-, Secure- und SameSite=Lax-Cookies – niemals in localStorage oder sessionStorage – Validieren Sie immer die Ansprüche exp, iss, aud und alg – vertrauen Sie niemals dem vorzeichenlosen none-Algorithmus – Aktualisierungs-Token-Rotation implementieren: Bei jeder Aktualisierung wird ein neues Paar ausgegeben und das alte Aktualisierungs-Token ungültig gemacht
  • Zugriffstoken-TTL kurz halten (15 Minuten); Verwenden Sie undurchsichtige Aktualisierungstoken, die in der Datenbank gespeichert sind
  • Speichern Sie niemals vertrauliche Daten (Passwörter, SSNs, Zahlungsinformationen) in JWT-Nutzdaten – die Nutzdaten sind Base64 und nicht verschlüsselt – Implementieren Sie den Token-Widerruf über eine Redis-Denylist oder einen Versionszähler in der Datenbank
  • Protokollieren Sie alle Token-Ausgabe- und Aktualisierungsereignisse für Sicherheitsprüfpfade

JWT-Struktur und Ansprüche

Ein JWT besteht aus drei Teilen: Header, Payload und Signatur, getrennt durch Punkte und base64-URL-codiert.

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiJ1c2VyXzEyMyIsImVtYWlsIjoidXNlckBleGFtcGxlLmNvbSIsInJvbGUiOiJhZG1pbiIsImlhdCI6MTc0MjM0NTYwMCwiZXhwIjoxNzQyMzQ2NTAwLCJpc3MiOiJodHRwczovL2FwaS5leGFtcGxlLmNvbSIsImF1ZCI6Imh0dHBzOi8vYXBwLmV4YW1wbGUuY29tIn0.
[signature]

Entschlüsselte Nutzlast:

{
  "sub": "user_123",
  "email": "[email protected]",
  "role": "admin",
  "iat": 1742345600,
  "exp": 1742346500,
  "iss": "https://api.example.com",
  "aud": "https://app.example.com"
}

Erforderliche Angaben zur Produktion:

  • sub – eindeutige Benutzerkennung (niemals E-Mail allein – E-Mails ändern sich)
  • exp – Ablaufzeitstempel (immer erforderlich)
  • iat – Zeitstempel des Ausgabedatums (Uhrzeitabweichung erkennen)
  • iss – Emittenten-URL (mit Ihrem erwarteten Emittenten validieren) – aud – Zielgruppe (validieren, um die Wiederverwendung von Tokens über mehrere Dienste hinweg zu verhindern) – jti – JWT-ID (eindeutig pro Token, ermöglicht exakten Widerruf)

RS256 vs. HS256: Welcher Algorithmus verwendet werden soll

Dies ist die einflussreichste Sicherheitsentscheidung in der JWT-Konfiguration.

HS256 (HMAC-SHA256) – Symmetrisch

// Both signing and verifying require the same secret
const token = jwt.sign(payload, process.env.JWT_SECRET, { algorithm: 'HS256' });
const verified = jwt.verify(token, process.env.JWT_SECRET);

Nur verwenden, wenn: Der Dienst, der Token signiert, ist derselbe Dienst, der sie überprüft. HS256 eignet sich gut für monolithische APIs, ist jedoch in Microservices gefährlich – jeder Dienst, der Token verifizieren kann, kann diese auch erstellen.

RS256 (RSA-SHA256) – Asymmetrisch

// Sign with private key (only the auth server holds this)
const privateKey = fs.readFileSync('/secrets/jwt-private.pem');
const token = jwt.sign(payload, privateKey, { algorithm: 'RS256', keyid: 'key-2026-01' });

// Verify with public key (any service can do this safely)
const publicKey = fs.readFileSync('/secrets/jwt-public.pem');
const verified = jwt.verify(token, publicKey, {
  algorithms: ['RS256'], // NEVER omit this — prevents algorithm confusion
  issuer: 'https://auth.example.com',
  audience: 'https://api.example.com',
});

Generieren Sie ein Produktions-RSA-Schlüsselpaar:

# Generate 4096-bit RSA private key
openssl genrsa -out jwt-private.pem 4096

# Extract public key
openssl rsa -in jwt-private.pem -pubout -out jwt-public.pem

Schlüsselrotation: Verwenden Sie keyid (kid) im Header. Veröffentlichen Sie aktuelle öffentliche Schlüssel unter /.well-known/jwks.json. Dienste zwischenspeichern die JWKS und rufen sie auf unbekanntem kid ab.


Das ist nicht verhandelbar. Durch das Speichern von Token in localStorage oder sessionStorage sind sie für jedes auf der Seite ausgeführte JavaScript zugänglich – einschließlich injizierter Skripts von XSS-Angriffen.

// NestJS: set HttpOnly cookie after authentication
@Post('login')
async login(@Body() dto: LoginDto, @Res({ passthrough: true }) res: Response) {
  const { accessToken, refreshToken } = await this.authService.login(dto);

  const cookieBase = {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax' as const,
    path: '/',
    domain: process.env.COOKIE_DOMAIN, // '.example.com' for subdomain sharing
  };

  res.cookie('access_token', accessToken, {
    ...cookieBase,
    maxAge: 15 * 60 * 1000, // 15 minutes
  });

  res.cookie('refresh_token', refreshToken, {
    ...cookieBase,
    maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days
    path: '/auth/refresh', // Scope refresh token to only the refresh endpoint
  });

  return { message: 'Login successful' };
}
// Extract token from cookie in JWT strategy
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
  constructor(private configService: ConfigService) {
    super({
      jwtFromRequest: ExtractJwt.fromExtractors([
        (req) => req?.cookies?.access_token, // Cookie first
        ExtractJwt.fromAuthHeaderAsBearerToken(), // Bearer fallback for API clients
      ]),
      secretOrKey: configService.get('JWT_PUBLIC_KEY'),
      algorithms: ['RS256'],
      issuer: configService.get('JWT_ISSUER'),
      audience: configService.get('JWT_AUDIENCE'),
    });
  }

  async validate(payload: JwtPayload): Promise<AuthenticatedUser> {
    // Always check token version against database
    const user = await this.usersService.findById(payload.sub);
    if (!user || user.tokenVersion !== payload.tokenVersion) {
      throw new UnauthorizedException('Token invalidated');
    }
    return { id: payload.sub, email: payload.email, role: payload.role };
  }
}

Token-Rotation aktualisieren

Stellen Sie niemals ein langlebiges Zugriffstoken aus. Halten Sie stattdessen die Zugriffstokens kurzlebig und wechseln Sie die Aktualisierungstokens bei jeder Verwendung.

// auth.service.ts
@Injectable()
export class AuthService {
  async refreshTokens(refreshToken: string, ipAddress: string) {
    // 1. Look up the refresh token in the database
    const storedToken = await this.db.query.refreshTokens.findFirst({
      where: and(
        eq(refreshTokens.token, this.hashToken(refreshToken)),
        eq(refreshTokens.revoked, false),
        gt(refreshTokens.expiresAt, new Date())
      ),
      with: { user: true },
    });

    if (!storedToken) {
      // Possible reuse attack — revoke all tokens for this user
      if (storedToken?.userId) {
        await this.revokeAllUserTokens(storedToken.userId);
        await this.alertService.send({
          message: `Refresh token reuse detected for user ${storedToken.userId}`,
          severity: 'high',
        });
      }
      throw new UnauthorizedException('Invalid refresh token');
    }

    // 2. Revoke the used refresh token (rotation)
    await this.db
      .update(refreshTokens)
      .set({ revoked: true, revokedAt: new Date(), revokedByIp: ipAddress })
      .where(eq(refreshTokens.id, storedToken.id));

    // 3. Issue new token pair
    const newAccessToken = this.issueAccessToken(storedToken.user);
    const newRefreshToken = await this.issueRefreshToken(
      storedToken.user.id,
      ipAddress,
      storedToken.family // Track token families for reuse detection
    );

    return { accessToken: newAccessToken, refreshToken: newRefreshToken };
  }

  private hashToken(token: string): string {
    return crypto.createHash('sha256').update(token).digest('hex');
  }
}

Das Datenbankschema für Aktualisierungstoken:

CREATE TABLE refresh_tokens (
  id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id     UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  token       VARCHAR(64) NOT NULL UNIQUE, -- SHA256 hash of the actual token
  family      UUID NOT NULL,               -- Token family for reuse detection
  expires_at  TIMESTAMPTZ NOT NULL,
  revoked     BOOLEAN DEFAULT false,
  revoked_at  TIMESTAMPTZ,
  revoked_by_ip INET,
  created_at  TIMESTAMPTZ DEFAULT now(),
  created_by_ip INET
);

CREATE INDEX idx_refresh_tokens_user_id ON refresh_tokens(user_id);
CREATE INDEX idx_refresh_tokens_token   ON refresh_tokens(token);

Token-Widerrufsstrategien

JWTs sind von Natur aus zustandslos – sobald sie ausgestellt wurden, können Sie sie ohne zusätzliche Infrastruktur nicht „zurücknehmen“. Hier sind drei Ansätze, sortiert nach Kompromiss:

1. Kurzes TTL (15 Minuten)

Die einfachste Widerrufsstrategie: Zugriffstoken verfallen so schnell, dass ein Widerruf nur selten erforderlich ist. Koppeln Sie mit der sofortigen Sperrung des Aktualisierungstokens in der Datenbank.

2. Token-Versionszähler

Speichern Sie einen tokenVersion in der Benutzertabelle. Erhöhen Sie den Wert, um alle vorhandenen Token ungültig zu machen:

// Increment version to logout all sessions
await this.db
  .update(users)
  .set({ tokenVersion: sql`token_version + 1` })
  .where(eq(users.id, userId));

// In JWT payload
const payload = {
  sub: user.id,
  tokenVersion: user.tokenVersion, // embedded at sign time
};

// In JwtStrategy.validate()
if (user.tokenVersion !== payload.tokenVersion) {
  throw new UnauthorizedException();
}

Erfordert eine Datenbanksuche pro Anfrage – für die meisten Anwendungen akzeptabel.

3. Redis-Denylist

Für sofortigen Widerruf ohne Datenbanksuche:

// Revoke specific token by JTI
async revokeToken(jti: string, expiresIn: number): Promise<void> {
  const key = `token:revoked:${jti}`;
  await this.redis.set(key, '1', 'EX', expiresIn);
}

// Check in JWT strategy before accepting
async validate(payload: JwtPayload): Promise<AuthenticatedUser> {
  const revoked = await this.redis.get(`token:revoked:${payload.jti}`);
  if (revoked) {
    throw new UnauthorizedException('Token revoked');
  }
  return this.buildUser(payload);
}

Kompromiss: Eine Redis-Suche pro Anfrage, aber Redis ist O(1) und unter einer Millisekunde. Akzeptabel für Endpunkte mit hoher Sicherheit.


Häufige Schwachstellen und Abhilfemaßnahmen

Algorithmus-Verwirrungsangriff

Der alg: "none"-Angriff: Ein Angreifer entfernt die Signatur, setzt alg auf none und übermittelt dann eine manipulierte Nutzlast. Bibliotheken, die nicht signierte Token akzeptieren, akzeptieren jede Nutzlast.

// WRONG — never do this
jwt.verify(token, secret); // Accepts alg:none if library allows it

// CORRECT — always specify algorithms explicitly
jwt.verify(token, publicKey, {
  algorithms: ['RS256'], // Whitelist only what you use
});

JWT-Header-Injektion (jwk/jku)

Angreifer erstellen ein JWT mit einem jku- (JWKS-URL) oder jwk-Header (Inline-Schlüssel), der auf ihren eigenen Schlüsselserver verweist, und signieren dann mit ihrem eigenen privaten Schlüssel. Ein angreifbarer Verifizierer ruft die Schlüssel des Angreifers ab und akzeptiert das Token.

// WRONG — never fetch keys from the token header
const jwksUri = decodedHeader.jku; // Attacker-controlled!

// CORRECT — always use a pinned, config-driven JWKS URI
const jwksClient = createRemoteJWKSet(new URL(configService.get('JWKS_URI')));

Schwache Geheimnisse für HS256

Ein 32-stelliges ASCII-Geheimnis für HS256 hat etwa 190 Bit Entropie – unzureichend. Verwenden Sie mindestens 256 Bit aus einer kryptografisch sicheren Zufallsquelle:

# Generate a strong HS256 secret
node -e "console.log(require('crypto').randomBytes(64).toString('base64url'))"

Konfiguration des NestJS-JWT-Moduls

// auth.module.ts
import { JwtModule } from '@nestjs/jwt';

@Module({
  imports: [
    JwtModule.registerAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        privateKey: config.get('JWT_PRIVATE_KEY'),
        publicKey: config.get('JWT_PUBLIC_KEY'),
        signOptions: {
          algorithm: 'RS256',
          expiresIn: '15m',
          issuer: config.get('JWT_ISSUER'),
          audience: config.get('JWT_AUDIENCE'),
        },
        verifyOptions: {
          algorithms: ['RS256'],
          issuer: config.get('JWT_ISSUER'),
          audience: config.get('JWT_AUDIENCE'),
        },
      }),
    }),
  ],
})
export class AuthModule {}

JWKS-Endpunkt für die Verteilung öffentlicher Schlüssel

// jwks.controller.ts
@Controller('.well-known')
export class JwksController {
  @Get('jwks.json')
  @Public()
  async getJwks() {
    const publicKeyPem = this.configService.get('JWT_PUBLIC_KEY');
    // Convert PEM to JWK format using the 'jose' library
    const publicKey = await importSPKI(publicKeyPem, 'RS256');
    const jwk = await exportJWK(publicKey);

    return {
      keys: [
        {
          ...jwk,
          use: 'sig',
          alg: 'RS256',
          kid: this.configService.get('JWT_KEY_ID'), // e.g., 'key-2026-01'
        },
      ],
    };
  }
}

Cachen Sie diesen Endpunkt aggressiv – öffentliche Schlüssel ändern sich selten. Legen Sie Cache-Control: public, max-age=3600 fest.


Sicherheitscheckliste

Überprüfen Sie Folgendes, bevor Sie die JWT-Authentifizierung in der Produktion bereitstellen:

– [ ] Algorithmus wird sowohl in Signierungs- als auch in Verifizierungsaufrufen explizit auf RS256 oder HS256 gesetzt

  • exp, iss, aud bei jeder Anfrage validiert
  • Zugriffstoken-TTL ≤ 15 Minuten
  • Tokens, die in HttpOnly-, Secure- und SameSite=Lax-Cookies gespeichert sind
  • Aktualisierungstokenrotation implementiert – altes Token wird bei jeder Verwendung widerrufen
  • Aktualisierungstoken als SHA256-Hash in der Datenbank gespeichert (kein Klartext) – [ ] jti-Anspruch für gezielte Widerrufsfähigkeit hinzugefügt – [ ] Redis-Sperrliste auf Endpunkte mit hoher Sicherheit überprüft
  • JWKS-Endpunkt für verteilte Dienste veröffentlicht
  • Private Schlüssel im Secrets Manager gespeichert (AWS Secrets Manager, Vault)
  • Schlüsselrotationsverfahren dokumentiert und getestet
  • Alle Token-Ausgabe- und Ungültigkeitsereignisse werden protokolliert

Häufig gestellte Fragen

Ist es sicher, ein JWT zu dekodieren, ohne die Signatur zu überprüfen?

Das Dekodieren ohne Verifizierung ist zum Lesen der Nutzlast sicher, aber vertrauen Sie niemals dem Inhalt zur Autorisierung. Überprüfen Sie immer die Unterschrift, bevor Sie auf Ansprüche reagieren. Die Funktion jwt.decode() in den meisten Bibliotheken überspringt die Überprüfung – verwenden Sie sie nur zur Diagnose oder zum Lesen von kid aus dem Header, bevor Sie den richtigen Schlüssel für die Überprüfung auswählen.

Soll ich Cookies oder Autorisierungsheader für Browser-Apps verwenden?

HttpOnly-Cookies für Browseranwendungen, Autorisierungsheader für native mobile Apps und Server-zu-Server-API-Aufrufe. Cookies sind immun gegen XSS-Exfiltration (JavaScript kann keine HttpOnly-Cookies lesen). Mobile Apps können Cookies nicht effektiv nutzen und nutzen Bearer-Tokens, die im sicheren Keystore des Geräts gespeichert sind.

Wie gehe ich mit dem Token-Ablauf im Frontend um?

Fangen Sie 401-Antworten ab und versuchen Sie eine stille Aktualisierung, bevor Sie die ursprüngliche Anfrage erneut versuchen. Verwenden Sie in React einen Axios- oder Fetch-Interceptor. Wenn die Aktualisierung ebenfalls fehlschlägt (abgelaufen oder widerrufen), leiten Sie zur Anmeldung weiter. Halten Sie ein einzelnes Aktualisierungsversprechen während des Flugs ein, um parallele Aktualisierungsstürme zu verhindern.

Was ist der Unterschied zwischen Zugriffstokens und Aktualisierungstokens?

Zugriffstoken sind kurzlebig (15 Minuten), zustandslos und werden bei jeder API-Anfrage mit einem öffentlichen Schlüssel oder einem gemeinsamen Geheimnis überprüft. Aktualisierungstoken sind langlebig (7–30 Tage), undurchsichtig (zufällige Zeichenfolgen, keine JWTs) und werden serverseitig in der Datenbank gespeichert. Der Aktualisierungs-Token-Endpunkt ist der einzige Ort, an dem Aktualisierungs-Tokens verwendet werden – mit einem schmalen Cookie-Pfad.

Kann ich Benutzerrollen in der JWT-Nutzlast speichern?

Ja, aber beachten Sie, dass im Token codierte Rollen zwischengespeichert werden, bis das Token abläuft. Wenn Sie einem Benutzer die Administratorrolle entziehen, behält er sie, bis sein aktuelles Zugriffstoken abläuft (bis zu 15 Minuten). Fügen Sie bei Rollenänderungen mit hoher Sicherheit den Benutzer außerdem zu einer Redis-Denylist hinzu oder erhöhen Sie seine Tokenversion, um eine sofortige erneute Authentifizierung zu erzwingen.

Wie setze ich „An mich erinnern“ mit JWTs um?

Geben Sie ein Aktualisierungstoken mit längerer Lebensdauer (90 Tage) aus, wenn der Benutzer „Angemeldet bleiben“ ankreuzt, im Vergleich zu den standardmäßigen 30 Tagen. Speichern Sie ein persistent-Flag in der Datenbankzeile des Aktualisierungstokens, damit Sie persistente Sitzungen in den Sicherheitseinstellungen des Benutzers separat anzeigen und widerrufen können. Erweitern Sie niemals die TTL des Zugriffstokens – das macht den Zweck zunichte.


Nächste Schritte

Die richtig durchgeführte JWT-Authentifizierung ist die Grundlage jeder sicheren Webanwendung. Von RS256-Signierung über HttpOnly-Cookie-Speicherung, Aktualisierungstokenrotation bis hin zu Sperrstrategien schützen die Muster in diesem Leitfaden Ihre Benutzer vor den häufigsten Authentifizierungsangriffen.

ECOSIRE implementiert eine kampferprobte Authentifizierungsarchitektur – einschließlich OIDC-Integration mit Authentik, HttpOnly-Cookie-Flows und Redis-gestützter Token-Verwaltung – in allen unseren Backend-Projekten. [Entdecken Sie unsere sicherheitsorientierten Entwicklungsdienste] (/services), um zu erfahren, wie wir Ihre Authentifizierungsebene härter machen können.

E

Geschrieben von

ECOSIRE Research and Development Team

Entwicklung von Enterprise-Digitalprodukten bei ECOSIRE. Einblicke in Odoo-Integrationen, E-Commerce-Automatisierung und KI-gestützte Geschäftslösungen.

Chatten Sie auf WhatsApp