JWT is Fine, Until it Isn't: A Case for PASETO Tokens
securityjwtpasetoauthtokenstypescript

JWT is Fine, Until it Isn't: A Case for PASETO Tokens

Understand how JWT works, where it falls short, and why PASETO tokens offer a more secure and opinionated alternative for modern web applications.

## Before JWT, There Were Sessions

Most web apps used to handle auth with server-side sessions. You log in, the server creates a session, stores it somewhere, and hands you back a session ID in a cookie. Every request, the server looks it up and figures out who you are.

It worked. But it had one big problem: state. Every server in your fleet needed access to the same session store. Scaling horizontally meant sticky sessions or a shared Redis instance. As microservices and APIs became the norm, dragging a session store across every service started feeling like the wrong approach.

Something had to change.

## Why JWT Came Along

JSON Web Tokens (JWT) were introduced as a stateless alternative. Instead of the server remembering you, it hands you a signed token with everything it needs to know about you baked right in. No lookups. No shared state.

A JWT is three Base64URL-encoded parts joined by dots — a header, a payload, and a signature. The header declares the token type and the signing algorithm. The payload holds your claims — user ID, roles, expiry. The signature lets the server verify nothing was tampered with.

### How it works

  1. You log in with your credentials.
  2. The server verifies them and signs a JWT using a secret key or private key.
  3. The token comes back to you — typically in the response body or an HttpOnly cookie.
  4. On every request, you send the JWT along.
  5. The server verifies the signature and reads the payload directly. No database call needed.

Clean, portable, works great across services and domains. It became the default for SPAs, mobile apps, and microservices almost overnight.

## The Cons of JWT

JWT is everywhere, but it carries some real baggage.

  • Algorithm confusion attacks: The header declares which algorithm was used to sign the token — and many libraries trust that header blindly. The classic exploit swaps RS256 (asymmetric) to HS256 (symmetric), then signs the token using the server's public key as the HMAC secret. Some libraries accepted it. This is not theoretical — it has happened in production.

  • The alg: none problem: The JWT spec originally allowed none as a valid algorithm value, meaning no signature at all. Some early libraries accepted these unsigned tokens as valid. Most have since patched this, but the fact that it was ever in the spec is a red flag.

  • Too many algorithm choices: JWT lets you pick from HS256, HS384, RS256, RS512, ES256, PS256, and more. That freedom is dangerous when a developer defaults to a weak algorithm or a library silently picks one for them.

  • No built-in encryption: By default, JWT only signs the payload — it does not encrypt it. Anyone who intercepts the token can Base64-decode it and read your claims. You need JWE (JSON Web Encryption) on top for actual confidentiality, which adds even more complexity.

  • Revocation is painful: Because JWTs are stateless, there is no native way to invalidate one before it expires. If a token is compromised or a user logs out, you either wait it out or build a blocklist — which brings back the state you were trying to avoid.

## Enter PASETO

PASETO stands for Platform-Agnostic Security Tokens. It was built to fix the footguns in JWT. The core idea is simple: remove dangerous choices from the developer.

Where JWT lets you pick your algorithm and get it wrong, PASETO locks the algorithm per version. No negotiation, no algorithm confusion, no none.

PASETO tokens come in two types.

### local — Symmetric Encryption

Both parties share a secret key. The token payload is fully encrypted using XChaCha20-Poly1305 (in v4), so even if someone intercepts it, they see nothing but ciphertext.

### public — Asymmetric Signing

The server signs the token with its Ed25519 private key. The payload is readable (like JWT), but the signature guarantees it was issued by your server. Anyone holding the public key can verify it.

## How PASETO Works

### Issuance

  1. The user authenticates.
  2. The server builds the payload — user ID, roles, expiry, any custom claims.
  3. For local tokens: the server encrypts the payload with the shared secret. The payload becomes ciphertext.
  4. For public tokens: the server signs the payload with its Ed25519 private key. The payload stays readable, the signature proves authenticity.

### Verification

For local tokens, only a party with the same shared secret can decrypt and read the payload. For public tokens, any service holding the server's public key can verify the signature and read the claims — no shared secret needed.

Here is what a basic public token flow looks like in Node.js using the paseto library:

import { V4 } from 'paseto';
 
// Issuance — runs on your server at login
const { privateKey, publicKey } = await V4.generateKey('public', {
  format: 'paserk',
});
 
const token = await V4.sign(
  { sub: user.id, role: user.role, exp: '2h' },
  privateKey,
);
 
// Verification — runs on your server per request
const payload = await V4.verify(token, publicKey);
console.log(payload.sub); // your user ID

### Token Storage

PASETO tokens are strings, just like JWTs. The storage rules are the same:

  • HttpOnly cookies: The safest option for web apps. Not accessible from JavaScript, so immune to XSS. Pair with SameSite=Strict or Lax.
  • In-memory state: Fine for short-lived tokens in SPAs, but lost on page refresh.
  • localStorage: Convenient but exposed to XSS. Avoid for anything sensitive.

For most apps, HttpOnly cookies is the right default.

## Should You Switch?

If you are starting a new project, PASETO v4 is worth defaulting to. The opinionated design means fewer decisions and fewer ways to misconfigure something critical.

If you have a working JWT setup, a full rewrite is not urgent. Just make sure your library enforces strict algorithm allowlists and you are not rolling your own validation logic.

The goal either way is the same: tokens your server issues, your server can trust, and your users cannot forge.

## Conclusion

JWT solved a real problem and is still widely used for good reason. But it gave developers too much freedom in the wrong places, and that has caused real vulnerabilities over the years.

PASETO keeps what works — stateless, portable tokens — and removes what does not. If you care about building secure auth without a long checklist of things to get right, it is worth knowing.

Want to see how this fits into a full-stack auth setup with Next.js? I am writing about that next. Follow along on LinkedIn or check out more at heykaran.dev.

Karan Singh
Written by Karan Singh

Full-stack developer and UI designer specializing in React, Next.js, TypeScript, and AI-powered web experiences. Building modern, performant applications from India.

Related Posts

Stop Paying for Resend: BetterAuth + Nodemailer Integration Guide

Stop Paying for Resend: BetterAuth + Nodemailer Integration Guide

How to replace Resend with Nodemailer in your Next.js application for free, unlimited email verification and password resets.

nextjsbetterauthnodemailer+2
Read More
Karansingh

Design & Developed by Karan Singh

© 2026. All rights reserved

Command Palette

Search for a command to run...