All Insights
technical· 8 min read

Stop Using JWT for Sessions

I see this mistake in 70% of the codebases I audit. Here's what to do instead.

SV
Sri VardhanJul 22, 20248 min

JWTs are an excellent token format and a terrible session mechanism. The pattern of stuffing user state in a JWT and skipping the session table is one of the most common security antipatterns I see in startup code.

Almost every codebase I audit has the same auth pattern. User logs in, server signs a JWT containing user id, roles, expiry. Client stores it. Every request, server verifies the signature, trusts the claims, moves on.

It's elegant. It's stateless. It scales horizontally with no shared session store.

It's also broken in three serious ways.

Failure 1: You can't revoke

A user resets their password because their account was compromised. The attacker still has a valid JWT with 24 more hours of life. The signature verifies. The expiry hasn't passed. There is nothing in your stack that knows this token should be dead.

The standard "fix" is to keep a deny list of revoked tokens in Redis. Congratulations, you now have session storage. You did not save anything.

Failure 2: Stale claims

You signed a JWT 10 minutes ago with role: admin. The user has just been demoted. Their JWT still says admin for the next 23 hours and 50 minutes. Your access control reads the claim and lets them through.

The "fix" is short-lived JWTs (5 minutes) plus refresh tokens. Now every 5 minutes, every client hits your refresh endpoint, which has to check the database to confirm the user still exists and still has the role. You did not save anything.

Failure 3: Token theft is forever

A JWT in localStorage is one XSS away from being stolen. Once it's stolen, the attacker has it until expiry. There is no equivalent of "log out all other sessions". There is no way to detect that the same token is being used from two devices simultaneously, because there's no server-side state to compare against.

Real session systems detect this. JWTs do not.

What to do instead

Use opaque session tokens. Store sessions in your existing database. It is genuinely fine.

sessions(
 id: text primary key,
 user_id: bigint,
 created_at: timestamptz,
 last_used_at: timestamptz,
 user_agent: text,
 ip: inet,
 revoked_at: timestamptz null
)

A session lookup is a single primary-key read on Postgres. On a properly indexed table, that's well under 1ms. With a Redis cache in front, sub-millisecond. You will not bottleneck on this until you have hundreds of thousands of QPS, at which point you will have other architectural advantages to lean on.

The session id goes in an HTTP-only, Secure, SameSite=Lax cookie. Not localStorage. Not a header. A cookie.

When JWTs actually make sense

JWTs are great for:

  • Short-lived service-to-service tokens in a microservice mesh, where you control issuance and the lifetime is measured in seconds.
  • One-time signed URLs like password reset links or magic logins.
  • Federation between systems that don't share a database, like OAuth issuer to resource server.

In all of those cases, the value is the signed payload, not session continuity. Use them there.

The banking lens

In every regulated bank I've worked in, the answer to "should this be a JWT or a session" was non-negotiable. Sessions, server-side, with full audit. The reason is simple: when fraud or compliance asks "was this user logged in at 2:14am on March 4?", you need an authoritative answer. A signed token in someone's browser is not an authoritative answer.

If your product handles money, health data, or anything regulated, you will eventually need to answer that question. Build the session table now.

The sharper insight

The reason JWT-as-session became popular is that it makes scaling easier on day one and harder forever after. Session tables are slightly more work upfront and dramatically less work over a five year lifecycle. If you're building something you intend to keep running, optimize for the five year version.

I cover this and three other auth antipatterns in detail during a typical architecture audit. If you want to discuss your stack specifically, reach out.

References

securityauthjwtsessions

Want to discuss this topic?

I'm always happy to dive deeper. Reach out if you have questions or want to collaborate.

Get in Touch

Command Palette

Search for a command to run...