Secure Code/Explainer
JWT security: signatures, algorithms and where to keep tokens
JSON Web Tokens are compact and convenient, and easy to get subtly wrong. What a token contains, which checks every verifier must make and when a plain session cookie is the better choice.
sha256("/secure-code/jwt-security/")A JSON Web Token (JWT) is a compact way to pass claims, such as a user ID and an expiry time, between systems in a form the receiver can check. The format is defined in RFC 7519 and appears in OAuth and OpenID Connect, in single page applications and between internal services. Most JWT vulnerabilities are not flaws in the format but in how verifiers check, or fail to check, the token.
What a JWT looks like
A signed JWT has three parts separated by dots: a header, a payload and a signature. Header and payload are JSON objects encoded with , a URL-safe variant of base64. The header names the algorithm, for example ES256 or RS256, and often a key ID. The payload holds the claims.
Header
{ "alg": "ES256", "typ": "JWT", "kid": "2026-10" }
Payload
{ "iss": "https://auth.example.com", "sub": "user-4711", "aud": "api.example.com",
"iat": 1793440800, "exp": 1793441700 }
Signature
(bytes computed over the encoded header and payload)iss: who issued the tokensub: whom it is about, usually a user IDaud: the service or services it is meant forexpandnbf: the time window in which it may be acceptediatandjti: when it was issued, and a unique ID that helps against replay
Signed is not encrypted
Anyone who holds a signed JWT can read its payload, because base64url is an encoding, not encryption. The signature only proves that the content has not changed since the issuer signed it. Never put passwords, secrets or personal data into a JWT that you would not show the user. If the content must stay confidential, use an encrypted token () or, more simply, keep the data on the server and put only a reference in the token.
The classic attacks: alg none and algorithm confusion
Because the header names the algorithm, a naive verifier lets the token decide how it should be checked. Two well-known attacks exploit that. The first uses the value none, which RFC 7519 defines for unsecured tokens without a signature: a library that accepts it treats an unsigned, modified token as valid.
The second is . A service expects tokens signed with RSA, but the header of a forged token names the HMAC algorithm HS256 instead. A library that takes the algorithm from the header may then use the RSA public key, which is public by definition, as the HMAC secret and accept a token that anyone could have computed.
RFC 8725, the JWT Best Current Practices, answers both with one principle: the verifier decides the algorithm, not the token. Configure an explicit allow-list, ideally with a single algorithm, tie each key to exactly one algorithm and reject everything else.
import jwt # PyJWT
claims = jwt.decode(
token,
public_key,
algorithms=["ES256"], # fixed in code, never read from the token header
issuer="https://auth.example.com",
audience="api.example.com",
options={"require": ["exp", "iss", "aud", "sub"]},
)Checks every verifier must make
- Verify the signature with the expected algorithm and a key you trust, and reject the whole token if any check fails.
- Check
expandnbf, allowing only a small clock skew. - Check
issagainst the issuer you expect, and make sure the signing key belongs to that issuer. - Check
aud, so a token issued for one service cannot be replayed against another. - Treat header fields such as
kid,jkuandx5uas untrusted input: lookkidup in your own key set, and never fetch keys from a URL in the token unless it is on an allow-list. - Use explicit typing through the
typheader, or validation rules that cannot overlap, so that one kind of token cannot be passed off as another. - Use keys with enough entropy: an HMAC secret must be a long random value, never a password a person chose.
Expiry, refresh tokens and the revocation problem
A JWT is valid until it expires, wherever it is. That is the price of not keeping session state on the server: you cannot simply delete a stolen token. Keep access tokens short-lived, minutes rather than days, and pair them with a that the client exchanges for new access tokens.
Refresh tokens are long-lived and powerful, so store them more carefully, replace them on each use and watch for reuse: if an already used refresh token appears again, revoke the whole chain it belongs to. For immediate logout or account suspension you need server-side state after all, such as a deny-list of token IDs (jti) or a per-user version number checked on each request.
Where to store tokens in the browser
Anything in localStorage or sessionStorage is readable by every script on the page. If an attacker succeeds with cross-site scripting, they can copy the token and use it from anywhere until it expires. A cookie marked HttpOnly cannot be read by JavaScript at all. That removes token theft by script, although an XSS attack can still send requests from the victim’s browser while the page is open.
Set-Cookie: __Host-access=token-value; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=900The __Host- prefix makes the browser insist on Secure, Path=/ and no Domain attribute, so the cookie stays with the exact host that set it. Cookies are sent automatically, so you need protection against cross-site request forgery (CSRF): SameSite helps, and state-changing requests should also carry an anti-CSRF token or pass a check for a custom header.
When a session cookie is the better choice
For a classic web application where browser and server belong to the same team, a random session ID in an HttpOnly cookie, with the session stored on the server, is simpler and easy to revoke. JWTs earn their place when a separate identity provider issues tokens, when several services must check identity without a shared session store, or for short-lived tokens between servers. Choosing JWTs only to avoid a session table often means rebuilding expiry, revocation and secure storage later.
Sources and further reading
- RFC 7519: JSON Web Token (JWT)rfc-editor.org
- RFC 8725: JSON Web Token Best Current Practicesrfc-editor.org
- PyJWT API referencepyjwt.readthedocs.io
- MDN: Set-Cookie headerdeveloper.mozilla.org
General information, checked on 31 October 2026. Test changes on one system first and keep a way back. Found something outdated? Tell the editors.