JWT vs. PASETO ist eine Abwägung zwischen Flexibilität und Schutz vor Fehlanwendung. JWT (RFC 7519, meist ein JWS) lässt das Token seinen Algorithmus aus einer langen Registry nennen, was klassische Bugs wie alg: none und die Verwechslung von RS256 mit HS256 ermöglichte. PASETO (Platform-Agnostic SEcurity TOkens) legt die Kryptografie pro Protokollversion fest, sodass ein v4-Token vom Typ public immer Ed25519 verwendet und ein v4-Token vom Typ local immer XChaCha20 mit BLAKE2b.
Auf einen Blick
| JWT (JWS) | PASETO v4 | |
|---|---|---|
| Spezifikation | RFC 7519 mit RFC 7515/7518 (IETF-Standards) | Community-Spezifikation (paseto-standard/paseto-spec); IETF-Entwürfe abgelaufen |
| Format | base64url(header).base64url(claims).base64url(signature) | v4.public. + base64url(Nachricht ‖ Signatur), optional .footer |
| Algorithmuswahl | Header alg aus der IANA-Registry | Durch Version und Zweck festgelegt |
| Signaturen mit öffentlichem Schlüssel | ES256, RS256, PS256, EdDSA/Ed25519 und weitere | v4.public: nur Ed25519 (v3.public: ECDSA P-384) |
| Symmetrische Tokens | HS256 (nur MAC, lesbare Claims) oder JWE | v4.local: XChaCha20 + BLAKE2b-MAC (verschlüsselt und authentifiziert) |
none und Algorithm Confusion | Möglich bei falsch konfiguriertem Verifizierer | Nicht ausdrückbar |
| Implizite Assertions (authentifiziert, nicht übertragen) | Nein | Ja (v3 und v4) |
| Beispielgröße (unten gemessen) | 227 Zeichen | 280 Zeichen |
| Schlüsselformat | JWK und JWKS | PASERK (k4.public…, k4.local…) |
| Ökosystem | Jeder IdP, OAuth/OIDC, Gateways, Cloud-Dienste | Bibliotheken in vielen Sprachen; kaum Unterstützung durch IdPs oder Gateways |
Unterschiede im Design
Wer ein JWT prüft, sieht einen Header, der den Algorithmus nennt, und muss entscheiden, ob er ihm vertraut. RFC 7518 registriert über ein Dutzend Signatur- und MAC-Algorithmen plus none, und Bibliotheken verzweigten früher anhand des Headers. Das führte 2015 zu einer Welle von Bugs: Unsignierte Tokens wurden akzeptiert, und öffentliche RSA-Schlüssel dienten als HMAC-Secrets (siehe HS256 vs. RS256). RFC 8725 behebt das mit Konfigurationsregeln, die jede moderne Bibliothek unterstützt, anwenden muss sie aber jedes System selbst.
PASETO ersetzt die Aushandlung durch Versionen. Jede Version legt pro Zweck ein Primitiv fest: v4 nutzt Ed25519 für Tokens vom Typ public und XChaCha20-Verschlüsselung mit einem schlüsselabhängigen BLAKE2b-MAC für Tokens vom Typ local; v3 nutzt NIST-Algorithmen (ECDSA P-384, AES-256-CTR mit HMAC-SHA-384) für Umgebungen, die sie verlangen. Ein Verifizierer, der für v4-Tokens vom Typ public konfiguriert ist, lässt sich zu nichts anderem verleiten, und Schlüssel sind typisiert, sodass sich ein Schlüssel für local nicht als Schlüssel für public verwenden lässt. Das Pre-Authentication Encoding (PAE) bindet Version, Zweck, Payload, Footer und optionale implizite Assertions, etwa eine Mandantenkennung, die beide Seiten kennen, die das Token aber nicht mitführt.
Zwei weitere Unterschiede zählen in der Praxis. Die symmetrischen Tokens vom Typ local sind bei PASETO verschlüsselt, symmetrische JWTs mit HS256 dagegen nur per MAC geschützt und lesbar; das Gegenstück zu v4.local ist bei JWT ein JWE. Außerdem hat PASETO kein Gegenstück zur JWKS-Discovery, die Schlüsselverteilung liegt also bei dir.
Direkter Vergleich
Das folgende Skript, ausgeführt unter Node.js 26.10 mit paseto 3.1.4 und jose 6.2.13, stellt dieselben Claims mit einem einzigen Ed25519-Schlüssel in beiden Formaten aus.
import { generateKeyPairSync } from "node:crypto";
import { SignJWT, jwtVerify } from "jose";
import { V4 } from "paseto";
const claims = { sub: "user-42", scope: "orders:read" };
const { privateKey, publicKey } = generateKeyPairSync("ed25519");
// PASETO v4.public: Ed25519 is implied by the version; no alg header exists.
const paseto = await V4.sign(claims, privateKey, { expiresIn: "15m", issuer: "https://auth.example.com" });
const pasetoClaims = await V4.verify(paseto, publicKey, { issuer: "https://auth.example.com" });
// JWT with the same key: the verifier must pin the algorithm itself.
const jwt = await new SignJWT(claims).setProtectedHeader({ alg: "EdDSA" }).setIssuer("https://auth.example.com").setExpirationTime("15m").sign(privateKey);
const { payload } = await jwtVerify(jwt, publicKey, { algorithms: ["EdDSA"], issuer: "https://auth.example.com" });
console.log("PASETO prefix:", paseto.slice(0, 10), "length:", paseto.length, "sub:", pasetoClaims.sub);
console.log("JWT prefix:", jwt.slice(0, 10), "length:", jwt.length, "sub:", payload.sub);
PASETO prefix: v4.public. length: 280 sub: user-42
JWT prefix: eyJhbGciOi length: 227 sub: user-42
Das PASETO ist hier etwas länger, weil die Bibliothek iat hinzufügt und Zeitstempel nach ISO 8601 verwendet, während das JWT numerische Datumswerte nutzt; die Signaturen sind gleich groß. Beachte die Option algorithms: ["EdDSA"] auf der JWT-Seite: Genau diese eine Zeile ist mit PASETO überflüssig.
Ökosystem und Interoperabilität
JWT steckt in OAuth 2.0 (Access Tokens nach RFC 9068), in ID-Tokens von OpenID Connect, in DPoP, SD-JWT, FAPI, in Workload-Identitäten in der Cloud und in praktisch jedem API-Gateway und Identity Provider. Bibliotheken gibt es für jede Sprache, und sie werden seit Jahren auf Sicherheit geprüft.
Das Projekt PASETO Standard pflegt die Spezifikation von PASETO auf GitHub; frühere Internet-Drafts der IETF liefen ab, ohne zu RFCs zu werden. Bibliotheken gibt es für viele Sprachen, aber Qualität und Abdeckung der Versionen schwanken, Gateways und Identity Provider akzeptieren PASETO selten, und es gibt keinen standardisierten Discovery-Mechanismus. Wählst du PASETO, verantwortest du deshalb Aussteller, Verifizierer und Schlüsselverteilung selbst.
Wann welches Format passt
- OAuth oder OpenID Connect, Empfänger bei Dritten, API-Gateways: JWT, mit einer Allowlist der Algorithmen pro Schlüssel.
- Interne Service-Tokens in einem polyglotten System, das du in der Hand hast: beides; PASETO, wenn es für jede Sprache eine gepflegte v4-Bibliothek gibt.
- Verschlüsselte Tokens, die Clients speichern (Sitzungsdaten in einem Cookie): PASETO
v4.localist einfacher als JWE; bei JWT nimm JWE mitdiroderA256KWundA256GCM. - Umgebungen mit FIPS-Vorgaben: JWT mit ES256 oder PS256 oder PASETO v3; XChaCha20 und BLAKE2b aus v4 sind nicht nach FIPS zugelassen.
- Teams, für die Token-Sicherheit neu ist: PASETO beseitigt eine ganze Klasse von Konfigurationsfehlern; eine JWT-Bibliothek mit sicheren Standardeinstellungen (etwa
jose, das verlangt, dass Algorithmen zu den Schlüsseltypen passen) verkleinert den Abstand.
Hinweise zur Migration
- Mische die Formate nicht hinter einem Verifizierer: Leite anhand des Präfixes (
v4.public.gegenübereyJ) auf getrennte Codepfade zur Prüfung mit getrennten Schlüsseln weiter. - Bilde Claims explizit ab: PASETO reserviert
iss,sub,aud,exp,nbf,iat,jtiundkidmit Datumsangaben nach ISO 8601; JWT verwendet Sekunden als NumericDate. - Ersetze JWKS durch eine eigene Schlüsselverteilung (PASERK-Strings, Konfigurationsmanagement oder ein Schlüsseldienst), einschließlich Rotation über
kidim PASETO-Footer. - Behalte JWT an den Außengrenzen: Viele Systeme stellen JWTs für externe Parteien aus und PASETO intern.
- Wenn du bei JWT bleibst, wende RFC 8725 jetzt an: Lege die Algorithmen fest, prüfe
typ,issundaud, verwende 256-Bit-HMAC-Secrets oder asymmetrische Schlüssel, und lehnenoneab.