"How does staying logged in work?" That was my question when I first learned web development. HTTP is stateless—how does the server remember me?
First, Understanding Sessions
To understand JWT, you need to know the traditional session approach.
- User logs in
- Server creates a session ID and stores it in memory/database
- Session ID sent to browser as a cookie
- Cookie automatically sent with every request
- Server looks up user info by session ID
The problem: The server must remember all sessions. Multiple servers? You need session synchronization. That's complicated.
JWT: The Server Doesn't Need to Remember
JWT (JSON Web Token) takes a different approach. Instead of the server storing user info, it puts the information in the token itself and gives it to the client.
The token includes the server's signature, so later when it receives the token, it can verify "yep, I made this."
JWT Structure: Three Parts Separated by Dots
A JWT looks like this:
xxxxx.yyyyy.zzzzz
Header.Payload.Signature
1. Header
Token type and signing algorithm. Base64Url encoded.
{"alg": "HS256", "typ": "JWT"}
2. Payload
The actual data. Called "claims."
{
"sub": "1234567890",
"name": "John Doe",
"admin": true,
"exp": 1706234567
}
⚠️ Important: The payload is NOT encrypted! Anyone can decode it. Don't put sensitive data here.
3. Signature
Created by combining header + payload + secret key.
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
This signature lets the server detect if the token was tampered with.
JWT Authentication Flow
- User logs in
- Server creates and returns a JWT
- Client stores JWT (usually localStorage or httpOnly cookie)
- Subsequent requests include
Authorization: Bearer {token}header - Server verifies signature and uses payload info
Sessions vs JWT
| Sessions | JWT | |
|---|---|---|
| Storage | Server | Client |
| Scalability | Needs session sync | No server sharing needed |
| Logout | Delete session, done | Hard to invalidate token |
| Security | Server-managed | Token theft is dangerous |
JWT Security Considerations
1. Expiration (exp) is a must
Set an expiration time. Permanent tokens are dangerous. Usually 15 minutes to 1 hour.
2. Use Refresh Tokens
Short-lived access tokens with refresh token rotation is safer.
3. HTTPS is mandatory
Tokens are exposed in plain text over HTTP.
4. localStorage vs httpOnly cookies
localStorage is vulnerable to XSS; cookies to CSRF. httpOnly cookie + CSRF token is safest.
When to Use JWT
- Microservices architecture (shared auth between services)
- Mobile apps (instead of cookies)
- Single Sign-On (SSO)
- API authentication
For simple websites, sessions might be simpler. Choose the right tool for the job.
JWT Claims in Detail
The payload section of a JWT contains claims, which are statements about the user and metadata about the token. The JWT specification (RFC 7519) defines a set of registered claims that are not mandatory but strongly recommended.
- iss (Issuer): Who created and signed the token. Typically a URL identifying your auth server, like
"https://auth.example.com". - sub (Subject): The user or entity the token represents. Usually a user ID like
"user-12345". - aud (Audience): The intended recipient of the token. An API server can reject tokens where the audience claim does not match its own identifier.
- exp (Expiration Time): A Unix timestamp after which the token is no longer valid. The server must reject expired tokens.
- iat (Issued At): When the token was created. Useful for determining token age.
- nbf (Not Before): The token should not be accepted before this timestamp. Rarely used but available for scheduled access.
- jti (JWT ID): A unique identifier for the token, useful for preventing replay attacks by tracking which tokens have been used.
Beyond registered claims, you can add custom claims for your application. Common examples include user roles, permissions, or tenant IDs in multi-tenant systems. Keep the payload lean, though. Every byte in the JWT is sent with every request, and large tokens waste bandwidth and slow down parsing.
Signing Algorithms: HS256 vs RS256
The signing algorithm determines how the signature is created and verified. The two most common choices are HS256 and RS256, and they work quite differently.
HS256 (HMAC + SHA-256)
This is a symmetric algorithm. The same secret key is used to both sign and verify the token. It is simple to implement and fast to compute. The drawback is that every service that needs to verify tokens must have access to the secret key. If you have a single backend server, this works well. If you have multiple services, you need to distribute the secret securely, which introduces risk.
// HS256: same key signs and verifies
const token = jwt.sign(payload, "my-shared-secret", { algorithm: "HS256" });
const decoded = jwt.verify(token, "my-shared-secret");
RS256 (RSA + SHA-256)
This is an asymmetric algorithm using a public/private key pair. The auth server signs tokens with its private key. Any service can verify tokens using the public key, which can be distributed freely without security risk. This is the standard choice for microservices architectures and third-party integrations. OIDC (OpenID Connect) providers like Google and Auth0 use RS256 by default.
// RS256: private key signs, public key verifies
const token = jwt.sign(payload, privateKey, { algorithm: "RS256" });
const decoded = jwt.verify(token, publicKey);
Other algorithms exist too. ES256 uses elliptic curve cryptography and produces shorter signatures than RS256. PS256 uses RSA-PSS padding. The none algorithm means no signature at all, and accepting it is a well-known security vulnerability that has led to real-world breaches. Always validate the algorithm in the header and reject none.
The Complete Authentication Flow
Let's walk through a complete JWT authentication cycle, step by step, to see how all the pieces fit together.
- Login: The user sends their username and password to the auth endpoint (
POST /auth/login). - Validation: The server checks the credentials against the database. If they match, it creates a JWT containing the user's ID, role, and an expiration time.
- Token delivery: The server returns the JWT (and optionally a refresh token) to the client. The client stores it, typically in an httpOnly cookie or memory.
- Authenticated requests: On every subsequent API call, the client includes the token in the
Authorization: Bearer <token>header. - Verification: The server extracts the token, verifies the signature using its secret or public key, checks the expiration, and reads the claims. No database lookup is needed for basic auth checks.
- Token refresh: When the access token expires, the client sends the refresh token to a dedicated endpoint (
POST /auth/refresh). The server issues a new access token without requiring the user to log in again.
Access Tokens vs Refresh Tokens
A common pattern splits authentication into two tokens with different lifetimes and purposes.
| Access Token | Refresh Token | |
|---|---|---|
| Lifetime | Short (15 min - 1 hour) | Long (7 - 30 days) |
| Purpose | Authorize API requests | Obtain new access tokens |
| Storage | Memory or httpOnly cookie | httpOnly cookie (secure) |
| Sent to | API servers | Auth server only |
| If stolen | Limited damage (short-lived) | Attacker can get new access tokens |
Refresh token rotation adds another layer of safety. Each time a refresh token is used, the server issues a new refresh token and invalidates the old one. If an attacker steals and uses a refresh token that the legitimate user has already rotated, the server detects the reuse and revokes the entire token family, forcing a fresh login.
Security Best Practices
JWT security is not just about the token itself. It is about how you handle it throughout its lifecycle.
- Always validate the signature. Never decode a JWT and trust the payload without verifying the signature first. Libraries like
jsonwebtoken(Node.js) do this automatically when you useverify()instead ofdecode(). - Check the algorithm. Pin the expected algorithm on the server side. Do not let the token's header dictate which algorithm to use. An attacker could set the algorithm to
noneand strip the signature. - Validate all relevant claims. Check
exp(expiration),iss(issuer), andaud(audience) on every request. A token intended for Service A should not be accepted by Service B. - Keep the payload small. Do not store entire user profiles in the token. Include only what is needed for authorization decisions: user ID, roles, and permissions.
- Use HTTPS everywhere. JWTs transmitted over HTTP can be intercepted by anyone on the network. There is no workaround for this except TLS.
- Implement token revocation. Despite JWT being stateless, you need a way to invalidate tokens in emergencies (compromised accounts, password changes). A short-lived access token combined with a server-side refresh token blacklist is the most practical approach.
- Rotate signing keys. Periodically change your signing keys. Use the JWKS (JSON Web Key Set) endpoint to publish current and recent public keys, allowing a graceful transition.
JWT vs Session-Based Auth: A Deeper Look
The choice between JWT and sessions is not simply "new vs old" or "better vs worse." Each approach has fundamental trade-offs that make it better suited to different architectures.
Sessions shine when you need instant revocation. Deleting a session from the server immediately locks out the user. With JWT, the token remains valid until it expires, unless you implement a blacklist, which reintroduces server-side state and partially defeats the purpose of JWT.
JWT shines in distributed systems. When you have 20 microservices behind a load balancer, sharing session state across all of them requires a centralized session store (like Redis). JWT tokens carry their own proof of identity, so any service can verify them independently using just the public key.
A hybrid approach is also common: use JWT for service-to-service communication within your infrastructure, and sessions for user-facing web applications where you need fine-grained control over user state. Many production systems use both, choosing the right tool for each specific boundary.
Frequently Asked Questions
Is JWT encrypted?
JWT is signed but not encrypted by default. Since it uses Base64URL encoding, anyone can read the contents. JWE (JSON Web Encryption) can encrypt the payload, but standard JWT only includes a signature.
How should I set JWT expiration time?
Set the exp (expiration) claim with a Unix timestamp. Access tokens typically expire in 15 minutes to 1 hour, refresh tokens in 7-30 days. Too long risks security, too short hurts usability.