JWT-Sicherheit: 5 kritische Fehler, die dein API-Backend gefährden
April 3, 2026JWT-Sicherheit: 5 kritische Fehler, die dein API-Backend gefährden
Was sind JSON Web Tokens (JWTs)?
Der Aufbau eines JWTs
- Header: Enthält Metadaten über den Token, typischerweise den Signaturalgorithmus (z.B. HS256 oder RS256) und den Typ des Tokens (JWT).
- Payload (Claims): Dies ist der eigentliche Datenblock. Hier werden sogenannte Claims gespeichert – Aussagen über eine Entität (normalerweise der Benutzer) und zusätzliche Metadaten. Beispiele sind die Benutzer-ID (sub), der Aussteller (iss), der Empfänger (aud), die Gültigkeitsdauer (exp) und die Rollen des Benutzers.
- Signatur: Eine kryptografische Signatur, die verwendet wird, um die Integrität des Tokens zu überprüfen. Sie wird aus dem kodierten Header, dem kodierten Payload und einem geheimen Schlüssel (oder einem privaten Schlüssel bei asymmetrischen Algorithmen) berechnet. Sie stellt sicher, dass der Token auf dem Transportweg nicht manipuliert wurde.
JWTs im Einsatz für Authentifizierung und Autorisierung
Sensible Daten im Payload? Vorsicht!
Kritische Sicherheitsaspekte bei der JWT-Implementierung
1. Starke Signaturen und kurze Gültigkeitsdauern
2. Gründliche Claim-Validierung ist Pflicht
- exp (Expiration Time): Das Token darf nicht abgelaufen sein.
- nbf (Not Before): Das Token darf noch nicht gültig sein.
- iss (Issuer): Prüfen Sie, ob der Aussteller des Tokens Ihr erwarteter Authentifizierungsdienst ist. Andernfalls könnten Tokens von anderen Diensten akzeptiert werden.
- aud (Audience): Stellen Sie sicher, dass das Token für Ihre spezifische Anwendung oder Ihren Dienst bestimmt ist. Dies verhindert, dass ein Token, das für Anwendung A ausgestellt wurde, von Anwendung B akzeptiert wird.
- sub (Subject): Die Identität des Principals, über den der Token Aussagen macht.
3. Die 'none'-Algorithmus-Falle
Das Dilemma der Token-Widerrufung (Revocation)
Strategien für den Widerruf: TTL, Blocklisten und Refresh Tokens
- Sehr kurze TTLs: Wenn Access Tokens nur wenige Minuten gültig sind, ist das Fenster für Missbrauch im Falle eines Diebstahls gering. Bei einem Logout wartet man einfach, bis das Token abläuft.
- Blocklisten (Blacklists): Man pflegt serverseitig eine Liste von kompromittierten oder explizit widerrufenen Token-IDs (JTI-Claim). Jede Anfrage mit einem JWT muss zusätzlich gegen diese Blockliste geprüft werden. Dies erfordert jedoch einen serverseitigen Zustand und kann die Performance beeinflussen, je nach Größe und Abfragefrequenz der Blockliste (oft in einem schnellen Cache wie Redis).
- Kombination mit Refresh Tokens: Eine bewährte Methode ist die Verwendung von kurzen Access Tokens (z.B. 5-15 Minuten) für den Zugriff auf Ressourcen und längeren Refresh Tokens (z.B. Tage oder Wochen) zum Anfordern neuer Access Tokens. Refresh Tokens werden serverseitig verwaltet und können dort jederzeit widerrufen werden. Sie werden typischerweise nur einmalig verwendet und dann durch ein neues Refresh Token ersetzt, um Reuse-Angriffe zu erschweren.
Sichere Speicherung von JWTs im Frontend
4. Warum localStorage ein Sicherheitsrisiko ist
- HttpOnly-Cookies: Dies ist die bevorzugte Methode für traditionelle Webanwendungen. HttpOnly-Cookies können nicht über JavaScript gelesen oder manipuliert werden, was sie resistenter gegen XSS macht. Sie werden bei jeder Anfrage automatisch mitgesendet. Allerdings sind sie anfälliger für CSRF (Cross-Site Request Forgery), was durch zusätzliche CSRF-Token gemindert werden muss.
- In-Memory-Speicher: Für Single Page Applications (SPAs) kann ein Access Token im Arbeitsspeicher des Browsers gespeichert werden. Dies bietet Schutz vor persistentem XSS, da das Token nach einem Seiten-Refresh (oder Browser-Neustart) verloren geht. Es erfordert jedoch, dass das Token häufig über ein Refresh Token neu angefordert wird.
Performance-Überlegungen: Payload schlank halten
5. Der Performance-Impact bei großen Payloads
Praktische Beispiele aus dem Entwickleralltag
JWT-Erstellung und Validierung im Python-Backend
Python
import jwt
import datetime
SECRET_KEY = "your-super-secret-key-that-is-strong-and-random-in-production"
def create_jwt(user_id: str) -> str:
payload = {
"user_id": user_id,
"exp": datetime.datetime.utcnow() + datetime.timedelta(minutes=30), # Ablauf in 30 Min
"iat": datetime.datetime.utcnow(),
"iss": "your-backend-service", # Aussteller
"aud": "your-client-application" # Empfänger
}
return jwt.encode(payload, SECRET_KEY, algorithm="HS256")
def decode_jwt(token: str) -> dict | None:
try:
# Strikte Validierung von issuer und audience ist hier integriert
decoded_payload = jwt.decode(
token,
SECRET_KEY,
algorithms=["HS256"], # Erlaubte Algorithmen
audience="your-client-application",
issuer="your-backend-service"
)
return decoded_payload
except (jwt.ExpiredSignatureError, jwt.InvalidTokenError, jwt.exceptions.InvalidAudienceError, jwt.exceptions.InvalidIssuerError) as e:
print(f"JWT Fehler: {e}")
return None
# Beispielnutzung:
token = create_jwt("user123")
print(f"Erzeugtes Token: {token}")
decoded_data = decode_jwt(token)
print(f"Dekodiertes Payload: {decoded_data}")
# Beispiel eines abgelaufenen Tokens (simuliert durch Manipulation des exp-Werts oder Wartezeit)
# decoded_data_expired = jwt.decode(token, SECRET_KEY, algorithms=["HS256"], audience="your-client-application", issuer="your-backend-service", options={"verify_exp": False})
# decoded_data_expired["exp"] = datetime.datetime.utcnow() - datetime.timedelta(minutes=1)
# expired_token = jwt.encode(decoded_data_expired, SECRET_KEY, algorithm="HS256") # Das ist KEIN sicherer Weg!
# print(f"Versuch dekodiert abgelaufen: {decode_jwt(expired_token)}")
JWT-Nutzung im TypeScript-Frontend
Typescript
async function callAuthenticatedApi(url: string, jwtToken: string) {
try {
const response = await fetch(url, {
method: 'GET',
headers: {
'Authorization': `Bearer ${jwtToken}`, // JWT im Authorization-Header
'Content-Type': 'application/json'
}
});
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
return await response.json();
} catch (error) {
console.error('Fehler beim API-Aufruf:', error);
throw error;
}
}
// Client-seitige Dekodierung (NUR zur Anzeige, NICHT zur Validierung!)
// Diese Funktion sollte niemals für Sicherheitsprüfungen verwendet werden.
// Die Validierung gehört IMMER ins Backend.
function decodeJwtPayload(token: string): any | null {
try {
const base64Url = token.split('.')[1];
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
const jsonPayload = decodeURIComponent(atob(base64).split('').map(function(c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
return JSON.parse(jsonPayload);
} catch (e) {
console.error('Fehler beim Dekodieren des JWT-Payloads:', e);
return null;
}
}
// Beispielaufruf (angenommen, das JWT kommt aus einem HttpOnly-Cookie oder In-Memory)
// const myJwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoidXNlcjEyMyIsImV4cCI6MTY3ODkwNTY3OCwiaWF0IjoxNjc4OTAyMDc4LCJpc3MiOiJ5b3VyLWJhY2tlbmQtc2VydmljZSIsImF1ZCI6InlvdXItY2xpZW50LWFwcGxpY2F0aW9uIn0.SIGNATUR_HIER";
// callAuthenticatedApi('https://api.example.com/data', myJwt)
// .then(data => console.log('API-Antwort:', data))
// .catch(err => console.error(err));
// const decodedClientInfo = decodeJwtPayload(myJwt);
// console.log('Client-seitig dekodierte Infos (nur Anzeige):', decodedClientInfo);
JWTs im DevOps-Kontext: Manuelle Dekodierung (Bash)
Bash
YOUR_JWT_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
echo "HEADER:"
echo "$YOUR_JWT_TOKEN" | awk -F'.' '{print $1}' | base64 --decode | jq '.'
echo "\nPAYLOAD:"
echo "$YOUR_JWT_TOKEN" | awk -F'.' '{print $2}' | base64 --decode | jq '.'
# Hinweis: Für die Signaturprüfung ist ein dediziertes Tool oder Code erforderlich.
# jq muss installiert sein (apt install jq / brew install jq)
JWT-Konfiguration mit Docker
Dockerfile
# Dockerfile Auszug für einen Backend-Dienst
FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
# Umgebungsvariablen für Entwicklung. In Produktion NIE so setzen!
ENV JWT_SECRET="mySuperSecureDevelopmentSecret"
ENV JWT_ALGORITHM="HS256"
ENV JWT_EXPIRATION_MINUTES="30"
CMD ["python", "app.py"]
Yaml
# docker-compose.yml Auszug für Dev-Umgebung
services:
backend:
build: .
ports:
- "8000:8000"
environment:
JWT_SECRET: "mySuperSecureDevelopmentSecret"
JWT_ALGORITHM: "HS256"
JWT_EXPIRATION_MINUTES: "30"
# In Produktion: secrets verwenden! z.B. Docker Secrets, Kubernetes Secrets oder HashiCorp Vault.