What Is a JWT?
A JSON Web Token (JWT), defined in RFC 7519, is a compact string that carries a JSON payload and a cryptographic signature. It looks like this:
1eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5cThree parts, separated by dots:
Anatomy: Header, Payload, Signature
1. Header
The header is a JSON object that declares the token type and signing algorithm:
1{2 "alg": "HS256",3 "typ": "JWT"4}Common algorithms: HS256 (symmetric HMAC), RS256 (asymmetric RSA), ES256 (ECDSA). The header is Base64URL-encoded, not encrypted.
2. Payload (Claims)
The payload carries claims — key-value pairs with information about the user and token:
1{2 "sub": "user_8742",3 "name": "Alice Chen",4 "email": "[email protected]",5 "role": "admin",6 "iat": 1711929600,7 "exp": 17119332008}Warning
Registered Claims (RFC 7519)
| Claim | Full Name | Purpose |
|---|---|---|
| iss | Issuer | Who created and signed the token |
| sub | Subject | Who the token represents (usually a user ID) |
| aud | Audience | Intended recipient (your API domain) |
| exp | Expiration | Unix timestamp when the token becomes invalid |
| nbf | Not Before | Token is invalid before this timestamp |
| iat | Issued At | When the token was created |
| jti | JWT ID | Unique token identifier (for revocation) |
3. Signature
The signature prevents tampering. For HS256:
1HMAC-SHA256(2 base64UrlEncode(header) + "." + base64UrlEncode(payload),3 secret4)If anyone changes a single character in the header or payload, the signature will not match and the token is rejected.
Decoding a JWT (No Secret Needed)
Because the header and payload are just Base64URL-encoded, you can decode them without knowing the secret. This is useful for debugging:
1function decodeJwt(token) {2 const [headerB64, payloadB64] = token.split('.');3 const header = JSON.parse(atob(headerB64.replace(/-/g, '+').replace(/_/g, '/')));4 const payload = JSON.parse(atob(payloadB64.replace(/-/g, '+').replace(/_/g, '/')));5 return { header, payload };6}78const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzg3NDIiLCJyb2xlIjoiYWRtaW4iLCJleHAiOjE3MTE5MzMyMDB9.abc123';9const { header, payload } = decodeJwt(token);1011console.log(header); // { alg: "HS256", typ: "JWT" }12console.log(payload); // { sub: "user_8742", role: "admin", exp: 1711933200 }Tip
Verifying a JWT
Verification means checking three things: (1) the signature is valid, (2) the token is not expired, and (3) the audience/issuer match your expectations.
1import jwt from 'jsonwebtoken';23const SECRET = process.env.JWT_SECRET;45try {6 const decoded = jwt.verify(token, SECRET, {7 algorithms: ['HS256'],8 audience: 'https://api.example.com',9 issuer: 'https://auth.example.com',10 });11 console.log('Valid token:', decoded);12} catch (err) {13 if (err.name === 'TokenExpiredError') {14 console.error('Token has expired at:', err.expiredAt);15 } else if (err.name === 'JsonWebTokenError') {16 console.error('Invalid token:', err.message);17 }18}1import jwt23SECRET = "your-secret-key"45try:6 decoded = jwt.decode(7 token,8 SECRET,9 algorithms=["HS256"],10 audience="https://api.example.com",11 )12 print("Valid:", decoded)13except jwt.ExpiredSignatureError:14 print("Token has expired")15except jwt.InvalidTokenError as e:16 print(f"Invalid token: {e}")The JWT Authentication Flow
Access Tokens vs Refresh Tokens
| Property | Access Token | Refresh Token |
|---|---|---|
| Lifetime | Short (5–15 minutes) | Long (hours to days) |
| Purpose | Authorize API requests | Get new access tokens |
| Stored in | Memory or HttpOnly cookie | HttpOnly cookie (never localStorage) |
| Sent to | Resource API | Auth server only |
| Contains | User claims, roles, permissions | Minimal: user ID + token ID |
| If stolen | Attacker has short window | Rotate immediately + invalidate |
Important
Signing Algorithms: HS256 vs RS256 vs ES256
| Algorithm | Type | Key | Best For |
|---|---|---|---|
| HS256 | Symmetric (HMAC) | Single shared secret | Simple apps where signer and verifier are the same server |
| RS256 | Asymmetric (RSA) | Private key signs, public key verifies | Microservices: auth server signs, other services verify with public key |
| ES256 | Asymmetric (ECDSA) | Smaller keys than RSA | Mobile apps, IoT — smaller token size and faster verification |
Common JWT Security Mistakes
1. Using 'none' Algorithm
Some libraries accept "alg": "none" in the header, which means no signature verification. An attacker can forge any payload.
1{2 "alg": "none",3 "typ": "JWT"4}Fix: Always explicitly specify allowed algorithms when verifying: algorithms: ['HS256'].
2. Storing Secrets in the Payload
The payload is not encrypted. Anyone who intercepts the token can Base64-decode it. Never include passwords, API keys, or PII beyond what is strictly necessary.
3. Not Validating exp, aud, and iss
If you only verify the signature but ignore expiration, audience, and issuer, a token meant for a different service or a stolen expired token can be reused.
4. Storing JWTs in localStorage
localStorage is accessible to any JavaScript on the page. A single XSS vulnerability exposes the token. Use HttpOnly cookies with SameSite=Strict and Secure flags instead.
5. Extremely Long Expiration Times
A JWT valid for 30 days means a stolen token gives the attacker a 30-day window. Use short-lived access tokens (5–15 min) with refresh token rotation.
JWT Best Practices
- ✓Always verify the signature and validate
exp,aud,issclaims - ✓Explicitly set
algorithmsin your verification library — never allow"none" - ✓Keep access tokens short-lived (5–15 minutes) and use refresh tokens for longer sessions
- ✓Store tokens in HttpOnly cookies, not localStorage
- ✓Use asymmetric algorithms (RS256/ES256) in distributed systems
- ✓Include a
jticlaim for token revocation via a server-side blocklist - ✓Minimize payload size — only include claims the API needs for authorization
- ✓Rotate signing keys periodically and support
kid(Key ID) in the header
Try It — Decode a JWT Payload
Paste a JWT payload (the middle section, between the two dots) into the editor. It should be valid JSON once decoded:
Try It Yourself
A valid JWT payload — experiment by adding custom claims