JWT vs PASETO vs Session-Based Auth
Are JWTs safe?
Should you switch to PASETO?
Is session-based auth outdated?
In this guide, we’ll compare modern token systems in depth with use cases, vulnerabilities, and practical advice.
The Token Confusion is Real
Developers are spoiled with options: JWT, PASETO, session-based auth, opaque tokens…
But wrong choices lead to real security issues: replay attacks, token leaks, poor scaling.
This post cuts through the noise:
-
What each system offers
-
Common myths (like “JWT is always better”)
-
When to use what with code and examples
What Are These Token Systems Anyway?
JSON Web Token (JWT)
-
Self-contained, signed token (JWS or JWE)
-
Carries user identity + claims (e.g.
sub,exp,role) -
No server-side session store needed
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
PASETO (Platform-Agnostic Security Token)
-
Like JWT, but designed to be safer by default
-
Eliminates common JWT footguns:
-
No
nonealgorithm -
Strong encryption (local/public mode)
-
v2.local.AAAAAAAAAA... (encrypted)
Session-Based Authentication
-
Server issues a session ID, stores session in memory/Redis
-
Client stores session ID in a secure HttpOnly cookie
-
Stateless = ❌ (server must track sessions)
-
Super common with Rails, Django, Express
Deep Comparison: JWT vs PASETO vs Session
| Feature | JWT | PASETO | Session-Based |
|---|---|---|---|
| Stateless | ✅ | ✅ | ❌ |
| Scalable | ✅ | ✅ | ⚠️ Needs Redis |
| Tamper-proof | ✅ (signed) | ✅ (signed/encrypted) | ✅ |
| Encrypted by default | ❌ (JWS not encrypted) | ✅ (v2.local) | ✅ |
| Revocation | ❌ (needs hacks) | ❌ (same) | ✅ Easy |
| Susceptible to replay | ✅ if not fingerprinted | ✅ if not fingerprinted | ❌ (tied to cookie/session store) |
| Complexity | Medium | High | Low |
| Security footguns | Several | Few | Few |
| Storage method | Header/localStorage | Header | Secure cookie |
Token System Comparison Flow
graph TD subgraph JWT A1[Client Logs In] --> A2[Server Issues JWT] A2 --> A3[Client Stores JWT] A3 --> A4[Client Sends JWT on Each Request] A4 --> A5[Server Verifies Signature] A5 --> A6[Access Granted if Valid] end subgraph PASETO B1[Client Logs In] --> B2[Server Issues Encrypted PASETO] B2 --> B3[Client Stores PASETO] B3 --> B4[Client Sends PASETO on Each Request] B4 --> B5[Server Decrypts and Verifies] B5 --> B6[Access Granted if Valid] end subgraph Session C1[Client Logs In] --> C2[Server Creates Session + ID] C2 --> C3[Session ID Stored in Server/Redis] C3 --> C4[Client Gets HttpOnly Cookie] C4 --> C5[Client Sends Cookie on Each Request] C5 --> C6[Server Looks Up Session ID] C6 --> C7[Access Granted if Session Valid] end
What This Shows?
| Model | Validation Location | Revocation Possible? | Token Storage |
|---|---|---|---|
| JWT | Verified by signature | ❌ No (unless using blacklist) | Header / localStorage |
| PASETO | Decrypted + verified | ❌ No (same) | Header / localStorage |
| Session | Validated server-side | ✅ Yes | HttpOnly Secure Cookie |
Security Tradeoffs You Should Know
🚨 JWT Pitfalls
-
No built-in revocation - token lives until expiry
-
Stored in
localStorage? Risk of XSS token theft -
Exposed to replay attacks if not bound to IP/device
🔐 PASETO Advantages
-
Better defaults (e.g., encrypted-by-default in
v2.local) -
Safer crypto choices (no
none, avoids RS/HS confusion) -
Libraries exist for Node, Go, Python, Rust
🛡️ Session-Based Auth Pros
-
Safe with secure HttpOnly cookies
-
Easy to revoke
-
Better suited for traditional web apps with CSRF protection
When to Use Each System
| Use Case | Recommendation |
|---|---|
| Public REST APIs with scaling needs | JWT or PASETO with short-lived tokens |
| Mobile apps / device auth | JWT/PASETO with refresh + fingerprint binding |
| Enterprise dashboards or internal portals | Session-based auth (fast, revocable) |
| High-risk environments | PASETO + rotated tokens + IP/device binding |
| Multi-device auth | Session-based or fingerprint-aware JWT |
Sample: PASETO Token in Node.js (v2.local)
npm install paseto
const { V2 } = require('paseto');
const key = await V2.generateKey('local');
const token = await V2.encrypt(
{ sub: 'user123', role: 'admin' },
key
);
// Later, verify:
const payload = await V2.decrypt(token, key);
PASETO automatically encrypts everything. You don’t need to worry about alg, exp, or iat footguns like in JWTs.
Don’t Forget Storage Strategy
| Method | Safe? | Notes |
|---|---|---|
localStorage | ❌ | Vulnerable to XSS |
sessionStorage | ⚠️ | Limited scope, still XSS-prone |
HttpOnly Secure Cookies | ✅ | Best for session or refresh token storage |
Advanced Patterns: Combine the Best of All Worlds
-
Access token + refresh token model (JWT/PASETO short-lived + session-like refresh)
-
Use fingerprinting (IP, User-Agent, device ID) for sensitive actions
-
OPA or RBAC rules to enforce access control server-side
Conclusion: No Silver Bullet, Only Smart Tradeoffs
There’s no one-size-fits-all token. Your choice should depend on:
-
Threat model (XSS? Replay? Insider access?)
-
Scaling needs (server-side sessions or stateless?)
-
App architecture (SPA vs server-rendered vs mobile)
Choose wisely and always treat token security as part of system design, not just implementation detail.
Call to Action
Which system are you using and why? Have you tried PASETO or are you still on JWTs?
Let’s break it down in the comments 👇