JWT Tokens Explained: Structure, Security, and Best Practices
· 10 min read
JWT Structure
A JWT consists of three Base64URL-encoded parts separated by dots:
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U
|---- Header ----|.|----- Payload ------|.|----- Signature -----|
| Part | Contains | Example (decoded) |
|---|---|---|
| Header | Algorithm + token type | {"alg": "HS256", "typ": "JWT"} |
| Payload | Claims (user data) | {"sub": "1234567890", "name": "John", "iat": 1516239022} |
| Signature | Verification hash | HMAC-SHA256(header + "." + payload, secret) |
Decode any JWT with our JWT Decoder or generate test tokens with JWT Generator.
Standard Claims (RFC 7519)
| Claim | Name | Purpose |
|---|---|---|
iss | Issuer | Who created the token |
sub | Subject | Who the token is about (user ID) |
aud | Audience | Who the token is intended for |
exp | Expiration | When the token expires (Unix timestamp) |
nbf | Not Before | Token is not valid before this time |
iat | Issued At | When the token was created |
jti | JWT ID | Unique identifier (for revocation) |
Authentication Flow
- User sends credentials (username + password) to login endpoint
- Server verifies credentials, creates JWT with user claims, signs it
- Server returns JWT to client
- Client stores JWT (memory, httpOnly cookie, or localStorage)
- Client sends JWT in Authorization header:
Bearer eyJhbG... - Server verifies signature and extracts claims — no database lookup needed
// Node.js example with jsonwebtoken
const jwt = require('jsonwebtoken');
// Create token
const token = jwt.sign(
{ userId: 123, role: 'admin' },
process.env.JWT_SECRET,
{ expiresIn: '15m' }
);
// Verify token
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
console.log(decoded.userId); // 123
} catch (err) {
// Token invalid or expired
}
Security Best Practices
| Do | Don't |
|---|---|
| Use short expiration (15 min) | Set tokens to never expire |
| Store in httpOnly cookies | Store in localStorage (XSS vulnerable) |
| Use RS256 for distributed systems | Use HS256 with weak secrets |
| Validate all claims (iss, aud, exp) | Only check the signature |
| Use a strong secret (256+ bits) | Use "secret" or short passwords |
| Implement token revocation | Assume tokens can't be invalidated |
Common attacks:
alg: "none"attack — Always reject tokens with algorithm "none"- Key confusion — Don't accept HS256 when expecting RS256
- Payload tampering — Always verify the signature before trusting claims
Refresh Tokens
Access tokens should be short-lived (15 min). Refresh tokens are long-lived (7-30 days) and used to get new access tokens without re-authentication:
- Login returns access token (15 min) + refresh token (7 days)
- Access token expires → client sends refresh token to /refresh endpoint
- Server validates refresh token, issues new access token
- Refresh token is rotated (old one invalidated, new one issued)
Refresh tokens should be stored server-side (database) so they can be revoked. Access tokens are stateless (no server storage needed).
Frequently Asked Questions
Is JWT encrypted?
Standard JWT (JWS) is signed but NOT encrypted. Anyone can decode the payload. Never put sensitive data (passwords, SSN) in a JWT. Use JWE (JSON Web Encryption) if you need encrypted tokens.
JWT vs session cookies?
JWTs are stateless (no server storage), work across domains, and scale horizontally. Sessions require server-side storage but are easier to revoke. Use sessions for traditional web apps, JWTs for APIs and microservices.
How do I revoke a JWT?
JWTs are stateless, so you can't directly revoke them. Options: short expiration + refresh tokens, token blacklist (Redis), or change the signing key (invalidates ALL tokens).
Should I use HS256 or RS256?
HS256 (symmetric) is simpler — one shared secret. RS256 (asymmetric) uses public/private keys — better for distributed systems where multiple services verify tokens but only one signs them.