Engineering ThoughtApril 2026

#LearnSecurity: PASETO vs JWT

Alternative to JWT, Sessions, Cookies, OAuth — Exploring PASETO with practical examples and RFC specs.

PASETO Token Security

// PASETO Token Security

Authentication is one of those things every backend developer has to deal with. We've all been there — staring at the screen wondering whether to use Sessions, JWT, or maybe just roll our own (please don't).
Recently I stumbled into PASETO (Platform-Agnostic Security Tokens) while reading through some RFC drafts. It's been around for a few years but still flies under the radar for many developers. Let me break down what I learned.

###🔐 What Problem Are We Solving?

Before PASETO, we had a few options for stateless auth:
MethodHow It WorksThe Catch
SessionsStore data server-side, send session ID in cookieServer memory, doesn't scale horizontally easily
JWTEncode claims in Base64, sign with algorithmAlgorithm confusion attacks, readable payloads, too many crypto choices
CookiesSimple key-value storageNot really auth, just storage
OAuthDelegate auth to third partyOverkill for internal services, complex flow
PASETO sits in the same space as JWT — stateless, self-contained tokens — but tries to fix the footguns.

###📝 JWT vs PASETO: The Payload Difference

Here's where it gets interesting. Let's look at a real payload comparison.
JWT (JSON Web Token):
snippet.json
// Header
{
  "alg": "HS256",
  "typ": "JWT"
}

// Payload (just Base64 encoded — anyone can read this!)
{
  "sub": "1234567890",
  "name": "Aldo Chandra",
  "admin": true,
  "iat": 1516239022
}

// Signature
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret
)
The full JWT looks like:
snippet.text
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFsZG8gQ2hhbmRyYSIsImFkbWluIjp0cnVlLCJpYXQiOjE1MTYyMzkwMjJ9.SflKxwRJSMeKKF2QT4fwpMe...
Problems I see: 1. That payload is just Base64 — paste it into jwt.io and anyone can read it 2. The alg header can be tampered with (famous alg: none attack) 3. I have to choose the algorithm — HS256? RS256? ES256?

PASETO (Platform-Agnostic Security Token):
PASETO has two modes: local (encrypted) and public (signed).
PASETO Local Mode (v4):
snippet.text
v4.local.E1Y_KQ6Ek8lSOKrJ6kI1YjWXfAKJ0OEcdhUPywznjBjK5SGDUr4...
What's inside? Encrypted. You can't read it without the shared secret. The structure is:
snippet.text
version.purpose.payload.footer
  • v4 = Version 4 (uses XChaCha20-Poly1305 for encryption)
  • local = Symmetric encryption mode
  • payload = Encrypted claims
  • footer (optional) = Unencrypted metadata (like key ID)
PASETO Public Mode (v4):
snippet.text
v4.public.eyJleHAiOiIyMDIzLTA1LTEwVDExOjQ2OjA0KzA5OjAwIiwiaWF0...
  • public = Signed with Ed25519 (asymmetric)
  • Anyone can verify with the public key
  • Only the private key holder can create valid tokens

###🛡️ Why I Started Paying Attention

1. No Algorithm Confusion JWT's algorithm is in the header. Change HS256 to none and some broken implementations accept it. PASETO bakes the algorithm into the version — you can't change it.
2. Encrypted by Default (Local Mode) With JWT, the payload is public. With PASETO local mode, it's encrypted. If I'm passing internal user IDs or permissions, I'd rather they weren't visible to anyone who intercepts the token.
3. Simple Choices JWT: "Pick your algorithm, pick your key size, configure it right" PASETO: "Pick local or public. Done."

###💻 Practical Example

Here's how you'd actually use PASETO in Node.js/Bun:
snippet.ts
import { V4 } from 'paseto';

// Local mode (symmetric encryption)
const localKey = await V4.generateKey('local');

const token = await V4.encrypt(
  { userId: '123', role: 'admin' },
  localKey,
  { expiresIn: '2 hours' }
);
// v4.local.xxx...

const payload = await V4.decrypt(token, localKey);
// { userId: '123', role: 'admin', exp: '2024-01-15T10:00:00Z' }
Compare to JWT:
snippet.ts
import jwt from 'jsonwebtoken';

// Have to choose algorithm, manage secrets carefully
const token = jwt.sign(
  { userId: '123', role: 'admin' },
  process.env.JWT_SECRET,
  { expiresIn: '2h', algorithm: 'HS256' }
);

// Payload is readable by anyone!

###📚 The Spec

PASETO isn't just a library — it's a specification. The RFC (currently in draft) defines:
  • v1: NIST-compliant algorithms (RSA-PSS, AES-256-CTR, HMAC-SHA384)
  • v2: libsodium-based (Ed25519, XChaCha20-Poly1305)
  • v3: NIST PQC-ready algorithms
  • v4: Modern libsodium (Ed25519, XChaCha20-Poly1305) — recommended

###🚀 My Implementations

I built two boilerplates to experiment with PASETO in real projects:
A minimal REST API with PASETO local mode. Good starting point if you want to see auth middleware, token refresh, and protected routes working together.
Same idea but with GraphQL. Shows how to handle authentication in a GraphQL context using PASETO for context/authorization.
Both use PASETO v4 and demonstrate the "local" mode for encrypted session tokens.

###🤔 When to Use What?

ScenarioApproachConsideration
Internal microservicesPASETO localFast, encrypted, shared secret
Public API for clientsPASETO publicSign with private key, verify with public
Browser sessionsSessions + CookiesSimple, battle-tested
Third-party authOAuth 2.0Standard, users expect it
Quick prototypeJWTEcosystem maturity, but be careful

###🌟 Bottom Line

JWT isn't going anywhere. The ecosystem is huge and it works fine if you're careful. But PASETO removes entire categories of mistakes — algorithm confusion, weak crypto choices, accidentally exposing payload data.
For new projects where I control the whole stack? I'm reaching for PASETO. The peace of mind is worth the smaller ecosystem.
For existing JWT implementations? Probably not worth refactoring unless you're already touching auth code.

Got questions or want to see more backend patterns? I post experiments and boilerplate project for learn on GitHub.
Happy Coding!