← // all posts
field notes // security

JWT Authentication Mistakes That Ship to Production

2026-09-22 // Nicholas Rushing // 7 min read

JWTs are not inherently unsafe. The trouble starts when a convenient token becomes the entire identity system: it never expires, lives somewhere JavaScript can read it, carries too much authority, and has no realistic way to be revoked.

The algorithm is only one part of the decision. The safer question is whether the token lifecycle makes a stolen credential short-lived, narrow, observable, and replaceable.

Mistake 1: tokens that live forever

An access token should be a temporary proof, not a permanent password. Keep access tokens short-lived and use a separate refresh mechanism with rotation and revocation. A seven-day bearer token turns one accidental exposure into a week-long incident.

Mistake 2: storing tokens where scripts can read them

Putting long-lived credentials in localStorage makes a successful script injection much more valuable. Prefer secure, httpOnly cookies for refresh credentials and keep the access token scope as narrow as the client experience allows.

Mistake 3: authenticating without authorizing

A valid token does not mean the caller can read every record or perform every mutation. Check the resource owner, role, tenant, and action at the server boundary. Never trust a user ID, role, or entitlement copied from the browser.

Mistake 4: no rotation or recovery

Users need a way to sign out sessions, rotate credentials, reset passwords, and invalidate a stolen refresh token. A token version field or server-side denylist can provide a practical revocation path without turning every request into a database-heavy session lookup.

Mistake 5: testing only the happy path

Test expired tokens, wrong audiences, wrong tenants, replayed refresh tokens, disabled users, altered claims, and access to another user's resource. Security bugs hide in the negative paths because normal product demos never visit them.

If authentication is carrying real accounts, data, or billing, have the whole path reviewed before launch.

request_the_$399_snapshot