JWT Tokens Explained: Structure, Security, and Common Pitfalls
JSON Web Tokens (JWTs) have revolutionized the way we handle authentication in modern web applications. By providing a compact, self-contained way to transmit information between parties, they enable stateless authentication that scales effortlessly. But with great power comes great responsibilityโand many ways to get it wrong. In this guide, we'll dive deep into the structure of JWTs, explore advanced security strategies, and identify the pitfalls that can leave your application vulnerable.
The Three Parts of a JWT
A JWT is a string consisting of three parts separated by dots (.):
- Header: Typically contains the type of token (JWT) and the signing algorithm being used (e.g., HS256 or RS256). It is Base64URL-encoded.
- Payload: Contains the claims. Claims are statements about an entity (typically, the user) and additional data. This is also Base64URL-encoded.
- Signature: Used to verify that the sender of the JWT is who it says it is and to ensure that the message wasn't changed along the way. It is created by signing the encoded header and payload with a secret key.
// Example of a decoded JWT Payload
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022,
"exp": 1516242622,
"admin": true
}
Registered, Public, and Private Claims
Claims are the heart of the JWT. They are divided into three categories:
- Registered claims: A set of predefined claims which are not mandatory but recommended (e.g.,
issfor issuer,expfor expiration time,subfor subject,audfor audience). - Public claims: Can be defined at will by those using JWTs. To avoid collisions, they should be defined in the IANA JSON Web Token Registry or be defined as a URI that contains a collision-resistant namespace.
- Private claims: Custom claims created to share information between parties that agree on using them. These are neither registered nor public.
JWT vs. Session Cookies: The Great Debate
One of the most common questions in web development is whether to use JWTs or traditional session cookies. Session cookies are stateful; the server stores a session ID in a database or cache and checks it on every request. JWTs are stateless; all the information is contained in the token itself. While JWTs scale better for microservices and mobile apps (no shared session store needed), they are harder to revoke. If a user logs out, you can't easily "delete" a JWT that is already in the wild unless you implement a revocation strategy.
Revocation Strategies: Handling the Stateless Nature
To handle logout and account suspension with JWTs, you have several options, each with its own trade-offs:
- Short TTL (Time To Live): Keep access tokens very short (e.g., 5-15 minutes) and use refresh tokens to get new ones. This minimizes the window of opportunity for an attacker if a token is stolen.
- Blacklisting: Store the IDs (
jticlaim) of revoked tokens in a fast cache like Redis. The server checks this list on every request. This adds a small amount of state but maintains the benefits of JWTs. - Whitelisting: Only allow tokens that are explicitly stored in your database. This effectively makes JWTs stateful but provides maximum control over active sessions.
- Rotating Secrets: In extreme cases, you can rotate the signing secret, which immediately invalidates all tokens signed with the old secret.
JOSE Header Deep Dive
The header of a JWT is part of the JOSE (JSON Object Signing and Encryption) framework. Beyond alg and typ, it can include fields like kid (Key ID), which tells the recipient which public key to use for verification. This is essential when you are rotating keys. More advanced headers like x5u or x5c can even include links to X.509 certificates for trust verification. Understanding these headers is key to building enterprise-grade authentication systems.
JWT in Microservices: Identity Propagation
In a microservices architecture, JWTs are often used to propagate identity between services. An API Gateway authenticates the user and generates a JWT, which is then passed to downstream services in the Authorization header. This allows each service to know who the user is and what they are allowed to do without needing to call a central authentication service for every request. This "identity propagation" is one of the primary reasons for JWT's popularity in cloud-native environments.
Security Pitfalls to Avoid
Despite their popularity, JWTs are often implemented insecurely. Here are the most common mistakes:
- The "none" algorithm: Some libraries allow the
algheader to be set tonone, which bypasses signature verification. Always explicitly whitelist the algorithms you support (e.g., only allow RS256). - Weak secrets: If you use a symmetric algorithm like HS256, your secret must be long, random, and kept strictly confidential. A weak secret can be brute-forced in minutes using modern hardware.
- Sensitive data in the payload: JWTs are encoded, not encrypted. Anyone with the token can read the payload. Never store passwords, API keys, or PII (Personally Identifiable Information) in a JWT.
- Missing expiration: Always set a short
expclaim. A token without an expiration date is a permanent key to your application if stolen.
Best Practices for Implementation
To use JWTs securely, follow these industry-standard best practices:
- Use asymmetric algorithms: RS256 or ES256 are generally safer than HS256 because the private key never leaves the authentication server. Downstream services only need the public key to verify the token.
- Implement refresh tokens: Use short-lived access tokens and longer-lived refresh tokens stored in
HttpOnlycookies to minimize the impact of a stolen token. - Validate everything: Check the signature, the expiration, the issuer (
iss), and the audience (aud) on every request. Don't trust any part of the token until the signature is verified. - Use secure storage: Store JWTs in
HttpOnly,Secure,SameSite=Strictcookies to prevent XSS and CSRF attacks from stealing or using them.
Inspecting and Debugging JWTs
When developing, you often need to see what's inside a token to debug claims or expiration issues. Use a client-side JWT Inspector to decode tokens safely. Unlike online decoders that might log your tokens, a client-side tool keeps your data in your browser. For more advanced scenarios involving public keys and key rotation, a JWK/JWKS Studio can help you manage and convert key formats between PEM and JWK.
The Future: JWE and OIDC
While standard JWTs are signed (JWS), you can also encrypt them using JWE (JSON Web Encryption) if you need to store sensitive data in the payload. Furthermore, OpenID Connect (OIDC) builds on top of JWT to provide a standardized identity layer, including a /userinfo endpoint and discovery documents. As you build more complex systems, you'll likely find yourself moving from simple JWTs to full OIDC implementations.
JWTs are a powerful tool for modern developers, but they require a deep understanding of security principles to use correctly. By following these guidelines and using the right tools, you can build authentication systems that are both flexible and secure. Remember: statelessness is a feature, but security is a requirement.