JWT Authentication
Sign, verify, and refresh JWTs in Python, JavaScript, and Java — access/refresh flows, RS256 key pairs, and token revocation done right.
Overview
JSON Web Tokens (JWT) remain the default way to do stateless authentication in modern APIs. A JWT is a compact, URL-safe string that carries signed claims — such as user identity and expiration time — between a client and a server.
Below: how to sign, verify, and refresh JWTs safely in Python, JavaScript, and Java — including the refresh-flow and revocation pieces most tutorials skip.
When to Use
Use JWTs when:
- Building a stateless REST API where sessions shouldn’t be stored server-side
- Authenticating microservices that call each other internally
- Issuing short-lived access tokens with separate long-lived refresh tokens
- Adding SSO or third-party login (OAuth2 / OpenID Connect)
Avoid JWTs when:
- You need immediate server-side token revocation (use sessions + a token blocklist instead)
- The payload is very large (JWTs are sent on every request)
- You aren’t prepared to rotate signing keys securely
Solution
Python (PyJWT)
import jwt
from datetime import datetime, timedelta, UTC
SECRET = "your-256-bit-secret" # store in env, never in code
ALGORITHM = "HS256"
def create_token(user_id: str, expires_minutes: int = 15) -> str:
payload = {
"sub": user_id,
"iat": datetime.now(UTC),
"exp": datetime.now(UTC) + timedelta(minutes=expires_minutes),
}
return jwt.encode(payload, SECRET, algorithm=ALGORITHM)
def verify_token(token: str) -> dict:
try:
return jwt.decode(token, SECRET, algorithms=[ALGORITHM])
except jwt.ExpiredSignatureError:
raise ValueError("Token has expired")
except jwt.InvalidTokenError:
raise ValueError("Invalid token")
# Usage
token = create_token("user-123")
claims = verify_token(token)
print(claims["sub"]) # user-123
JavaScript (jsonwebtoken)
const jwt = require('jsonwebtoken');
const SECRET = process.env.JWT_SECRET; // 256-bit secret from env
function createToken(userId, expiresIn = '15m') {
return jwt.sign({ sub: userId }, SECRET, { expiresIn });
}
function verifyToken(token) {
try {
return jwt.verify(token, SECRET);
} catch (err) {
if (err.name === 'TokenExpiredError') throw new Error('Token has expired');
throw new Error('Invalid token');
}
}
// Usage
const token = createToken('user-123');
const claims = verifyToken(token);
console.log(claims.sub); // user-123
Java (JJWT 0.12+)
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
public class JwtUtil {
private static final SecretKey KEY =
Keys.hmacShaKeyFor(System.getenv("JWT_SECRET").getBytes(StandardCharsets.UTF_8));
private static final long ACCESS_TTL = 15 * 60 * 1000L; // 15 min
public String createToken(String userId) {
Date now = new Date();
return Jwts.builder()
.subject(userId)
.issuedAt(now)
.expiration(new Date(now.getTime() + ACCESS_TTL))
.signWith(KEY) // HS256 inferred from the key
.compact();
}
public Claims verifyToken(String token) {
try {
return Jwts.parser()
.verifyWith(KEY)
.build()
.parseSignedClaims(token)
.getPayload();
} catch (ExpiredJwtException e) {
throw new IllegalArgumentException("Token has expired");
} catch (JwtException e) {
throw new IllegalArgumentException("Invalid token");
}
}
}
Explanation
A JWT has three parts separated by dots: header.payload.signature.
- Header: tells the verifier which algorithm (
alg) produced the signature and the token type (typ). - Payload: the claim set — who the token is about (
sub), when it was issued (iat), and when it dies (exp). - Signature: the integrity check. You get it by signing
base64(header) + "." + base64(payload)with your secret key — one flipped bit in the payload and verification fails.
Security notes:
- Always use HTTPS in production — JWTs are bearer tokens; intercepting one is game over.
- Store the signing secret in an environment variable or a secrets manager (AWS Secrets Manager, HashiCorp Vault, etc.).
- Prefer short expiration times (5–15 minutes) and issue refresh tokens for longer sessions.
- Use
HS256only when a single service signs and verifies. Once a second service needs to verify, switch to asymmetricRS256with a public/private key pair.
Variants
Refresh Token Flow
Access tokens stay short-lived — 5 to 15 minutes is the usual range. When they expire, the client sends the refresh token to get a new access token without re-authenticating.
import jwt
import secrets
from datetime import datetime, timedelta, UTC
SECRET = "your-256-bit-secret"
ALGORITHM = "HS256"
REFRESH_TTL_DAYS = 7
# Store refresh tokens in a database or Redis keyed by user ID
refresh_store = {} # In production, use Redis or a database
def create_access_token(user_id: str) -> str:
payload = {
"sub": user_id,
"type": "access",
"iat": datetime.now(UTC),
"exp": datetime.now(UTC) + timedelta(minutes=15),
}
return jwt.encode(payload, SECRET, algorithm=ALGORITHM)
def create_refresh_token(user_id: str) -> str:
token_id = secrets.token_urlsafe(32)
payload = {
"sub": user_id,
"type": "refresh",
"jti": token_id,
"iat": datetime.now(UTC),
"exp": datetime.now(UTC) + timedelta(days=REFRESH_TTL_DAYS),
}
token = jwt.encode(payload, SECRET, algorithm=ALGORITHM)
refresh_store[token_id] = user_id
return token
def refresh_access_token(refresh_token: str) -> str:
try:
claims = jwt.decode(refresh_token, SECRET, algorithms=[ALGORITHM])
except jwt.ExpiredSignatureError:
raise ValueError("Refresh token has expired")
except jwt.InvalidTokenError:
raise ValueError("Invalid refresh token")
if claims.get("type") != "refresh":
raise ValueError("Not a refresh token")
token_id = claims.get("jti")
if token_id not in refresh_store:
raise ValueError("Refresh token has been revoked")
return create_access_token(claims["sub"])
def revoke_refresh_token(refresh_token: str) -> None:
try:
claims = jwt.decode(refresh_token, SECRET, algorithms=[ALGORITHM])
except jwt.InvalidTokenError:
return
token_id = claims.get("jti")
if token_id and token_id in refresh_store:
del refresh_store[token_id]
# Usage
access = create_access_token("user-123")
refresh = create_refresh_token("user-123")
# When the access token expires, get a new one
new_access = refresh_access_token(refresh)
# On logout, revoke the refresh token
revoke_refresh_token(refresh)
RS256 (Asymmetric) Variant
When more than one service needs to verify tokens, use RS256 so each service only needs the public key.
from cryptography.hazmat.primitives import serialization
import jwt
from datetime import datetime, timedelta, UTC
# Load private key (only on the auth service)
with open("private.pem", "rb") as f:
private_key = serialization.load_pem_private_key(f, password=None)
# Load public key (on every service that verifies)
with open("public.pem", "rb") as f:
public_key = serialization.load_pem_public_key(f)
def create_token_rs256(user_id: str) -> str:
payload = {"sub": user_id, "exp": datetime.now(UTC) + timedelta(minutes=15)}
return jwt.encode(payload, private_key, algorithm="RS256")
def verify_token_rs256(token: str) -> dict:
return jwt.decode(token, public_key, algorithms=["RS256"])
const jwt = require('jsonwebtoken');
const fs = require('fs');
const privateKey = fs.readFileSync('private.pem');
const publicKey = fs.readFileSync('public.pem');
function createTokenRS256(userId) {
return jwt.sign({ sub: userId }, privateKey, { algorithm: 'RS256', expiresIn: '15m' });
}
function verifyTokenRS256(token) {
return jwt.verify(token, publicKey, { algorithms: ['RS256'] });
}
Token Blocklist with Redis
import redis
import jwt
from datetime import datetime, UTC
r = redis.Redis(host="localhost", port=6379, db=0, decode_responses=True)
def verify_with_blocklist(token: str, secret: str) -> dict:
claims = jwt.decode(token, secret, algorithms=["HS256"])
jti = claims.get("jti")
if jti and r.exists(f"blocklist:{jti}"):
raise ValueError("Token has been revoked")
return claims
def revoke_token(token: str, secret: str) -> None:
claims = jwt.decode(token, secret, algorithms=["HS256"])
jti = claims.get("jti")
if jti:
# exp is a UNIX timestamp; keep the blocklist entry only until expiry
ttl = int(claims["exp"] - datetime.now(UTC).timestamp())
if ttl > 0:
r.setex(f"blocklist:{jti}", ttl, "revoked")
Troubleshooting
Signature verification failed: wrong secret/key, or an algorithm mismatch — a verifier expecting RS256 rejecting an HS256 token. Whitelist the accepted algorithms explicitly on every verifier.Token has expiredon valid-looking tokens: clock skew between services. Allow a smallleeway(30–60s) onjwt.decode/jwt.verify, and keep servers on NTP.- HTTP 431 Request Header Fields Too Large: the JWT grew too big for the request header budget (common when stuffing roles/claims). Trim claims or shorten the token.
- Refresh token replay: a stolen refresh token lets an attacker mint access tokens forever. Rotate refresh tokens on every use and invalidate the old
jti— the pattern in the refresh flow above is where to enforce it.
What Works
- Rotate keys regularly: implement a key version (
kidheader claim) so you can rotate without invalidating all active tokens. - Use refresh tokens: store them in
HttpOnly,Secure,SameSite=Strictcookies and keep access tokens in memory only — nothing readable from JS. - Validate the algorithm: pin the allowed list (
algorithms=['HS256']) — otherwise a forgedalg: nonetoken sails through. - Never put secrets in the payload: JWTs are base64-encoded, not encrypted. Anyone can read the payload.
- Log token IDs, not tokens: if you log authentication events, log
jti(token ID) orsub, never the full token string.
Common Mistakes
- Putting secrets in the JWT payload: anything sensitive is readable by whoever intercepts the token.
- Skipping algorithm checks: accepting
alg: none, or letting the token pick its own algorithm, is the classic JWT forgery. - Infinite-lived tokens: tokens without
expare dangerous — if leaked, they’re valid forever. - Using weak secrets: a short secret puts brute-forcing the HMAC signature within reach.
- Trusting the client to delete tokens: always enforce expiration server-side; clients can be compromised.
See Also
- RFC 7519 — JSON Web Token (JWT) — the spec: claims, signature formats, JWS/JWE
- jwt.io debugger — paste a token, inspect header/payload/signature interactively
- PyJWT documentation — claims validation,
leeway, algorithm options - OWASP JSON Web Token cheat sheet — hardening checklist for Java/JWT
- Companion repo — runnable token flows for Python, JavaScript, and Java
Frequently Asked Questions
Should I store JWTs in localStorage or cookies?
Access tokens live in memory (a variable). Refresh tokens go in HttpOnly, Secure, SameSite=Strict cookies so XSS can't read them.
How do I revoke a JWT before it expires?
Maintain a token blocklist (e.g., Redis with TTL matching token expiry) and check it on every verification. Alternatively, keep sessions server-side.
What's the difference between HS256 and RS256?
HS256 is symmetric — the same secret signs and verifies, so anyone holding it can also forge. RS256 splits the roles: a private key signs, and any service holding the public key can only verify. Reach for RS256 once two or more services verify tokens — sign with the private key, verify with the public one.
How long should a JWT access token last?
Keep access tokens at 5–15 minutes; refresh tokens usually sit in the 7–30 day range depending on how strict your security posture is. A short-lived access token means a leaked one is useless within minutes.
Can I use JWTs with WebSockets?
Yes. Pass the JWT as a query parameter during the WebSocket handshake (wss://example.com/ws?token=...) or in the Sec-WebSocket-Protocol header. Validate the token before accepting the connection — the initial validation is sufficient, don't resend tokens in regular messages.
What claims should I include in a JWT?
Standard claims: sub (user ID), iat (issued at), exp (expiration), jti (unique token ID for revocation). Optional: iss (issuer), aud (audience), roles, scope. Avoid PII or secrets in the payload — it's base64-encoded, not encrypted.
How do I rotate signing keys without invalidating all tokens?
Use the kid (key ID) header claim. During rotation, accept tokens signed with both the old and new keys for a transition period. Once all old tokens have expired, remove the old key. Publish public keys via a JWKS endpoint for RS256.
Related Resources
Implement Secure Session Management
How to create, validate, and expire user sessions securely using HTTP-only cookies, server-side stores like Redis, and CSRF protection.
RecipeOAuth 2.0 Login
How to implement OAuth 2.0 authentication with Google, GitHub, and other providers. Covers the Authorization Code flow with PKCE, state validation, and refresh tokens.
RecipeHow to Hash Passwords Securely (Python, JavaScript, Java)
Learn how to hash and verify passwords with bcrypt, Argon2, and PBKDF2. Practical examples in Python, JavaScript, and Java, plus migration steps and parameter trade-offs.
RecipeSecure API Key Authentication for Services and Clients
How to generate, distribute, validate, and rotate API keys for machine-to-machine authentication using HMAC signatures, scopes, and rate-limited key policies.
RecipeTwo-Factor Authentication (2FA / TOTP)
How to implement time-based one-time password (TOTP) two-factor authentication for secure user login.
GuideSecurity Best Practices Guide
A thorough guide to application security: authentication, authorization, input validation, secrets management, and common vulnerability prevention.