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,audundalg– vertrauen Sie niemals dem vorzeichenlosennone-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.
HttpOnly-Cookie-Speicherung
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,audbei 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.
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.
Verwandte Artikel
API Rate Limiting: Patterns and Best Practices
Master API rate limiting with token bucket, sliding window, and fixed counter patterns. Protect your backend with NestJS throttler, Redis, and real-world configuration examples.
Authentik OIDC/SSO: Complete Integration Guide
Complete Authentik OIDC and SSO integration guide: OAuth2 provider setup, Next.js callback handling, NestJS JWT validation, user provisioning, and production configuration.
Cybersecurity Trends 2026-2027: Zero Trust, AI Threats, and Defense
The definitive guide to cybersecurity trends for 2026-2027—AI-powered attacks, zero trust implementation, supply chain security, and building resilient security programs.