What is a JWT?
A JWT (JSON Web Token) is a compact, URL-safe string that encodes a JSON payload along with a cryptographic signature. The signature lets any party with the right key verify that the payload hasn't been tampered with — without checking a database. This is the key insight: JWTs are self-contained. Once issued, a server can verify a JWT using only its own key, without making a database round-trip to look up the session.
The three-part structure
A JWT looks like this (line breaks added for readability):
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 . eyJ1c2VySWQiOiI0MiIsInJvbGUiOiJhZG1pbiIsImV4cCI6MTc0ODAwMDAwMH0 . SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Three base64url-encoded sections separated by dots:
- Header: Decoded, the first section is a JSON object specifying the token type and signing algorithm:
{ "alg": "HS256", "typ": "JWT" } - Payload (claims): The actual data. Standard claims include
iss(issuer),sub(subject / user ID),exp(expiration timestamp),iat(issued-at timestamp), andaud(audience). You can add any custom claims: user ID, role, permissions, tenant ID. - Signature: Computed as
HMAC-SHA256(base64(header) + "." + base64(payload), secret)for HS256. The signature binds the header and payload together — any modification invalidates it.
Signing algorithms: HS256 vs RS256
HS256 (HMAC-SHA256) uses a single symmetric secret key for both signing and verification. This means every service that needs to verify tokens must have the same secret — fine for single-service architectures, problematic for microservices where you don't want every service to hold the signing key. RS256 (RSA-SHA256) uses a public/private key pair: the auth service signs with the private key, and any other service can verify with the public key. The public key can be safely distributed. For multi-service architectures and any system where you issue tokens externally (OAuth2, OpenID Connect), RS256 is the standard.
Critical security pitfalls
- The "none" algorithm attack. Early JWT libraries accepted
"alg": "none"as valid, meaning no signature was required. An attacker could craft an arbitrary token, set the algorithm to "none", and the library would accept it. Always explicitly specify which algorithms your library accepts and reject "none". - JWTs are not encrypted. The payload is base64-encoded, not encrypted — anyone who has the token can decode and read the claims. Never store sensitive data (passwords, credit card numbers, PII you need to protect) in a JWT payload. If you need encrypted tokens, use JWE (JSON Web Encryption).
- Expiration is not revocation. A valid JWT remains valid until its
exptimestamp. You cannot invalidate a JWT without a blocklist (which requires a database lookup, eliminating the statelessness benefit). Short expiration times (15 minutes) combined with refresh tokens is the standard pattern. - Store tokens securely. In browser applications, JWTs stored in localStorage are vulnerable to XSS attacks — any injected script can steal them. HttpOnly cookies are safer from XSS but require CSRF protection. The right choice depends on your security threat model.
When to use JWTs vs sessions
Traditional sessions store state server-side (in memory, Redis, or a database) and give the client an opaque session ID. JWTs store state client-side in the token itself. JWTs shine in stateless architectures, microservices, and mobile apps where you don't want a centralized session store. Sessions are better when you need instant revocation (user logout takes effect immediately), when your payload is large (JWTs grow with payload size), or when you're building a traditional server-rendered app where cookies are the natural choice anyway. Many modern systems use both: short-lived JWTs for access tokens and database-backed refresh tokens for revocation capability.
Related tools
JSON Formatter — inspect JWT payload claims →