JWT Security: Signing, Expiry, and Storage Rules
A JWT is a self-contained credential: whoever holds it can present it, and your server decides what it proves. That puts three decisions at the center of every implementation — how the token is signed, how long it lives, and where the client keeps it — and getting any one wrong dooms the other two. This article covers HS256 versus RS256, expiry and revocation, storage, and a pre-launch checklist. You can mint tokens with the JWT generator and inspect any token with the JWT decoder.
Signing: HS256 vs RS256 and when
HS256 is HMAC-SHA256: one secret, used both to sign and to verify. It is simple, fast, and right for a single service that mints and verifies its own tokens. Its danger is distribution — every verifier must hold the same secret, so each additional service widens the blast radius of a leak.
RS256, and its elliptic-curve cousin ES256, is asymmetric: the auth server signs with a private key, and services verify with a public key that can be cached and rotated independently. RS256 is the standard choice when multiple services or third parties verify your tokens.
The attack that ties them together is algorithm confusion. If your verifier trusts the alg header to choose the algorithm, an attacker can change RS256 to HS256 and sign the token with the public key — which anyone can read — as the HMAC secret. Pin the algorithm server-side: verify exactly the algorithm you issue, reject alg: none unconditionally, and use at least 256 bits of entropy kept in a secrets manager.
Expiry, refresh, and revocation reality
Access tokens should be short-lived — five to fifteen minutes is the common range — because there is no practical way to revoke one before its exp. A refresh token is the long-lived credential that trades for new access tokens; it should be single-use with rotation, so a stolen token can be used at most once before the legitimate client notices.
The honest position on revocation: a stateless JWT cannot be revoked, only expired. If your model truly needs instant revocation, you either check a denylist of revoked jti values in Redis on each request, which reintroduces the state you were avoiding, or you keep access tokens very short and accept the replay window. Sliding sessions must still enforce an absolute lifetime, or a stolen token lives forever.
Where to store tokens (and where not to)
Storage is an XSS-versus-CSRF question. localStorage is readable by any script on your page, so an XSS bug anywhere in your front end becomes token theft. HttpOnly cookies are invisible to JavaScript, but browsers attach them automatically, so you need CSRF protection — the SameSite=Strict or Lax attribute, plus a token on older browsers.
The common recommendation for browser apps: refresh token in an HttpOnly, Secure, SameSite cookie scoped to your auth endpoint; access token in memory only, refetched on page load. Never put tokens in URLs — they leak into logs, referrers, and browser history — and never log them, even truncated. For native apps, the platform keychain is the right home. For machine-to-machine credentials, an API key from the API key generator fits better than a JWT.
A pre-launch checklist
| Check | Pass condition |
|---|---|
| Algorithm pinned | Verifier accepts exactly one alg and rejects none |
| Key strength | 256 bits or more of entropy, loaded from a secrets manager |
iss and aud verified | A token minted for another service is rejected |
exp set and short | Access token 15 minutes or less; refresh rotates on use |
| Storage | No long-lived credentials in localStorage |
| Transport and logging | HTTPS only; tokens never in URLs or logs |
Most JWT incidents are not broken cryptography; they are a missing audience check, a 30-day access token, or a token in a URL that ended up in an analytics log. Walk the checklist at launch and whenever the auth flow changes — and keep the JWT decoder open while you do.
JWT Generator — build signed HS256 or RS256 tokens with custom claims and expiry for testing, entirely in your browser.
Related tools: JWT Decoder · API Key Generator