Home → Blog → Auth Token Comparison
JWT vs Session Cookies vs PASETO vs Biscuit — Auth Token Comparison (2026)
"Just use JWT" became the default authentication advice for web applications in the late 2010s, and the consequences are still being felt. JWT is genuinely useful for some scenarios — and an actively bad choice for others. Meanwhile, alternatives that addressed JWT's footguns have matured: PASETO removed the algorithm-confusion problem at the protocol level, Biscuit added offline attenuation, and server-side session cookies — the option JWT was supposed to replace — remain the simplest secure default for the vast majority of web applications. This article compares the four most relevant token approaches in 2026: JWT, server-side session cookies, PASETO, and Biscuit. The recommendation at the end is opinionated and grounded in the security literature.
Decode a JWT to follow along
Paste any JWT into the decoder to see header, payload, and signature. Examples in this article are decodable inline.
Open JWT Decoder →The Comparison at a Glance
| Property | JWT | Server-side session | PASETO | Biscuit |
|---|---|---|---|---|
| Stateless (no server lookup) | Yes | No | Yes | Yes |
| Easy revocation | No (or stateful) | Yes | No (or stateful) | Partial |
| Algorithm-confusion attacks possible | Yes (alg header) | N/A | No (versioned) | No |
| Confidentiality of claims | No (just base64) | Yes (server-only) | v*.local: Yes | No |
| Offline attenuation / delegation | No | No | No | Yes |
| Storage in browser | Cookie / localStorage | HttpOnly cookie (best) | Cookie / localStorage | Cookie / localStorage |
| Library maturity (Node) | Excellent | Excellent | Good | Limited |
| Cross-language support | Excellent | N/A (server-internal) | Good | Limited |
| RFC / standard | RFC 7519 | De facto | PASETO spec | Biscuit spec |
Server-Side Session Cookies
The traditional approach: when a user logs in, the server creates a session record (in Redis, in a database, or in memory), generates an opaque random session ID, and sets that ID in an HttpOnly cookie. On every request, the server looks up the session record by ID.
// Express + express-session
import session from "express-session";
app.use(session({
secret: process.env.SESSION_SECRET,
cookie: { httpOnly: true, secure: true, sameSite: "lax", maxAge: 86400000 },
store: new RedisStore({ client: redis }),
resave: false,
saveUninitialized: false,
}));
Strengths: Trivial revocation (delete the session row). All session data lives server-side, so claims can't be tampered with or read by clients. The cookie is opaque — leaking it doesn't expose user data. HttpOnly + Secure + SameSite cookies are the safest place to store credentials in a browser.
Weaknesses: Requires a session store (Redis or database). Doesn't work across different domains or for stateless microservices. Doesn't work for native mobile clients without extra work (cookie management is awkward outside the browser).
JWT (JSON Web Token)
JWT is a self-contained token: the user's claims (id, role, expiry) are encoded into the token itself, signed by the server. The server validates the signature on every request without consulting a session store.
// Three base64url parts joined by dots
header.payload.signature
// Example payload (decoded):
{
"sub": "user_123",
"role": "admin",
"iat": 1714780000,
"exp": 1714783600
}
Strengths: Stateless. Validation needs only the public key (RS256/ES256/EdDSA) or shared secret (HS256). Works naturally across services, domains, and native clients. Standard format with libraries in every language.
Weaknesses (security):
- alg:none attacks — historically, naive validators accepted tokens with
alg: noneas valid. Most libraries fixed this years ago, but the footgun is in the spec. - Algorithm confusion — if a server uses RS256 (asymmetric) but the validator allows HS256 (symmetric), an attacker can sign a token with the public key as the HMAC secret.
- Claims are visible — the payload is base64-encoded, not encrypted. Anyone with the token can read the claims.
- Revocation is hard — a stolen JWT is valid until expiry. Either accept long-lived security exposure or implement a revocation list (which defeats statelessness).
- Storage in the browser — putting JWT in localStorage exposes it to XSS. Putting it in a cookie negates the cross-domain advantage and you should have just used a session.
If you must use JWT, do this
- Use asymmetric signatures (RS256, ES256, or EdDSA) — never HS256 with a shared secret across services.
- Validate the algorithm explicitly in your verifier, do not let the token decide.
- Set short access-token lifetimes (5–15 min). Pair with a server-side refresh-token store.
- Store the access token in an HttpOnly cookie if it lives in a browser. localStorage exposes it to any XSS.
- Validate every claim:
iss,aud,exp,nbf,sub. - Never put sensitive data in the payload — it is not encrypted.
- Use a decoder to inspect tokens during development. Never trust unverified claims.
PASETO (Platform-Agnostic Security Tokens)
PASETO is JWT's successor, designed by security researchers (notably Scott Arciszewski) to remove JWT's cryptographic footguns. The core idea: tokens have a version that fixes the algorithm. There is no alg header to manipulate.
// PASETO v4.public token (Ed25519 signed, claims visible like JWT)
v4.public.eyJzdWIiOiJ1c2VyXzEyMyIsImV4cCI6IjIwMjYtMDUtMDNUMTI6MDA6MDBaIn0.aBcDeFg...
// PASETO v4.local (XChaCha20-Poly1305 encrypted, claims confidential)
v4.local.RnB1RQg...
The format is similar to JWT (header.payload.signature) but with critical differences:
- Version determines the algorithm. v4.public = Ed25519. v4.local = XChaCha20-Poly1305. No negotiation, no choice.
- Two purposes:
localtokens are encrypted (claims hidden),publictokens are signed (claims visible). - No alg:none, no algorithm confusion.
- Same self-contained model as JWT — stateless validation.
Strengths: Eliminates the most common JWT vulnerabilities at the protocol level. Local tokens give you payload confidentiality if you need claims hidden from clients. Spec is opinionated and discourages footguns.
Weaknesses: Smaller ecosystem than JWT. Library quality varies. Same fundamental revocation problem as JWT. Most identity providers (Auth0, Cognito, Okta) speak JWT, not PASETO — interop may force you back to JWT.
Biscuit
Biscuit is a 2021-era token format with a unique feature: offline attenuation. A token holder can derive a more restricted token without contacting the server. The new token cannot grant more permissions than the original — only less.
This solves a real problem: delegation. If your user wants to grant a third-party tool read-only access to a single project, the user can attenuate their existing token offline rather than going through an OAuth dance.
Biscuit also has a Datalog-like policy language baked into the token, so authorization decisions can be made at the API gateway without consulting the application.
Strengths: Offline attenuation is genuinely novel. Useful for capability-style auth, delegated access, microservice mesh authorization. Ed25519 by default — no algorithm-choice footgun.
Weaknesses: Smallest ecosystem of the four — primarily Rust, with bindings to other languages. Limited documentation outside the spec. Most teams don't actually need offline attenuation; if you don't, the complexity isn't worth it.
Decision Tree
- Is this a single-domain web application with a server you control? → Server-side session cookies. The simplest secure choice.
- Is this a microservices architecture where services can't share a session store? → JWT or PASETO. Prefer PASETO if the team can adopt it; JWT if you need broad ecosystem compatibility (especially with identity providers).
- Are you integrating with Auth0, Cognito, Firebase Auth, Clerk, or another identity provider? → JWT. The provider speaks JWT; using anything else means re-implementing auth.
- Do you need delegation / offline attenuation as a first-class feature? → Biscuit, or build a custom token format. Most teams do not need this.
- Do you need claims confidentiality (the client should not see the contents)? → PASETO v*.local, or a server-side session, or encrypt the JWT payload separately (uncommon).
- Is performance a real concern (10k+ req/sec)? → JWT or PASETO with cached public-key validation. Server-side sessions add a Redis round-trip per request.
The Honest Recommendation for 2026
- Default for new web applications: server-side sessions with HttpOnly+Secure+SameSite cookies. Boring, well-understood, easy to revoke. The "JWT for everything" advice from the late 2010s was overcorrected; sessions remain the simpler choice for most use cases.
- For microservices or third-party API authentication: JWT. Pair with short-lived access tokens + server-side refresh tokens for revocation.
- If you have the freedom to choose and the team is comfortable with newer tooling: PASETO. Fewer footguns, same stateless model.
- If you're building a system with explicit delegation requirements: evaluate Biscuit.
- If you're stuck with JWT (you probably are at some level): use asymmetric signing, validate algorithm explicitly, keep tokens short-lived, store in HttpOnly cookies, and never trust unverified claims.
Storage Pitfalls in the Browser
Whatever token format you choose, the storage decision in the browser is critical:
| Storage | XSS-readable? | CSRF-vulnerable? | Recommended? |
|---|---|---|---|
| localStorage | Yes (any script) | No | No (avoid for credentials) |
| sessionStorage | Yes | No | No (avoid for credentials) |
| Cookie (HttpOnly + Secure + SameSite=Strict/Lax) | No | Mitigated by SameSite | Yes (best for browsers) |
| In-memory JS variable | Yes (page-level) | No | OK for short-lived in-page state |
Putting a JWT in localStorage so you can send it as a Bearer header sounds clean but is a regression in security from a session cookie. If your app runs in a browser, you almost always want HttpOnly cookies — at which point much of the JWT story is just adding cryptographic signatures to data the server could have stored in Redis.
Common Misconceptions
"JWT is encrypted"
Almost always false. The default JWT (JWS) is signed, not encrypted. Anyone who has the token can base64-decode it and read every claim. JWE provides encrypted JWT but is rare and complex; PASETO v*.local is the cleaner option if you need confidentiality.
"JWT is faster than sessions"
Only sometimes. JWT validation is a signature check (microseconds with ECDSA). Session validation is a Redis lookup (sub-millisecond on a local network). For a single request, the difference is rounding error. For 100k req/sec on a global edge with no Redis nearby, JWT wins. For a typical web app, the performance difference is irrelevant.
"JWT solves single sign-on"
Not by itself. SSO is solved by OAuth 2.0 and OIDC, which often use JWT as the access token format but also include flows for issuance, consent, and revocation. JWT alone is a token format, not an SSO protocol.
"JWT can't be stolen because it's signed"
Wrong. Signing prevents tampering, not theft. A stolen JWT is valid until it expires. This is exactly why short expiry + refresh tokens is essential, and why server-side sessions (which can be revoked instantly) remain attractive.
Practical Tooling
Whichever token format you settle on, you'll spend time inspecting and debugging tokens:
- JWT Decoder — paste any JWT to see header, payload, signing algorithm, and claim expiry status.
- JWT Tutorial — start-from-zero introduction to JWT with worked examples.
- paseto.io — PASETO specification and library list.
- biscuitsec.org — Biscuit specification and Rust implementation.
- RFC 7519 — the canonical JWT specification.
- RFC 8259 — the JSON spec underlying JWT/PASETO payload encoding.
Decode a JWT in your browser
Paste any JWT to see the header, payload claims, and signature breakdown. Tokens never leave your browser.
Open JWT Decoder →Frequently Asked Questions
For most web applications, server-side session cookies are the simpler, more secure default. They support easy revocation and integrate naturally with framework session middleware. Use JWT for genuine stateless requirements like microservices that cannot share a session store.
JWT can be secure when used correctly: short expiry, asymmetric signatures (RS256/ES256/EdDSA, not HS256), no sensitive data in the payload, and proper validation of every claim. Most JWT vulnerabilities involve algorithm confusion, weak keys, or trusting unverified claims.
PASETO is an alternative to JWT designed to remove cryptographic foot-guns. Instead of letting the token specify its algorithm (which led to algorithm-confusion attacks against JWT), PASETO has fixed versions: each version specifies exactly one algorithm. There is no alg:none and no algorithm-substitution attack.
Not without compromising statelessness. Common approaches: maintain a server-side blocklist (defeats most performance benefits), or keep access tokens very short-lived (5–15 minutes) with longer-lived refresh tokens stored server-side. Server-side sessions remain simpler when revocation is critical.
Biscuit is a 2021-era token format that supports offline attenuation: a holder can derive a more restricted token from it without contacting the server. Useful for delegation patterns. Adoption is still limited compared to JWT and PASETO.