Your Auth Is Not Safer Because You Used JWTs
JWTs can prove that claims were signed. They cannot decide where credentials live, how logout works, or what a user may do. A token format is not an auth system.

This entry is part 1 of 5 in
Auth Is More Than Login
A practical series on XSS, CSRF, JWTs, sessions, refresh tokens, and the auth rules people skip.
Browse series posts
- 1Your Auth Is Not Safer Because You Used JWTs
1 post is available now. The planned series has 5 parts.
On this page
A lot of teams replace sessions with JWTs and somehow become more confident without becoming more secure.
The same application still has weak logout, credentials that live for days, no useful revocation story, and authorization checks scattered across controllers. But now the credentials are longer and contain three dots, so the architecture diagram looks serious.
I like JWTs. I use them. I just do not confuse a token format with an authentication system.
The Token Is Not the System
A JWT is a compact way to carry claims. In the common signed form, it lets an API verify that the token was signed by the holder of a trusted signing key and was not changed afterwards.
That is useful. It can also be useful to put a random session ID in a cookie and look it up in Redis. The right choice depends on the system, not on which option sounds more modern.
Here is the boundary I wish more auth discussions started with:
| Concern | What a JWT gives you | What the system still needs |
|---|---|---|
| Integrity | A signature over the token | Trusted keys, algorithm restrictions, and rotation |
| Identity context | Claims such as sub, iss, and aud | Account status and tenant boundaries |
| Expiry | An exp claim the verifier can reject | Sensible lifetimes and a renewal strategy |
| Theft | Nothing by default | Storage decisions and incident response |
| Logout | Nothing by default | A way to end active access |
| Authorization | Data the policy can consider | The actual policy for this resource and action |
A signed JWT is also usually encoded, not encrypted. Anyone who gets one can typically decode its header and payload. Do not put passwords, secrets, or private profile data in there and hope Base64 develops morals overnight.
JWTs solve a narrow problem well. Trouble starts when we promote that narrow solution into proof that the whole auth design is sound.
Verification Is the Useful Part
If an API accepts JWTs, it has to verify the exact kind of token it expects. Checking that the payload parses is not verification. Checking a signature without constraining the algorithm, issuer, and audience is not enough either.
With jose, a focused verifier can look like this:
import { importSPKI, jwtVerify } from "jose";
const publicKey = await importSPKI(process.env.JWT_PUBLIC_KEY!, "RS256");
export async function verifyAccessToken(token: string) {
return jwtVerify(token, publicKey, {
algorithms: ["RS256"],
issuer: "https://auth.example.com",
audience: "https://api.example.com",
requiredClaims: ["exp", "sub"],
clockTolerance: "5s",
});
}
The example uses a static public key to stay focused. An identity provider will usually publish a JWKS so keys can rotate. Resolve it from a fixed, trusted URL. Do not construct that URL from an unverified token unless you enjoy letting attackers help with configuration.
The JWT Best Current Practices RFC goes deeper on algorithm confusion, cross-JWT confusion, issuer validation, audience validation, and other ways a technically valid token can still be wrong for your application.
Even perfect verification only establishes identity context. It does not prove that sub may edit invoice 123, cross into tenant acme, or promote another user. Those are authorization decisions, and the token cannot make them for you.

The Format Argument Is a Distraction
Most session-versus-JWT debates start too late. They compare credential formats before agreeing on the system's actual requirements.
An opaque session ID normally sends the server to a trusted store for the current session state. A JWT lets a service verify signed claims without performing that lookup on every request. That can matter when several independent services need the same identity context.
It matters much less when one web application and one backend already share a database or cache.
For a server-rendered application or a monolith, I often choose an opaque session cookie. It is boring, revocable, and supported by mature frameworks. For distributed APIs, mobile clients, or services that need independently verifiable claims, JWTs can be a strong fit.
Neither choice answers the uncomfortable questions around it.

The Questions JWTs Do Not Answer
Imagine an application issues one valid bearer token for seven days and stores it somewhere malicious JavaScript can read. The signature can be flawless. The issuer and audience can be correct. One successful theft still gives the attacker a working credential.
Changing the storage mechanism creates different trade-offs. Adding refresh tokens creates another credential lifecycle. Moving credentials into cookies changes which browser protections matter. Putting roles into claims raises questions about stale permissions and account disablement.
Those are not edge cases. They are the auth system.
The rest of this series will take them one at a time:
- What token storage changes when XSS enters the threat model.
- Why cookie-authenticated routes still need a CSRF policy.
- How sessions and JWTs fit into the same enforcement checklist.
- Why refresh rotation, reuse detection, device sessions, and revocation need server-side design.
This post is the boundary marker. A JWT can carry trustworthy claims. Everything that determines how safely those claims are issued, stored, renewed, rejected, and used belongs to the wider system.
The Honest Part
JWTs can remove a database lookup from a request path and make identity context portable across services. Those are real advantages. They also create operational work around signing keys, claim contracts, clock skew, and token lifetime.
They can make revocation less immediate. Claims can become stale. Teams often reintroduce state for logout, device sessions, refresh-token families, or incident response after describing the architecture as stateless.
That is not hypocrisy. Security requirements are allowed to beat conference-slide aesthetics.
The honest answer is that neither JWTs nor opaque sessions are universally safer. Safety comes from choosing a design that matches the threats, then enforcing the boring rules consistently.
My Recommendation
Use JWTs when independent verification solves a real architectural problem. Do not use them as a certificate that your auth is modern.
If you are building a normal web application, start with the simplest mature session architecture your framework supports. If you genuinely need JWTs, write down the rest of the system before shipping the login screen:
- Where do credentials live?
- How long does each credential remain useful?
- What happens on logout, password reset, and account disablement?
- Which browser threats apply to every authenticated route?
- Where are authorization rules enforced?
If those answers are vague, changing the token format will not rescue the design.
The token is one component. The enforcement around it is the auth system.
What is the most confident JWT setup you have seen that fell apart after one uncomfortable security question? I have a few candidates, including some I wrote myself 😅