Introduction: What Is a JSON Web Token?
JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact, self-contained way to securely transmit information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (HMAC algorithm) or a public/private key pair (RSA or ECDSA).
JWTs were created to address a common challenge in distributed systems: how can a server trust a request from a client without needing to look up a session in a database? The answer is to encode and sign the user identity and permissions directly into a token that the client carries with every request.
Today, JWTs are the backbone of modern authentication and authorization:
- Single Sign-On (SSO): One login grants access to multiple services.
- API Authentication: Mobile apps and SPAs authenticate API calls with a Bearer token.
- OAuth 2.0 / OpenID Connect: JWTs serve as access tokens and ID tokens.
- Microservices: Services verify caller identity without a central session store.
JWT Structure: header.payload.signature
A JWT is a string of three Base64url-encoded parts separated by dots (.):
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Part 1 — Header decodes to:
{
"alg": "HS256",
"typ": "JWT"
}
The header specifies the signing algorithm (alg) and token type (typ).
Part 2 — Payload decodes to:
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
The payload contains the claims — statements about the entity (typically a user) and metadata.
Part 3 — Signature (for HS256) is computed as:
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
The signature ensures the token has not been tampered with. Critical: The payload is only Base64url-encoded, not encrypted. Anyone who obtains the token can read all claims.
Signing Algorithms: HS256, RS256, and ES256
HS256 (HMAC-SHA256) — Symmetric
Uses a single shared secret for both signing and verification. Simple to implement, but the same secret must be known by both issuer and verifier.
- Best for: Monolithic applications or tightly coupled services sharing a secret.
- Risk: If the secret leaks, all tokens can be forged.
RS256 (RSA-SHA256) — Asymmetric
Uses a private key to sign and a public key to verify. The private key stays secret; the public key can be shared freely via a JWKS endpoint.
- Best for: Distributed systems, microservices, and multi-service verification.
- Key size: Minimum 2048-bit RSA key recommended.
ES256 (ECDSA-SHA256) — Asymmetric
Uses the Elliptic Curve Digital Signature Algorithm with the P-256 curve. Produces smaller signatures than RS256 with equivalent security.
- Best for: Performance-sensitive or bandwidth-constrained environments.
| Algorithm | Type | Key | Signature Size | Use Case |
|---|---|---|---|---|
| HS256 | Symmetric | Shared secret | 32 bytes | Single-service apps |
| RS256 | Asymmetric | RSA key pair | 256+ bytes | Distributed systems |
| ES256 | Asymmetric | EC key pair | 64 bytes | High-performance APIs |
JWT Claims: Registered, Public, and Private
Claims are statements about an entity (typically a user) and metadata encoded in the payload.
Registered Claims (IANA-defined)
| Claim | Full Name | Description |
|---|---|---|
iss |
Issuer | Who issued the token (e.g., "auth.example.com") |
sub |
Subject | Whom the token refers to (e.g., a user ID) |
aud |
Audience | Who the token is intended for |
exp |
Expiration | Unix timestamp after which the token is invalid |
nbf |
Not Before | Unix timestamp before which the token must not be used |
iat |
Issued At | Unix timestamp when the token was issued |
jti |
JWT ID | Unique identifier; prevents replay attacks |
Public Claims
Defined in the IANA JWT Claims Registry. Examples: email, name, picture, roles.
Private Claims
Custom claims agreed upon by issuer and consumer. Use namespaced names to avoid collisions:
{
"https://example.com/tenant_id": "acme-corp",
"https://example.com/roles": ["admin", "editor"]
}
How JWT Authentication Flow Works
- User Login: The client sends credentials to the authentication server.
- Token Issuance: The server validates credentials, creates a signed JWT with appropriate claims, and returns it.
- Token Storage: The client stores the JWT (in memory, localStorage, or an HttpOnly cookie).
- Authenticated Requests: For each subsequent request to a protected resource, the client includes the JWT:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... - Token Verification: The server verifies the signature, checks
exp, and grants or denies access. - Token Refresh: When the access token expires, the client uses a long-lived refresh token to obtain a new one.
Client Auth Server Resource Server
| | |
|-- POST /login ---->| |
|<-- 200 + JWT ------| |
| | |
|-- GET /api (Authorization: Bearer JWT) ->|
| | verify JWT |
|<----------- 200 + data ----------------->|
Security Considerations
The Payload Is NOT Encrypted
Base64url encoding is not encryption. Anyone who holds a JWT can decode and read all claims instantly. Never include in a JWT payload:
- Passwords or password hashes
- Credit card or financial data
- Sensitive PII beyond what is necessary
- Private keys or internal secrets
Always Verify the Signature
// Node.js — jsonwebtoken library
const jwt = require("jsonwebtoken");
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
console.log(decoded.sub); // Trusted user ID
} catch (err) {
res.status(401).json({ error: "Invalid or expired token" });
}
Use Short Expiration Times
Access tokens should have short lifespans (15 minutes to 1 hour). Use refresh tokens for long-lived sessions to limit damage from token theft.
Always Use HTTPS
Never transmit tokens over unencrypted connections. An intercepted token is as good as a stolen password.
Common Vulnerabilities
1. Algorithm Confusion Attack (CVE-2015-9235)
An attacker changes alg in the header from RS256 to HS256, then signs the token using the server's public key as the HMAC secret. A naive server accepts it.
Mitigation: Always specify the expected algorithm explicitly:
jwt.verify(token, publicKey, { algorithms: ["RS256"] });
2. The "none" Algorithm Attack
If a server accepts alg: "none", an attacker can craft tokens with no signature at all.
Mitigation: Explicitly reject tokens with alg: "none". Modern libraries have patched this, but older versions remain vulnerable.
3. Weak Secrets (HS256)
A short or guessable HMAC secret can be brute-forced offline using tools like hashcat against a captured token.
Mitigation: Use a cryptographically random secret of at least 256 bits:
openssl rand -hex 32
4. Missing Claim Validation
Failing to validate iss, aud, or nbf allows cross-service token reuse or premature token use.
5. Insecure Token Storage
localStorage is accessible to any JavaScript on the page, making tokens vulnerable to XSS. Prefer HttpOnly cookies.
JWT vs Session Tokens: Stateless vs Stateful
| Feature | JWT (Stateless) | Session Token (Stateful) |
|---|---|---|
| Storage | Client-side | Server-side (DB or cache) |
| Scalability | Excellent — no server lookup required | Requires a shared session store |
| Revocation | Difficult — valid until exp |
Easy — delete session from store |
| Token size | Larger per request | Small opaque token, data on server |
| Microservices | Ideal — verify independently | Requires shared session infrastructure |
| Payload visibility | Readable by anyone with the token | Data stays server-side |
Choose JWT when building distributed systems, stateless REST APIs, or mobile apps. Choose sessions when you need instant revocation (e.g., "log out everywhere").
The Revocation Challenge
Because JWTs are self-contained, you cannot cancel a token before it expires without extra infrastructure:
- Short expiry + refresh tokens: Access tokens expire fast; revoke refresh tokens in the DB.
- Token blocklist: Store revoked
jtivalues in Redis (reintroduces some statefulness). - Secret rotation: Rotate the signing secret to invalidate all tokens (disrupts all users).
OAuth 2.0 and OpenID Connect
OAuth 2.0
OAuth 2.0 is an authorization framework. JWTs are widely used as access tokens because they are self-contained and carry authorization scopes:
{
"iss": "https://auth.example.com",
"sub": "user_123",
"aud": "https://api.example.com",
"scope": "read:profile write:posts",
"exp": 1893456000
}
OpenID Connect (OIDC)
OpenID Connect is an identity layer on top of OAuth 2.0. It introduces the ID Token, always a JWT, containing identity claims about the authenticated user:
{
"iss": "https://accounts.google.com",
"sub": "110169484474386276334",
"aud": "my-client-id.apps.googleusercontent.com",
"email": "[email protected]",
"name": "Jane Doe",
"exp": 1893456000,
"iat": 1893452400
}
OIDC also defines the JWKS endpoint (/.well-known/jwks.json), where the authorization server publishes its public keys. Any service can fetch these keys to verify tokens without a prior key exchange.
Best Practices
- Use asymmetric algorithms (RS256/ES256) for public-facing systems — protect the private signing key while letting anyone verify with the public key.
- Set short
expvalues for access tokens — 15 minutes is a common safe default. - Always rotate refresh tokens — issue a new one each time it is used; invalidate the old one.
- Validate all relevant claims — check
iss,aud,exp, andnbfat minimum. - Specify the expected algorithm explicitly — never rely solely on the
algfield in the token header. - Never put sensitive data in the payload — it is encoded, not encrypted.
- Always use HTTPS — never transmit tokens over unencrypted connections.
- Store tokens securely — prefer
HttpOnly,Secure,SameSite=Strictcookies over localStorage. - Implement a token revocation strategy — use a blocklist or short-lived tokens with refresh rotation.
- Keep JWT libraries up to date — security vulnerabilities are patched in newer releases.
FAQ
Q: Can I decrypt a JWT payload? A: JWT payloads are Base64url-encoded, not encrypted — no key is needed to read them. If you need confidentiality, use JWE (JSON Web Encryption, RFC 7516), which encrypts the payload.
Q: What is the difference between decode and verify?
A: decode simply Base64url-decodes the parts and returns JSON with no security checks. verify also validates the signature against your key and checks time-based claims like exp. Always use verify in production.
Q: How do I handle token refresh securely? A: Issue a short-lived access token (15–60 min) and a long-lived refresh token (days to weeks). Store refresh tokens server-side so they can be revoked. When the access token expires, the client sends the refresh token to a dedicated endpoint to get a new access token.
Q: Can I use JWT for both authentication and authorization?
A: Yes. Include identity claims (who the user is) and authorization claims (what they can do, e.g., roles, scope) in the same token. Keep the payload size reasonable.
Q: What happens if my JWT secret leaks? A: An attacker can forge valid tokens for any user. Rotate the secret immediately (this invalidates all existing tokens), investigate the breach, and force all users to re-authenticate.
Q: Should I use HS256 or RS256? A: Use RS256 (or ES256) when multiple services verify tokens or when the verifier is not fully trusted. Use HS256 only when the same service both issues and verifies tokens within a tightly controlled boundary.
Q: How large can a JWT get? A: Keep JWTs under 4 KB to avoid HTTP header size limits and performance issues. Do not embed large data structures; reference them by ID instead.