Table of Contents
1. How JWT Authentication Works
In traditional session-based authentication, the server stores session data and sends the client a session ID cookie. With JWT authentication, the server issues a signed token containing the user's identity and claims. The client stores this token and sends it with each request. The server verifies the token's signature and reads the claims — no database lookup required.
This stateless approach has a significant advantage: any server with the verification key can validate the token independently. This makes JWT ideal for distributed systems, microservices, and horizontally scaled applications where session sharing would add complexity.
Client Server
| |
| POST /login {credentials} |
|------------------------------>|
| | Validate credentials
| | Generate JWT
| 200 OK {access_token, ...} |
|<------------------------------|
| |
| GET /api/data |
| Authorization: Bearer <JWT> |
|------------------------------>|
| | Verify JWT signature
| | Read claims
| 200 OK {data} |
|<------------------------------|2. Access Tokens vs Refresh Tokens
Production JWT authentication systems use two types of tokens with different purposes and lifetimes:
Access Token
- Purpose: Grants access to protected resources
- Lifetime: Short (5–60 minutes)
- Sent with: Every API request in the Authorization header
- Contains: User identity, roles, permissions, expiration
- Storage: Memory (JavaScript variable) or httpOnly cookie
Refresh Token
- Purpose: Obtains new access tokens without re-login
- Lifetime: Long (7–30 days)
- Sent to: Only the token refresh endpoint
- Contains: User identity, token family ID
- Storage: httpOnly cookie (never in localStorage)
Why two tokens? Short-lived access tokens limit the damage window if stolen. Long-lived refresh tokens provide a good user experience (no constant re-login). Refresh tokens can be revoked server-side, giving you a "kill switch" that pure access tokens lack.
3. The Login Flow Step by Step
- User submits credentials
The client sends a POST request with username and password (over HTTPS) to the authentication endpoint.
- Server validates credentials
The server checks the credentials against the user database. Passwords are compared using a secure hash function (bcrypt, scrypt, or Argon2).
- Server generates tokens
On success, the server creates an access token (short-lived, with user claims) and a refresh token (long-lived, stored server-side with the user record).
- Tokens are returned to the client
The access token is returned in the response body. The refresh token is set as an httpOnly, Secure, SameSite cookie.
- Client stores and uses the access token
The client includes the access token in the
Authorization: Bearerheader for all subsequent API requests.
// Example login response
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 900
}
// Refresh token is set as an httpOnly cookie:
// Set-Cookie: refresh_token=eyJ...; HttpOnly; Secure; SameSite=Strict; Path=/auth/refresh4. Token Refresh Flow
When the access token expires, the client uses the refresh token to get a new one without asking the user to log in again.
Client Server
| |
| GET /api/data |
| Authorization: Bearer <expired JWT>
|------------------------------>|
| 401 Unauthorized |
|<------------------------------|
| |
| POST /auth/refresh |
| Cookie: refresh_token=<RT> |
|------------------------------>|
| | Validate refresh token
| | Generate new access token
| | (Optionally rotate refresh token)
| 200 OK {access_token} |
|<------------------------------|
| |
| Retry: GET /api/data |
| Authorization: Bearer <new JWT>
|------------------------------>|Refresh Token Rotation
For additional security, issue a new refresh token every time the old one is used. This is called refresh token rotation. If an attacker steals a refresh token and uses it, the legitimate user's next refresh attempt will fail (because the token was already consumed), alerting the system to a potential compromise.
Token families help detect this: assign a family ID when the user logs in. All rotated refresh tokens in a session share the same family ID. If a refresh token from the same family is used twice, invalidate the entire family and force a re-login.
5. JWT in OAuth 2.0 and OpenID Connect
While you can build custom JWT auth, most production systems use OAuth 2.0 and OpenID Connect (OIDC) as standardized frameworks. Here's how JWT fits in:
OAuth 2.0
OAuth 2.0 is an authorization framework. While the spec doesn't require JWTs, most implementations use JWTs as access tokens because they're self-contained and verifiable without calling the authorization server. The token's scope claim defines what API operations are permitted.
OpenID Connect (OIDC)
OIDC is an identity layer built on top of OAuth 2.0. It introduces the ID token — always a JWT — which contains claims about the authenticated user (name, email, etc.). The flow produces three tokens:
- ID Token (JWT) — Proves the user's identity to the client
- Access Token (often a JWT) — Grants API access
- Refresh Token (opaque string) — Used to get new access tokens
JWKS (JSON Web Key Sets)
In OAuth/OIDC systems, the authorization server publishes its public keys at a well-known endpoint (typically /.well-known/jwks.json). Resource servers fetch these keys to verify token signatures. The kid (Key ID) header in the JWT identifies which key was used for signing.
// Example JWKS endpoint response
{
"keys": [
{
"kty": "RSA",
"kid": "key-2024-01",
"use": "sig",
"alg": "RS256",
"n": "0vx7agoebGcQSuu...",
"e": "AQAB"
}
]
}6. JWT in Microservices
JWTs are particularly well-suited for microservices architectures where multiple independent services need to authenticate requests. Here's why and how:
API Gateway Pattern
A common pattern is to validate the JWT at the API gateway and then propagate trusted claims to downstream services via internal headers:
Client → API Gateway → Service A → Service B
(validates JWT) ↓
Propagates claims:
X-User-ID: user_123
X-User-Roles: editor,reviewerThis way, internal services don't need to re-validate the JWT. They trust the gateway and read claims from headers. The internal network must be secured to prevent header injection.
Service-to-Service Authentication
Services can also issue their own JWTs for internal communication. Each service has its own key pair. The iss claim identifies which service generated the token, and the aud claim specifies the target service.
7. Implementing Logout
Logout with JWTs is more nuanced than with sessions. Since JWTs are stateless, you can't simply "delete" a token server-side. Here are practical approaches, from simplest to most comprehensive:
1. Client-Side Only (Simplest)
Delete the access token from memory and the refresh token cookie. The access token remains valid until it expires, but without the refresh token, the session won't renew.
Limitation: A stolen access token is still usable until expiration.
2. Refresh Token Revocation
Delete or mark the refresh token as revoked in the database. Combined with short access token lifetimes (5 min), the user is effectively logged out within minutes.
Recommended for most applications — good balance of security and simplicity.
3. Token Blacklist (Most Secure)
Add the access token's jti to a blacklist (Redis) and check on every request. Provides immediate logout but adds a database lookup to every API call.
Best for: High-security applications where immediate token invalidation is required.
8. Architecture Recommendations
For Single-Page Applications (SPAs)
- Store access tokens in memory (JavaScript variable), not localStorage
- Use httpOnly cookies for refresh tokens
- Implement silent refresh to get new tokens before expiration
- Consider the Backend-for-Frontend (BFF) pattern for added security
For Mobile Apps
- Store tokens in the platform's secure storage (Keychain on iOS, Keystore on Android)
- Use certificate pinning to prevent MITM attacks
- Implement biometric authentication for refresh token access
- Use PKCE (Proof Key for Code Exchange) for OAuth flows
For Server-Rendered Apps
- Store both tokens in httpOnly cookies
- The server reads the JWT from the cookie on each request
- Add CSRF protection since cookies are sent automatically
- This approach is the simplest and most secure for traditional web apps
Debug Your Auth Tokens
Inspect access tokens, ID tokens, and refresh tokens to verify claims and debug auth issues.