JWT best practices: expiry, storage and validation
JSON Web Tokens are simple to issue and easy to get wrong. Short lifetimes, refresh token rotation, safe storage and strict validation close the common holes.
A JSON Web Token (JWT) is three Base64 encoded parts separated by dots: a header, a payload and a signature. The server signs the token at login, and afterwards trusts any request that presents a token with a valid signature. No session lookup is needed.
That convenience is also the risk. A stolen token works for the thief exactly as it works for the user, and a server that validates carelessly will accept tokens it should not. These practices cover the common failures.
1. Keep access tokens short-lived
An access token should expire in minutes, typically 5 to 15. If it leaks, the damage has a time limit.
To avoid logging users out every few minutes, issue a longer-lived refresh token as well. When the access token expires, the client exchanges the refresh token for a new one.
Rotate refresh tokens. Each use returns a new refresh token and invalidates the old one. If an old one is ever presented again, someone has copied it, and the server should revoke the whole session.
2. Store tokens where scripts cannot reach them
| Client | Refresh token | Access token |
|---|---|---|
| Browser | A cookie marked HttpOnly, Secure and SameSite |
In memory |
| Mobile app | Keychain on iOS, Keystore on Android | In memory |
Do not keep tokens in localStorage. Any script that runs on your page, including one injected through a vulnerable dependency, can read it.
3. Validate every claim on every request
Checking that the token decodes is not validation. The server must verify:
- the signature, against an explicit list of allowed algorithms,
exp, so expired tokens are rejected,iss, so the token came from your auth server,aud, so a token issued for another service is not accepted here.
Use a maintained library and state those expectations explicitly.
import { jwtVerify } from "jose";
const { payload } = await jwtVerify(token, publicKey, {
algorithms: ["RS256"],
issuer: "https://auth.example.com",
audience: "orders-api",
});
Never let the token's own header decide which algorithm to use, and never accept none.
4. Use strong keys and rotate them
- For HMAC signing such as HS256, the secret must be long and random: at least 256 bits, generated and not typed.
- When several services verify tokens, prefer asymmetric signing such as RS256 or ES256. Only the auth server holds the private key. Everyone else verifies with the public key.
- Publish public keys through a JWKS endpoint and rotate them on a schedule. The
kidheader tells verifiers which key to use.
5. Plan for revocation
A JWT cannot be recalled once issued. Short expiry is the main defence. For logout and compromised accounts, keep a small denylist of token IDs (jti) until they expire, and revoke the refresh token.
What does not belong in a token
The payload is signed, not encrypted. Anyone holding the token can read it. Keep it to identifiers and roles: no passwords, no personal data, no secrets.
The flow in practice
A user logs in on a mobile app. The auth server returns a ten minute access token and a refresh token, which the app stores in the device's secure storage. API calls carry the access token. When it expires, the app exchanges the refresh token for a new pair, and the user never sees a login screen.
Tokens are one layer. For the rest, see our API endpoint security checklist.