Skip to content
ecdsa.com

JWT vs PASETO: flexible standard or fixed-cryptography tokens

PASETO removes the alg header and the choices that caused most JWT vulnerabilities. JWT keeps the ecosystem: identity providers, standards and libraries in every language.

JWT vs PASETO is a trade between flexibility and misuse resistance. JWT (RFC 7519, usually a JWS) lets the token name its algorithm from a long registry, which allowed classic bugs such as alg: none and RS256-to-HS256 confusion. PASETO (Platform-Agnostic SEcurity TOkens) fixes the cryptography per protocol version, so a v4 public token is always Ed25519 and a v4 local token is always XChaCha20 with BLAKE2b.

At a glance

JWT (JWS)PASETO v4
SpecificationRFC 7519 with RFC 7515/7518 (IETF standards)Community specification (paseto-standard/paseto-spec); IETF drafts expired
Formatbase64url(header).base64url(claims).base64url(signature)v4.public. + base64url(message ‖ signature), optional .footer
Algorithm selectionalg header from the IANA registryFixed by version and purpose
Public-key signingES256, RS256, PS256, EdDSA/Ed25519 and morev4.public: Ed25519 only (v3.public: ECDSA P-384)
Symmetric tokensHS256 (MAC only, readable claims) or JWEv4.local: XChaCha20 + BLAKE2b-MAC (encrypted and authenticated)
none and algorithm confusionPossible if the verifier misconfiguredNot expressible
Implicit assertions (authenticated, not transmitted)NoYes (v3 and v4)
Example size (measured below)227 characters280 characters
Key formatJWK and JWKSPASERK (k4.public…, k4.local…)
EcosystemEvery IdP, OAuth/OIDC, gateways, cloud servicesLibraries in many languages; little IdP or gateway support

Design differences

A JWT verifier sees a header that names the algorithm and must decide whether to trust it. RFC 7518 registers over a dozen signature and MAC algorithms plus none, and libraries historically dispatched on the header, which produced the 2015 wave of bugs: unsigned tokens accepted, and RSA public keys used as HMAC secrets (see HS256 vs RS256). RFC 8725 fixes this with configuration rules that every modern library supports, but each deployment must apply them.

PASETO replaces negotiation with versions. Each version fixes one primitive per purpose: v4 uses Ed25519 for public tokens and XChaCha20 encryption with a keyed BLAKE2b MAC for local tokens; v3 uses NIST algorithms (ECDSA P-384, AES-256-CTR with HMAC-SHA-384) for environments that need them. A verifier configured for v4 public tokens cannot be tricked into anything else, and keys are typed so a local key cannot be used as a public key. Pre-authentication encoding (PAE) binds the version, purpose, payload, footer and optional implicit assertions, such as a tenant identifier that both sides know but the token does not carry.

Two other differences matter in practice. PASETO's symmetric local tokens are encrypted, while JWT's symmetric HS256 tokens are only MACed and readable; the JWT equivalent of v4.local is a JWE. And PASETO has no counterpart to JWKS discovery, so key distribution is up to you.

Side by side

The script below, run on Node.js 26.10 with paseto 3.1.4 and jose 6.2.13, issues the same claims with one Ed25519 key in both formats.

The same Ed25519 key and claims as PASETO v4.public and as a JWT
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);
Output
PASETO prefix: v4.public. length: 280 sub: user-42
JWT    prefix: eyJhbGciOi length: 227 sub: user-42

The PASETO is slightly longer here because the library adds iat and uses ISO 8601 timestamps, while the JWT uses numeric dates; signature sizes are identical. Note the algorithms: ["EdDSA"] option on the JWT side: that one line is what PASETO makes unnecessary.

Ecosystem and interoperability

JWT is embedded in OAuth 2.0 (RFC 9068 access tokens), OpenID Connect ID tokens, DPoP, SD-JWT, FAPI, cloud workload identity and practically every API gateway and identity provider. Libraries exist for every language, with years of security review.

PASETO's specification is maintained on GitHub by the PASETO standard project; earlier IETF Internet-Drafts expired without becoming RFCs. Libraries exist for many languages, but quality and version coverage vary, gateways and identity providers rarely accept PASETO, and there is no standard discovery mechanism. Choosing PASETO therefore means owning the issuer, the verifiers and key distribution.

When to choose which

  • OAuth or OpenID Connect, third-party consumers, API gateways: JWT, with an algorithm allowlist per key.
  • Internal service tokens in a polyglot system you control: either; PASETO if every language has a maintained v4 library.
  • Encrypted tokens stored by clients (session data in a cookie): PASETO v4.local is simpler than JWE; with JWT, use JWE with dir or A256KW and A256GCM.
  • FIPS-constrained environments: JWT with ES256 or PS256, or PASETO v3; v4's XChaCha20 and BLAKE2b are not FIPS-approved.
  • Teams new to token security: PASETO removes a class of configuration errors; a JWT library with safe defaults (such as jose, which requires algorithms to match key types) narrows the gap.

Migration notes

  1. Do not mix formats behind one verifier: route by prefix (v4.public. versus eyJ) to separate verification code paths with separate keys.
  2. Map claims explicitly: PASETO reserves iss, sub, aud, exp, nbf, iat, jti and kid with ISO 8601 dates; JWT uses NumericDate seconds.
  3. Replace JWKS with your own key distribution (PASERK strings, configuration management or a key service), including rotation by kid in the PASETO footer.
  4. Keep JWT at the edges: many systems issue JWTs to external parties and PASETO internally.
  5. If you stay with JWT, apply RFC 8725 now: pin algorithms, validate typ, iss and aud, use 256-bit HMAC secrets or asymmetric keys, and reject none.

References

All comparisons