JWT Authentication Explained: How Tokens Work Under the Hood
Understand the internals of JSON Web Tokens — header, payload, signature — and learn how modern applications use them for stateless authentication.
How JWT Authentication Actually Works
JSON Web Tokens (JWTs) have become the dominant authentication mechanism for modern web applications, single-page apps, and microservices architectures. But despite their ubiquity, they are frequently misunderstood and misused. This guide explains how JWTs work from the inside out, when to use them, and the security pitfalls you need to avoid.
The Problem JWTs Solve
Traditional session-based authentication works like this: when a user logs in, the server creates a session record in its database, generates a session ID, and sends it to the client as a cookie. On every subsequent request, the client sends the cookie, and the server looks up the session in its database to verify the user's identity.
This approach has a scalability problem: every request requires a database lookup. In a microservices architecture with dozens of services, each service needs access to the session database — creating a central bottleneck and a single point of failure.
JWTs solve this by encoding the user's identity and permissions directly into the token itself. The server signs the token with a secret key, and any service that knows the key can verify the token without making a database call. This is stateless authentication.
Anatomy of a JWT
A JWT consists of three parts separated by dots: header.payload.signature
Here is a real JWT (decoded for readability):
Header:
{
"alg": "HS256",
"typ": "JWT"
}The header specifies the signing algorithm (HS256 = HMAC-SHA256) and the token type.
Payload:
{
"sub": "user_12345",
"name": "Alice Johnson",
"role": "admin",
"iat": 1716192000,
"exp": 1716278400
}The payload contains claims — statements about the user and metadata:
sub(subject): The user identifieriat(issued at): When the token was created (Unix timestamp)exp(expiration): When the token expires (Unix timestamp)- Custom claims: Any additional data you need (
role,name,permissions)
Signature:
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret_key
)The signature is created by hashing the encoded header and payload with a secret key. This is what prevents tampering — if anyone modifies the payload (changing their role from "user" to "admin"), the signature verification will fail.
The Authentication Flow
- Login: User sends credentials (email + password) to the auth server
- Verification: Server validates credentials against the user database
- Token creation: Server creates a JWT with the user's identity and signs it
- Token delivery: Server sends the JWT to the client (usually in the response body)
- Storage: Client stores the JWT (localStorage, sessionStorage, or memory)
- Authenticated requests: Client includes the JWT in the Authorization header:
Authorization: Bearer eyJhbG... - Verification: Each service verifies the signature and reads the claims — no database call needed
Signing Algorithms: HS256 vs RS256
HS256 (HMAC-SHA256) uses a symmetric key — the same secret signs and verifies the token. This is simpler but means every service that needs to verify tokens must know the secret.
Sign: HMAC(header.payload, shared_secret) → signature
Verify: HMAC(header.payload, shared_secret) === received_signatureRS256 (RSA-SHA256) uses asymmetric keys — a private key signs the token and a public key verifies it. This is more secure for distributed systems because you can share the public key without compromising the signing capability.
Sign: RSA_SIGN(header.payload, private_key) → signature
Verify: RSA_VERIFY(header.payload, public_key, signature) → true/falseRecommendation: Use RS256 for production microservices architectures. Use HS256 for simple single-server applications.
Security Best Practices
1. Set short expiration times. JWTs cannot be revoked once issued (without maintaining a blacklist, which defeats the stateless advantage). Use short-lived access tokens (15-30 minutes) combined with refresh tokens (days/weeks).
2. Never store sensitive data in the payload. The payload is only Base64-encoded, not encrypted. Anyone can decode it. Never include passwords, credit card numbers, or SSNs.
3. Always validate the algorithm. Some JWT libraries accept the "alg": "none" header, which disables signature verification entirely. Always validate that the algorithm in the header matches your expected algorithm.
4. Use HTTPS exclusively. JWTs transmitted over HTTP can be intercepted and reused. Always use HTTPS to prevent token theft.
5. Prefer httpOnly cookies over localStorage. Storing JWTs in localStorage makes them accessible to JavaScript, which means they are vulnerable to XSS attacks. httpOnly cookies cannot be accessed by JavaScript.
When NOT to Use JWTs
JWTs are not the right choice for every application:
- When you need instant revocation: If a user changes their password or an admin needs to immediately revoke access, session-based auth with a server-side store is simpler.
- When tokens contain too much data: JWTs are sent with every request. If your payload is large (hundreds of roles and permissions), the overhead adds up.
- For long-lived sessions: JWTs work best as short-lived access tokens. For "remember me" functionality, use a server-side refresh token.
Common JWT Vulnerabilities
Algorithm confusion: An attacker modifies the header to use "alg": "none" or switches from RS256 to HS256, using the public key as the HMAC secret. Mitigation: always enforce the expected algorithm in your verification code.
Token sidejacking: If a token is stolen (via XSS, network sniffing, or a compromised device), the attacker can impersonate the user until the token expires. Mitigation: short expiration times, HTTPS, and httpOnly cookies.
Payload tampering: Modifying the payload without updating the signature will cause verification to fail — but only if verification is actually performed. Always verify the signature before trusting any claims.
Refresh Token Pattern
The recommended pattern for production applications combines short-lived access tokens with long-lived refresh tokens:
- On login, issue both an access token (15 min expiry) and a refresh token (7 day expiry)
- Store the refresh token securely (httpOnly cookie or encrypted storage)
- When the access token expires, send the refresh token to get a new access token
- If the refresh token is expired or revoked, the user must log in again
This pattern balances security (short-lived access tokens limit the window of exploitation) with usability (users do not have to log in every 15 minutes).
Summary
JWTs enable stateless authentication by encoding user identity directly into the token, eliminating per-request database lookups. They consist of three Base64URL-encoded parts — header, payload, and signature — where the signature prevents tampering. While JWTs are powerful, they must be implemented carefully: use short expiration times, RS256 for distributed systems, HTTPS for transport, and httpOnly cookies for storage. Understanding these fundamentals will help you build secure, scalable authentication systems.
Try the Related Tool
Put this knowledge into practice with our free, privacy-first tool.
Open Jwt Tool →