Ein JWT zu prüfen heißt nicht eine Prüfung, sondern sechs, und die Signatur ist nur eine davon. Kurz gesagt: Der Verifizierer entscheidet, wie das Token geprüft wird, nicht das Token. Lege den Algorithmus auf dem Server fest, suche den Schlüssel unter den Schlüsseln, denen du für diesen Aussteller bereits vertraust, verlange exp, vergleiche iss und aud exakt, prüfe typ, damit sich eine Art von Token nicht als eine andere ausgeben kann, lass höchstens eine Minute Uhrenabweichung zu, und halte das JWKS im Cache vor, mit einer durch ein Rate Limit begrenzten Aktualisierung bei unbekannten Werten von kid. Alles Weitere beschreibt, was schiefgeht, wenn einer dieser Punkte fehlt, gemessen an echten Bibliotheken.
Alle Ausgaben auf dieser Seite stammen von Tokens, die für diesen Artikel mit einem frisch erzeugten ES256-Schlüssel ausgestellt wurden. Geprüft haben sie jose 6.2.13 und jsonwebtoken 9.0.3 (mit jwks-rsa 4.1.0) unter Node.js 24.22, PyJWT 2.15.1 unter Python 3.14.8 sowie golang-jwt 5.3.1 mit keyfunc 3.8.2 unter Go 1.27.3, jeweils im offiziellen Docker-Image.
Was eine Bibliothek prüft, wenn du nur den Schlüssel übergibst
Die meisten JWT-Bugs sind keine Bugs in der Bibliothek. Es sind Aufrufe, die die Signatur prüfen und dann aufhören, weil der Aufrufer der Bibliothek nie gesagt hat, welchen Aussteller, welche Audience oder welchen Algorithmus sie erwarten soll. So ging jede Bibliothek mit drei Tokens um, wenn sie mit einem gültigen öffentlichen Schlüssel und ohne weitere Optionen aufgerufen wurde (PyJWT läuft ohne algorithms nicht, deshalb erhielt PyJWT ["ES256"]):
| Token | jose jwtVerify | jsonwebtoken verify | PyJWT decode | golang-jwt Parse |
|---|---|---|---|---|
Mit dem richtigen Schlüssel signiert, aber iss und aud eines anderen Dienstes | akzeptiert | akzeptiert | abgelehnt: Invalid audience | akzeptiert |
Mit dem richtigen Schlüssel signiert, ohne Claim exp | akzeptiert | akzeptiert | akzeptiert | akzeptiert |
alg: "none", leere Signatur | abgelehnt | abgelehnt | abgelehnt | abgelehnt |
Daraus lassen sich zwei Lehren ziehen. Erstens lehnen alle vier Bibliotheken unsignierte Tokens standardmäßig ab; das berüchtigte Problem mit none steckt heute also vor allem in selbst geschriebenen Verifizierern. Zweitens ist ein Token ohne exp in allen vier für immer gültig, und drei der vier akzeptieren ein Token, das für einen anderen Dienst ausgestellt wurde, solange derselbe Schlüssel es signiert hat. exp, nbf, iss und aud sind in RFC 7519 §4.1 optionale Claims; Bibliotheken prüfen sie, wenn sie vorhanden sind und die Prüfung konfiguriert ist, um den Rest musst du dich selbst kümmern.
Fehler 1: das Token den Algorithmus wählen lassen
Der Header alg steckt in dem Token, das du noch nicht geprüft hast. Ein Verifizierer, der ihn liest und danach verzweigt, lässt den Absender die Regeln bestimmen. RFC 8725 §3.1, die JWT Best Current Practices, ist da unmissverständlich: Bibliotheken müssen dem Aufrufer erlauben, die unterstützten Algorithmen festzulegen, und dürfen keine anderen verwenden, und jeder Schlüssel muss mit genau einem Algorithmus verwendet werden.
Der klassische Fehlerfall ist die HS/RS-Verwechslung (oder HS/ES-Verwechslung), beschrieben in RFC 8725 §2.1 und für jsonwebtoken als CVE-2015-9235 erfasst. Ein Dienst hat einen öffentlichen Schlüssel und prüft, „was auch immer der Header sagt“. Jemand berechnet einen HMAC über einen Header mit "alg": "HS256" und nimmt dabei den PEM-Text des öffentlichen Schlüssels als Secret. Der öffentliche Schlüssel ist öffentlich, also kann das jeder. Hier ist ein selbst geschriebener Verifizierer mit genau diesem Aufbau:
import { createHmac, timingSafeEqual, verify } from "node:crypto";
import { readFileSync } from "node:fs";
// Anti-pattern: the token picks the algorithm, one key object serves both families.
function naiveVerify(token, publicKeyPem) {
const [h, p, s] = token.split(".");
const { alg } = JSON.parse(Buffer.from(h, "base64url"));
const input = Buffer.from(`${h}.${p}`);
const sig = Buffer.from(s, "base64url");
if (alg === "HS256") {
const mac = createHmac("sha256", publicKeyPem).update(input).digest();
return mac.length === sig.length && timingSafeEqual(mac, sig);
}
if (alg === "ES256") {
return verify("sha256", input, { key: publicKeyPem, dsaEncoding: "ieee-p1363" }, sig);
}
throw new Error(`unsupported alg ${alg}`);
}
const pem = readFileSync("out/public.pem", "utf8");
const tokens = JSON.parse(readFileSync("out/tokens.json", "utf8"));
console.log("ES256 token from the issuer:", naiveVerify(tokens.ok, pem));
console.log("HS256 token keyed with the public PEM:", naiveVerify(tokens["HS256 keyed with the public PEM"], pem));
ES256 token from the issuer: true
HS256 token keyed with the public PEM: true
Beide liefern true. Das zweite Token wurde ohne den privaten Schlüssel erstellt. Beachte, dass der zeitkonstante Vergleich nicht hilft: Der Code achtet sorgfältig auf das Timing und liegt beim Vertrauen trotzdem falsch.
Die Abhilfe ist eine feste Allowlist auf der Seite des Verifizierers, idealerweise mit einem einzigen Wert, und Schlüssel, die ihren Algorithmus mit sich tragen. Alle vier Bibliotheken lehnten das gefälschte Token ab, sobald sie algorithms: ["ES256"] bekamen, jede mit ihrer eigenen Meldung: jose "alg" (Algorithm) Header Parameter value not allowed, jsonwebtoken invalid algorithm, PyJWT The specified alg value is not allowed, golang-jwt signing method HS256 is invalid. Außerdem hat jede eine zweite Verteidigungslinie, weshalb die Verwechslung in Bibliothekscode heute selten ist:
- jsonwebtoken 9.x (seit 9.0.0, Dezember 2022) leitet die erlaubten Algorithmen aus dem Schlüsseltyp ab, wenn du
algorithmsweglässt: Ein EC-Schlüssel erlaubt nurES256,ES384undES512. - PyJWT weigert sich, einen öffentlichen PEM-Schlüssel oder ein Zertifikat als HMAC-Secret zu verwenden: The specified key is an asymmetric key or x509 certificate and should not be used as an HMAC secret.
- golang-jwt prüft den Go-Typ des Schlüssels: Die HMAC-Methode erwartet
[]byte, deshalb scheitert ein*ecdsa.PublicKeymit dem Fehler key is of invalid type. - jose prüft, ob der Schlüsseltyp zum Algorithmus passt. Selbst ohne die Option
algorithmsscheiterte das gefälschte Token am öffentlichen EC-Schlüssel mitKeyObject instances must be of type "secret" for the HS256 algorithm.
Keiner dieser Mechanismen ersetzt die Allowlist. Diese Schutzmaßnahmen decken den üblichen Fall ab, nicht einen eigenen Schlüssel-Loader, der der Bibliothek rohe Bytes übergibt.
Fehler 2: „none“ in einem selbst gebauten Codepfad akzeptieren
RFC 7518 §3.6 registriert "alg": "none" für Tokens, deren Integrität durch etwas anderes geschützt ist. In der Tabelle oben hat jede Bibliothek es standardmäßig abgelehnt. Das Risiko liegt im Code rund um die Bibliothek: ein „Debug-Modus“, der die Prüfung überspringt, ein Zweig, der eine leere Signatur als „noch nicht signiert“ behandelt, oder eine Denylist, die alg unter Beachtung der Groß- und Kleinschreibung vergleicht. Die Überarbeitung der BCP, draft-ietf-oauth-rfc8725bis (Stand Oktober 2026 in der Warteschlange des RFC-Editors), nennt genau diesen Fall: eine Denylist für none, an der ein Header mit noNE vorbeirutscht. Eine Allowlist hat keine solche Lücke.
Fehler 3: kid, jku, x5u oder einem eingebetteten jwk vertrauen
kid dient dazu, einen von mehreren Schlüsseln auszuwählen, denen du bereits vertraust. Er ist kein Verweis auf einen Schlüssel, den das Token mitliefert. Laut RFC 8725 §3.10 sind empfangene Header als nicht vertrauenswürdige Eingabe zu behandeln:
- Schlag
kidin einer Map nach, nicht in einer Abfrage oder einem Pfad. Einkid, der in SQL, LDAP oder einen Dateinamen gelangt, ist ein Injection-Punkt. Die JWKS-Bibliotheken in diesem Artikel vergleichen ihn nur mit den Werten vonkidder Schlüssel, die sie von deiner konfigurierten URL abgerufen haben, und genau so ist es richtig. - Ignoriere
jkuundx5u. Beide sind URLs im Token. Ruft dein Server sie ab, bestimmt der Absender, wohin sich dein Server verbindet und welche Schlüssel er verwendet. Konfiguriere stattdessen die JWKS-URL pro Aussteller; RFC 8725 empfiehlt, solche URLs mit einer Allowlist abzugleichen, falls du sie überhaupt verwenden musst. - Prüfe ein Access Token nie mit einem
jwkoderx5c, der in seinem eigenen Header eingebettet ist. Ein Token, das mit einem Schlüssel signiert ist, den es selbst mitbringt, beweist nur, dass der Absender irgendeinen Schlüssel besitzt. Protokolle, die tatsächlich einen eingebetteten Schlüssel verwenden, etwa DPoP-Proofs, binden ihn an etwas anderes (den Claimcnfdes Access Tokens). Siehe DPoP erklärt. - Behandle einen fehlenden
kidexplizit. jwks-rsa weigert sich zu raten, wenn der Schlüsselsatz mehrere Schlüssel enthält und das Token keinen nennt (No KID specified and JWKS endpoint returned more than 1 key); eine selbst geschriebene Schlüsselsuche, die „den ersten Schlüssel“ zurückgibt, wählt nach einer Rotation stillschweigend den falschen.
Fehler 4: aud und iss nicht oder zu locker prüfen
Ein Identity Provider signiert Tokens für viele Clients und APIs meist mit demselben Schlüssel. Prüft deine API aud nicht, funktioniert ein Token, das für irgendeine andere Anwendung desselben Anbieters ausgestellt wurde, auch bei deiner – das ist die erste Zeile der Tabelle. RFC 8725 §3.9 verlangt, dass der Empfänger ein JWT ablehnt, dessen Audience fehlt oder nicht seine eigene ist, und RFC 9068 §4 verlangt für OAuth-Access-Tokens eine exakte Übereinstimmung von iss.
In der Praxis kommt es auf drei Details an:
- Exakter String-Vergleich.
audundisssind Strings, bei denen Groß- und Kleinschreibung zählt, undhttps://id.example.comist nichthttps://id.example.com/. Kopiere den Aussteller aus dem Discovery-Dokument des Anbieters, nicht aus einem Token, das du zufällig gerade hast. audkann ein Array sein. Bibliotheken akzeptieren das Token, wenn irgendein Element übereinstimmt. Das ist nach RFC 7519 korrekt, bedeutet aber, dass ein Token für["https://api.example.com", "https://admin.example.com"]bei beiden APIs funktioniert. Soll deine API nur Tokens akzeptieren, die ausschließlich für sie ausgestellt wurden, bietet PyJWToptions={"strict_aud": True}; anderswo prüfst du die Länge des Arrays selbst.- Schlüssel müssen zum Aussteller gehören. RFC 8725 §3.8: Hat ein Token einen
iss, muss der Schlüssel, mit dem es geprüft wurde, ein Schlüssel dieses Ausstellers sein. Bei mehreren Ausstellern hältst du einen JWKS-Client pro Aussteller und wählst ihn anhand der Konfiguration aus, nie allein anhand des ungeprüftenissdes Tokens. Bei mandantenfähigen Anbietern, die einen Schlüsselsatz über alle Mandanten hinweg teilen (Microsoft Entra ID ist der typische Fall), ist die Prüfung vonissdas Einzige, was deinen Mandanten von allen anderen trennt; Validierung von OIDC-ID-Tokens behandelt das im Detail.
Fehler 5: Tokens ohne exp und großzügige Toleranz
Eine Bibliothek prüft exp, wenn der Claim vorhanden ist. Fehlte er, akzeptierten alle vier das Token. Vergisst dein Aussteller den Claim einmal oder findet jemand einen Codepfad, der Tokens ohne ihn ausstellt, laufen diese Tokens nie ab. Mach exp zur Pflicht: requiredClaims in jose, options={"require": [...]} in PyJWT, jwt.WithExpirationRequired() in golang-jwt und eine explizite Prüfung in jsonwebtoken, das keine solche Option hat.
Uhrenabweichung gibt es wirklich, aber sie ist klein. RFC 7519 §4.1.4 erlaubt „einen kleinen Spielraum, in der Regel nicht mehr als ein paar Minuten“. Dreißig bis sechzig Sekunden reichen für Server, die per NTP synchron gehalten werden. Eine Toleranz von einer Stunde, oft eingebaut, damit ein instabiler Test durchläuft, verlängert die Lebensdauer jedes Tokens um eine Stunde, auch bei Tokens, deren Gültigkeit du bewusst beenden wolltest. Die Option heißt clockTolerance in jose und jsonwebtoken (in Sekunden), leeway in PyJWT und jwt.WithLeeway in golang-jwt. Dieselbe Toleranz gilt für nbf.
Schlägt die Prüfung eines Tokens, das gültig sein „sollte“, mit einem Ablauf- oder Not-Before-Fehler fehl, vergleiche die Uhr des Servers mit der des Ausstellers, bevor du die Toleranz anfasst. Ein Container oder eine VM, die mit veralteter Uhr aus dem Ruhezustand zurückkehrt, erzeugt genau die Fehler jwt not active und "nbf" claim timestamp check failed aus der Ausgabe unten.
Fehler 6: die falsche Art von Token akzeptieren
Ein Anbieter für OpenID Connect signiert ID-Tokens und Access Tokens mit demselben Schlüssel, oft mit demselben iss und manchmal mit überlappendem aud. Eine API, die nur Signatur, Aussteller und Audience prüft, kann ein ID-Token als Access Token akzeptieren. RFC 8725 §3.11 empfiehlt explizite Typisierung, und RFC 9068 macht sie für JWT-Access-Tokens verpflichtend: Der Header typ muss at+jwt (oder application/at+jwt) sein, und Ressourcenserver müssen jeden anderen Wert ablehnen. Stellt dein Anbieter Tokens nach RFC 9068 aus, prüfe typ; falls nicht, prüfe einen Claim, den nur Access Tokens tragen, etwa scope oder client_id.
Fehler 7: decode aufrufen, wo du verify gemeint hast
Jede Bibliothek hat eine Funktion, die ein Token parst, ohne es zu prüfen: jwt.decode() in jsonwebtoken, decodeJwt() in jose, jwt.decode(..., options={"verify_signature": False}) in PyJWT, ParseUnverified in golang-jwt. Es gibt sie für das Routing (iss lesen, um einen JWKS-Client auszuwählen) und für das Debugging. Im Code-Review sollte jede Verwendung dieser Funktionen auf einem Anfragepfad als verdächtig gelten und jede Entscheidung auf Grundlage ihrer Ausgabe – außer der Wahl, welcher Verifizierer als Nächstes aufgerufen wird – als Bug.
Fehler 8: das JWKS bei jeder Anfrage abrufen – oder nie
Den Schlüsselsatz bei jeder Anfrage abzurufen, fügt jedem Aufruf einen Netzwerk-Roundtrip hinzu und macht die Verfügbarkeit des Identity Providers zu deiner eigenen. Ihn dauerhaft im Cache zu halten bedeutet, dass die erste Schlüsselrotation jede Anfrage bis zum nächsten Neustart scheitern lässt. Richtig ist das Verhalten, das die JWKS-Clients unten umsetzen: ein zeitbasierter Cache plus eine Aktualisierung, wenn ein Token einen unbekannten kid nennt, wobei ein Rate Limit diese Aktualisierung begrenzt, damit ein Strom von Tokens mit zufälligen Werten von kid deine API nicht in einen Lastgenerator gegen den Anbieter verwandeln kann.
| Client | Cache-Lebensdauer (Standard) | Unbekannter kid | Timeout beim Abruf |
|---|---|---|---|
jose createRemoteJWKSet | 10 Minuten (cacheMaxAge) | erneuter Abruf, höchstens einmal pro 30 s (cooldownDuration) | 5 s |
| jwks-rsa 4.1.0 | 10 Minuten pro Schlüssel, bis zu 5 Schlüssel | erneuter Abruf bei jedem Fehltreffer; rateLimit ist aus, sofern nicht aktiviert (dann 10 pro Minute) | 30 s |
PyJWT PyJWKClient | 300 s (lifespan) | erneuter Abruf, höchstens einmal pro 30 s (cooldown_duration) | 30 s |
keyfunc v3 NewDefault | Aktualisierung im Hintergrund jede Stunde | erneuter Abruf, höchstens einmal pro 5 Minuten | 1 Minute |
Die Werte sind die Standardwerte in den oben genannten Versionen, abgelesen aus deren Quellcode. Der Unterschied ist messbar: 20 Tokens mit zufälligen Werten von kid führten mit den Standardeinstellungen von jwks-rsa zu 20 Abrufen des Schlüsselsatzes, mit rateLimit: true zu 10 (die anderen zehn scheiterten sofort mit JwksRateLimitError) und mit jose zu einem einzigen Abruf. Erstelle den Client einmal pro Aussteller und verwende ihn wieder: Ein JWKS-Client, der im Request-Handler erzeugt wird, hat jedes Mal einen leeren Cache. Die Rotation selbst, aus Sicht des Ausstellers, behandelt JWKS-Schlüsselrotation ohne Ausfallzeit.
Ein Verifizierer, der es richtig macht, in vier Bibliotheken
Die vier Verifizierer unten tun dasselbe: nur ES256, Schlüssel von der JWKS-URL des Ausstellers, exakter Vergleich von iss und aud, exp als Pflicht, typ: at+jwt, 30 Sekunden Toleranz. Jeder wurde mit denselben acht Tokens getestet.
import { createRemoteJWKSet, jwtVerify } from "jose";
// Created once per issuer and reused: caches the key set, re-fetches it
// when a token names an unknown kid (at most once per cooldown).
const JWKS = createRemoteJWKSet(new URL(process.env.JWKS_URL), {
cacheMaxAge: 600_000, // re-fetch at least every 10 minutes
cooldownDuration: 30_000, // at most one kid-miss re-fetch per 30 s
});
export async function verifyAccessToken(token) {
const { payload } = await jwtVerify(token, JWKS, {
algorithms: ["ES256"], // fixed by the verifier, never taken from the token
issuer: "https://id.example.com",
audience: "https://api.example.com",
typ: "at+jwt", // RFC 9068 access token, not an ID token
requiredClaims: ["exp", "sub"], // exp is only checked when present
clockTolerance: 30, // seconds
});
return payload;
}
import jwt from "jsonwebtoken";
import jwksClient from "jwks-rsa";
const client = jwksClient({
jwksUri: process.env.JWKS_URL,
cache: true, // per-kid cache, 10 minutes by default
rateLimit: true, // off by default; caps JWKS fetches at 10 per minute
});
const getKey = (header, callback) =>
client.getSigningKey(header.kid, (err, key) => callback(err, key?.getPublicKey()));
export function verifyAccessToken(token) {
const options = {
algorithms: ["ES256"],
issuer: "https://id.example.com",
audience: "https://api.example.com",
clockTolerance: 30,
complete: true, // also return the header, to check typ
};
return new Promise((resolve, reject) =>
jwt.verify(token, getKey, options, (err, decoded) => {
if (err) return reject(err);
if (decoded.header.typ !== "at+jwt") return reject(new Error("unexpected typ"));
if (typeof decoded.payload.exp !== "number") return reject(new Error("exp is required"));
resolve(decoded.payload);
}),
);
}
import os
import jwt
# One client per issuer, reused across requests: caches the key set and
# re-fetches on an unknown kid (at most once per cooldown).
jwks = jwt.PyJWKClient(os.environ["JWKS_URL"], lifespan=600)
def verify_access_token(token: str) -> dict:
signing_key = jwks.get_signing_key_from_jwt(token)
decoded = jwt.decode_complete(
token,
signing_key,
algorithms=["ES256"], # required argument in PyJWT 2.x
issuer="https://id.example.com",
audience="https://api.example.com",
leeway=30,
options={"require": ["exp", "iss", "aud", "sub"]},
)
if decoded["header"].get("typ") != "at+jwt":
raise jwt.InvalidTokenError("unexpected typ")
return decoded["payload"]
package main
import (
"context"
"errors"
"time"
"github.com/MicahParks/keyfunc/v3"
"github.com/golang-jwt/jwt/v5"
)
type Verifier struct {
jwks keyfunc.Keyfunc
parser *jwt.Parser
}
func NewVerifier(ctx context.Context, jwksURL string) (*Verifier, error) {
// Background refresh every hour; an unknown kid triggers at most
// one extra fetch per 5 minutes (keyfunc defaults).
jwks, err := keyfunc.NewDefaultCtx(ctx, []string{jwksURL})
if err != nil {
return nil, err
}
parser := jwt.NewParser(
jwt.WithValidMethods([]string{"ES256"}),
jwt.WithIssuer("https://id.example.com"),
jwt.WithAudience("https://api.example.com"),
jwt.WithExpirationRequired(),
jwt.WithLeeway(30*time.Second),
)
return &Verifier{jwks: jwks, parser: parser}, nil
}
func (v *Verifier) VerifyAccessToken(raw string) (jwt.MapClaims, error) {
claims := jwt.MapClaims{}
token, err := v.parser.ParseWithClaims(raw, claims, v.jwks.Keyfunc)
if err != nil {
return nil, err
}
if token.Header["typ"] != "at+jwt" {
return nil, errors.New("unexpected typ")
}
return claims, nil
}
Die Test-Tokens: ein gültiges Token und sieben, die jeweils genau eine Regel verletzen. Das JWKS wurde innerhalb des Containers über HTTP ausgeliefert, und jede Bibliothek rief es über ihren JWKS-Client ab.
--- jose
ok ACCEPTED sub=user-42
aud for another API ERR_JWT_CLAIM_VALIDATION_FAILED: unexpected "aud" claim value
no exp ERR_JWT_CLAIM_VALIDATION_FAILED: missing required "exp" claim
expired 1 h ago ERR_JWT_EXPIRED: "exp" claim timestamp check failed
nbf in 1 h ERR_JWT_CLAIM_VALIDATION_FAILED: "nbf" claim timestamp check failed
typ JWT (an ID token) ERR_JWT_CLAIM_VALIDATION_FAILED: unexpected "typ" JWT header value
unknown kid ERR_JWKS_NO_MATCHING_KEY: no applicable key found in the JSON Web Key Set
HS256 keyed with the public PEM ERR_JOSE_ALG_NOT_ALLOWED: "alg" (Algorithm) Header Parameter value not allowed
--- jsonwebtoken
ok ACCEPTED sub=user-42
aud for another API JsonWebTokenError: jwt audience invalid. expected: https://api.example.com
no exp Error: exp is required
expired 1 h ago TokenExpiredError: jwt expired
nbf in 1 h NotBeforeError: jwt not active
typ JWT (an ID token) Error: unexpected typ
unknown kid JsonWebTokenError: error in secret or public key callback: Unable to find a signing key that matches '2026-09-z'
HS256 keyed with the public PEM JsonWebTokenError: invalid algorithm
--- PyJWT
ok ACCEPTED sub=user-42
aud for another API InvalidAudienceError: Audience doesn't match
no exp MissingRequiredClaimError: Token is missing the "exp" claim
expired 1 h ago ExpiredSignatureError: Signature has expired
nbf in 1 h ImmatureSignatureError: The token is not yet valid (nbf)
typ JWT (an ID token) InvalidTokenError: unexpected typ
unknown kid PyJWKClientError: Unable to find a signing key that matches: "2026-09-z"
HS256 keyed with the public PEM InvalidAlgorithmError: The specified alg value is not allowed
--- golang-jwt v5 + keyfunc v3
ok ACCEPTED sub=user-42
aud for another API token has invalid claims: token has invalid audience
no exp token has invalid claims: token is missing required claim: exp claim is required
expired 1 h ago token has invalid claims: token is expired
nbf in 1 h token has invalid claims: token is not valid yet
typ JWT (an ID token) unexpected typ
unknown kid token is unverifiable: error while executing keyfunc: key not found "2026-09-z"
failed keyfunc: could not read JWK from storage
HS256 keyed with the public PEM token signature is invalid: signing method HS256 is invalid
Die meisten Bibliotheksmeldungen oben haben im Fehlerkatalog eine eigene Seite mit Ursachen und Lösungen, zum Beispiel unexpected "aud" claim value, jwt not active und no applicable key found in the JSON Web Key Set.
Checkliste
- Eine Verifizierer-Konfiguration pro Aussteller, beim Start erstellt: JWKS-URL, Aussteller, Audience, Liste der Algorithmen.
- Algorithmen: eine feste Allowlist, meist mit einem einzigen Wert. Nie aus dem Token abgeleitet, nie mit
none. - Schlüssel: nur aus dem konfigurierten JWKS oder einer Schlüsseldatei;
kidals Suchschlüssel in einer Map;jku,x5uund eingebetteterjwkbei Access Tokens ignoriert. - Claims:
expverpflichtend;issundaudexakt verglichen;nbfbeachtet; Toleranz von 30 bis 60 Sekunden. - Token-Typ:
typgeprüft (at+jwtfür Access Tokens nach RFC 9068) oder ein anderer Claim, der Access Tokens von ID-Tokens unterscheidet. - JWKS-Cache: ein Client, wiederverwendet; zeitbasierte Aktualisierung; Aktualisierung bei unbekanntem
kidmit Rate Limit. - Kein
decodeohne Prüfung auf einem Anfragepfad. - Tests: ein gültiges Token und ein Token pro Regel oben, bei dem jeweils ein Fehlschlag erwartet wird. Die acht Tokens aus diesem Artikel eignen sich als Ausgangsbasis.
Quellen
- RFC 7519: JSON Web Token, §4.1 (registrierte Claims), §7.2 (Validierung)
- RFC 7518: JSON Web Algorithms, §3.6 (
none) - RFC 8725: JSON Web Token Best Current Practices, §2.1, §3.1, §3.8 bis §3.11
- draft-ietf-oauth-rfc8725bis, die Aktualisierung von RFC 8725
- RFC 9068: JWT-Profil für Access Tokens in OAuth 2.0, §2.1 und §4
- Dokumentation von jose:
jwtVerify,createRemoteJWKSet - READMEs von jsonwebtoken und jwks-rsa
- PyJWT-Dokumentation: Verwendung,
PyJWKClient - golang-jwt/jwt v5 und keyfunc v3