View Categories

Authentication – JWT vs. Session Cookies

2 min read

The “Microservices” Trap #

If you start a new project today, 90% of tutorials will tell you to use JWTs (JSON Web Tokens) stored in localStorage. The reasoning usually goes: “Sessions are stateful and hard to scale. JWTs are stateless and modern.”

The Logic Check: This is one of the most dangerous security myths in modern web development. By moving session state to the client (JWT), you gain scalability, but you lose Control. Specifically, you lose the ability to Revoke Access.

If a user’s JWT is stolen, or if you ban a user from your admin panel, you cannot stop them. Their token is valid until it expires (often 15–60 minutes). In a banking app, 15 minutes is an eternity for a hacker to drain an account.

The Core Logic: The Ledger vs. The Check #

To understand the trade-off, you must understand where the “Truth” lives.

1. Session Cookies (The Ledger)

  • Logic: The user holds a meaningless ID (session_id=123). The Server holds the data (User=Admin, Expires=Tomorrow).
  • The Flow: Every time the user clicks a button, the server looks up 123 in its database (Redis/SQL).
  • The Power: If you delete key 123 from Redis, the user is instantly logged out. You have total control.

2. JWT (The Signed Check)

  • Logic: The user holds the data ({id: 1, role: "admin"}). The data is cryptographically signed by the server so it can’t be forged.
  • The Flow: The server receives the token, checks the signature, and trusts the data inside. It does not check the database.
  • The Weakness: Once you issue the “Check,” you can’t tear it up. If you demote an admin to a regular user, but they still hold a valid “Admin JWT,” they remain an admin until that token expires.

Architecture Diagram: The Validation Difference #

graph TD
    subgraph "Session (Stateful)"
    Client1[Browser] -- "Cookie: SessID=123" --> Server1[API Server]
    Server1 -- "Does 123 exist?" --> Redis[(Redis Session Store)]
    Redis -- "Yes, User is Bob" --> Server1
    Server1 -- "200 OK" --> Client1
    note1[Control: Server checks DB on every request]
    end

    subgraph "JWT (Stateless)"
    Client2[Browser] -- "Header: Bearer eyJhbG..." --> Server2[API Server]
    Server2 -- "Crypto Verify Signature" --> CPU[CPU Logic]
    CPU -- "Signature Valid (User is Bob)" --> Server2
    Server2 -- "200 OK" --> Client2
    note2[Scale: No Database Hit. Fast but Uncontrollable.]
    end

The “Revocation” Paradox #

Proponents of JWTs will argue: “If you want to ban a user, just put their JWT in a Blacklist in Redis!”

The Logic Check: Stop and think about that. If every API request now has to check Redis to see if the JWT is blacklisted… you have just re-invented Sessions. You have added the database lookup back in, but with a more complex architecture (parsing crypto + checking DB) than if you just used a session ID in the first place.

The Decision Matrix: Security vs. Scale #

FeatureSession CookiesJWT (Stateless)
RevocationInstant. Delete the key.Impossible (without complex workarounds).
Payload SizeTiny (32 bytes).Large (Contains user data + signature).
ScalabilityMedium (Needs Redis lookup).High (CPU only).
Multi-DomainHard (CORS/Cookies issues).Easy (Pass token in Header).
Mobile AppsHarder (Cookie management).Standard (easy to store).

Real-World Logic: The Hybrid Approach (Refresh Tokens) #

Most robust systems (like Auth0 or AWS Cognito) use a Hybrid Model to solve the revocation problem.

  1. Access Token (JWT): Short-lived (5 minutes). Used for API calls. Stateless.
  2. Refresh Token (Opaque String): Long-lived (7 days). Stored in the Database.

The Logic:

  • The user makes API calls with the JWT. Fast, scalable, no DB hits.
  • When the JWT expires (after 5 mins), the client sends the Refresh Token to get a new JWT.
  • The Checkpoint: This “Refresh” action does check the database. If you have banned the user, you simply delete their Refresh Token. They can finish their current 5-minute session, but they will never get a new one.
  • Trade-off: You accept a 5-minute security window in exchange for massive scalability.

Conclusion #

Default to Session Cookies for standard web applications (Monoliths, Rails, Django, Next.js). They are secure by default, HttpOnly (immune to XSS), and revocable.

Only use JWTs if:

  1. You are effectively Google scale (billions of requests where a Redis lookup is too expensive).
  2. You have multiple distinct services (Microservices) that need to trust the same user without calling a central Auth Service every time.