jose (Node.js), PyJWT (Python), golang-jwt/jwt (Go), jjwt (Java)alg header from the token; hard-code the expected algorithm on the server.verify(), never decode() alone -- unsigned tokens grant full access if only decodedalg: "none" in production -- it completely disables signature verificationalg header (prevents algorithm confusion attacks)exp, nbf, iss, and aud claims -- missing any one enables token reuse across services or after expiration| Algorithm | Type | Key Size | Performance | Use Case | Security Level |
|---|---|---|---|---|---|
| HS256 | Symmetric (HMAC-SHA256) | 256-bit secret | Fastest | Single-service auth, internal APIs | High (if secret is strong) |
| HS384 | Symmetric (HMAC-SHA384) | 384-bit secret | Fast | Higher security symmetric | High |
| HS512 | Symmetric (HMAC-SHA512) | 512-bit secret | Fast | Maximum symmetric security | High |
| RS256 | Asymmetric (RSA-SHA256) | 2048-bit+ RSA | Slower sign, fast verify | Multi-service, public key distribution | High |
| RS384 | Asymmetric (RSA-SHA384) | 2048-bit+ RSA | Slower | Higher security RSA | High |
| RS512 | Asymmetric (RSA-SHA512) | 2048-bit+ RSA | Slowest RSA | Maximum RSA security | High |
| ES256 | Asymmetric (ECDSA P-256) | 256-bit EC | Fast sign and verify | Mobile, IoT, modern APIs | High |
| ES384 | Asymmetric (ECDSA P-384) | 384-bit EC | Fast | Higher security ECDSA | High |
| ES512 | Asymmetric (ECDSA P-521) | 521-bit EC | Moderate | Maximum ECDSA security | High |
| EdDSA | Asymmetric (Ed25519) | 256-bit EdDSA | Fastest asymmetric | Modern systems, highest performance | High |
| PS256 | Asymmetric (RSASSA-PSS) | 2048-bit+ RSA | Similar to RS256 | Compliance-driven, FIPS | High |
| none | None | N/A | N/A | NEVER use in production | None |
| Token Type | Recommended TTL | Storage | Revocable |
|---|---|---|---|
| Access token | 5-15 minutes | Memory / httpOnly cookie | Not without denylist |
| Refresh token | 7-30 days | httpOnly secure cookie / server DB | Yes (delete from DB) |
| ID token (OIDC) | 5-60 minutes | Memory only | Not needed (one-time use) |
| Service-to-service | 5-60 minutes | Env variable / secrets manager | Rotate keys |
START: What is the token for?
+-- Authentication (user login)?
| +-- Single service (monolith)?
| | +-- YES -> HS256 with strong secret (>= 256 bits)
| | | Access token: 15min, Refresh: 7 days (opaque, server-stored)
| | +-- NO |
| +-- Multiple services (microservices)?
| | +-- YES -> RS256 or ES256 with key pair
| | | Publish public key via JWKS endpoint
| | | Access token: 5-15min, Refresh: opaque token in DB
| | +-- NO |
| +-- Mobile / IoT clients?
| +-- YES -> ES256 (compact signatures, fast on constrained devices)
| Store refresh token in secure storage (Keychain/Keystore)
|
+-- Service-to-service (machine auth)?
| +-- Same trust boundary?
| | +-- YES -> HS256 with shared secret from secrets manager
| | +-- NO |
| +-- Cross-boundary / zero trust?
| +-- YES -> RS256/ES256 with JWKS, validate iss + aud strictly
|
+-- Data exchange (signed claims)?
| +-- -> RS256 or EdDSA; include iat + exp + custom claims
| Recipient verifies with issuer's public key
|
+-- Need payload confidentiality?
+-- -> Use JWE (RFC 7516), not JWS -- JWTs do NOT encrypt
Choose your algorithm and generate appropriate keys. [src1]
# RS256: Generate RSA key pair (2048-bit minimum, 4096 recommended)
openssl genrsa -out private.pem 4096
openssl rsa -in private.pem -pubout -out public.pem
# ES256: Generate ECDSA P-256 key pair
openssl ecparam -name prime256v1 -genkey -noout -out ec-private.pem
openssl ec -in ec-private.pem -pubout -out ec-public.pem
# HS256: Generate a cryptographically random secret (>= 256 bits)
openssl rand -base64 32
Verify: openssl rsa -in private.pem -check -> expected: RSA key ok
Build the token with required claims and sign it. [src3]
# Python (PyJWT >= 2.8.0)
import jwt, datetime
payload = {
"sub": "user-123",
"iss": "https://api.example.com",
"aud": "https://app.example.com",
"iat": datetime.datetime.now(datetime.timezone.utc),
"exp": datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(minutes=15),
"nbf": datetime.datetime.now(datetime.timezone.utc),
"roles": ["user", "admin"],
"jti": "unique-token-id-abc"
}
with open("private.pem", "r") as f:
private_key = f.read()
token = jwt.encode(payload, private_key, algorithm="RS256")
Verify: Paste the token at jwt.io -- decoded payload should match your claims.
Always verify signature and validate all standard claims. [src2]
# Python -- SECURE validation
decoded = jwt.decode(
token, public_key,
algorithms=["RS256"], # Hard-code allowed algorithms!
audience="https://app.example.com",
issuer="https://api.example.com",
options={"require": ["exp", "iss", "aud", "sub"]}
)
Verify: Pass an expired token -> should raise ExpiredSignatureError
Use opaque refresh tokens stored server-side for revocability. [src7]
import secrets, hashlib
refresh_token = secrets.token_urlsafe(64)
refresh_hash = hashlib.sha256(refresh_token.encode()).hexdigest()
# Store hash in DB: { user_id, refresh_hash, expires_at, created_at, revoked }
Verify: Revoking a refresh token in DB should prevent new access tokens from being issued.
Publish public keys so verifying services can fetch them dynamically. [src1]
// GET /.well-known/jwks.json
{
"keys": [{
"kty": "RSA", "use": "sig", "alg": "RS256",
"kid": "key-2026-02",
"n": "<base64url-encoded modulus>", "e": "AQAB"
}]
}
Verify: curl https://api.example.com/.well-known/jwks.json | jq '.keys[0].alg' -> "RS256"
# Input: HTTP request with Authorization: Bearer <token>
# Output: Authenticated user context or 401 response
# pip install PyJWT>=2.8.0 cryptography>=42.0
import jwt
from functools import wraps
from flask import request, jsonify, g
PUBLIC_KEY = open("public.pem").read()
ALLOWED_ALGORITHMS = ["RS256"] # Hard-coded, never from token
def require_auth(f):
@wraps(f)
def decorated(*args, **kwargs):
auth_header = request.headers.get("Authorization", "")
if not auth_header.startswith("Bearer "):
return jsonify({"error": "Missing bearer token"}), 401
token = auth_header[7:]
try:
g.user = jwt.decode(token, PUBLIC_KEY,
algorithms=ALLOWED_ALGORITHMS,
audience="https://app.example.com",
issuer="https://api.example.com",
options={"require": ["exp", "sub", "iss", "aud"]})
except jwt.ExpiredSignatureError:
return jsonify({"error": "Token expired"}), 401
except jwt.InvalidTokenError as e:
return jsonify({"error": f"Invalid token: {e}"}), 401
return f(*args, **kwargs)
return decorated
// Input: User credentials (after authentication)
// Output: Signed JWT access token
// npm install jose@5
import { SignJWT, jwtVerify, importPKCS8, importSPKI } from "jose";
import { readFileSync } from "fs";
const privateKey = await importPKCS8(readFileSync("private.pem", "utf-8"), "RS256");
const publicKey = await importSPKI(readFileSync("public.pem", "utf-8"), "RS256");
// Sign
const token = await new SignJWT({ sub: "user-123", roles: ["admin"] })
.setProtectedHeader({ alg: "RS256", typ: "JWT" })
.setIssuer("https://api.example.com")
.setAudience("https://app.example.com")
.setExpirationTime("15m")
.setIssuedAt()
.sign(privateKey);
// Verify
const { payload } = await jwtVerify(token, publicKey, {
algorithms: ["RS256"],
issuer: "https://api.example.com",
audience: "https://app.example.com",
});
// Input: HTTP request with Authorization header
// Output: Validated claims or 401 error
// go get github.com/golang-jwt/jwt/v5
func JWTMiddleware(publicKey *rsa.PublicKey) func(http.Handler) http.Handler {
parser := jwt.NewParser(
jwt.WithValidMethods([]string{"RS256"}),
jwt.WithIssuer("https://api.example.com"),
jwt.WithAudience("https://app.example.com"),
jwt.WithExpirationRequired(),
)
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
auth := r.Header.Get("Authorization")
if !strings.HasPrefix(auth, "Bearer ") {
http.Error(w, "missing token", http.StatusUnauthorized)
return
}
token, err := parser.Parse(auth[7:], func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodRSA); !ok {
return nil, fmt.Errorf("unexpected method: %v", t.Header["alg"])
}
return publicKey, nil
})
if err != nil || !token.Valid {
http.Error(w, "invalid token", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}
}
// Input: Authenticated user details
// Output: Signed JWT string
// implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
String token = Jwts.builder()
.subject("user-123")
.issuer("https://api.example.com")
.audience().add("https://app.example.com").and()
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + 900_000))
.signWith(keyPair.getPrivate(), Jwts.SIG.RS256)
.compact();
// Verify
var claims = Jwts.parser()
.verifyWith(keyPair.getPublic())
.requireIssuer("https://api.example.com")
.requireAudience("https://app.example.com")
.build()
.parseSignedClaims(token)
.getPayload();
# BAD -- decode() skips signature verification entirely
payload = jwt.decode(token, options={"verify_signature": False})
user_id = payload["sub"] # Trusting unverified claims!
# GOOD -- verify() checks signature + all claims
payload = jwt.decode(token, public_key,
algorithms=["RS256"],
audience="https://app.example.com",
issuer="https://api.example.com")
// BAD -- attacker can switch alg to "none" or "HS256" with public key as secret
const decoded = jwt.verify(token, key); // Algorithm derived from token header!
// GOOD -- algorithm is fixed server-side, ignoring token header
const decoded = jwt.verify(token, publicKey, { algorithms: ["RS256"] });
// BAD -- localStorage is accessible to any JS on the page (XSS vulnerable)
localStorage.setItem("token", jwt);
// Any XSS exploit can steal: localStorage.getItem("token")
// GOOD -- httpOnly cookies are inaccessible to JavaScript
res.cookie("access_token", jwt, {
httpOnly: true, secure: true, sameSite: "Strict",
maxAge: 900000, path: "/api"
});
// BAD -- sharing HMAC secret with every microservice
const token = jwt.sign(payload, "shared-secret-across-10-services", { algorithm: "HS256" });
// GOOD -- only auth service has private key; verifiers use public key
const token = new SignJWT(payload)
.setProtectedHeader({ alg: "RS256", kid: "key-2026-02" })
.sign(privateKey);
# BAD -- 30-day JWT with no revocation mechanism
payload = {"sub": "user-123", "exp": now + timedelta(days=30)}
token = jwt.encode(payload, key, algorithm="RS256")
# GOOD -- 15-min access token + server-side refresh token
access_payload = {"sub": "user-123", "exp": now + timedelta(minutes=15)}
access_token = jwt.encode(access_payload, private_key, algorithm="RS256")
refresh_token = secrets.token_urlsafe(64) # Opaque, stored in DB
leeway parameter to verification. [src2]kid in the JWT header and match it against your JWKS endpoint. [src1]sub. Token should be < 1KB. [src3]iss claim. Fix: Always validate iss against a whitelist of trusted issuers. [src2]# Decode a JWT without verification (inspect claims)
echo "eyJhbGciOiJSUz..." | cut -d. -f2 | base64 -d 2>/dev/null | python3 -m json.tool
# Check RSA key pair match (modulus should be identical)
openssl rsa -in private.pem -modulus -noout | openssl md5
openssl rsa -pubin -in public.pem -modulus -noout | openssl md5
# Generate a strong HS256 secret (256 bits)
openssl rand -base64 32
# Check token expiration
echo "eyJhbGci..." | cut -d. -f2 | base64 -d 2>/dev/null | python3 -c \
"import sys,json,datetime; d=json.load(sys.stdin); print('Expires:', datetime.datetime.fromtimestamp(d['exp']))"
# Test JWKS endpoint
curl -s https://api.example.com/.well-known/jwks.json | python3 -m json.tool
# Verify ES256 key curve
openssl ec -in ec-private.pem -text -noout 2>&1 | grep "ASN1 OID: prime256v1"
| Standard / Library | Version | Status | Key Changes |
|---|---|---|---|
| RFC 7519 (JWT) | 1.0 | Current (since 2015) | Original JWT specification |
| RFC 8725 (BCP) | 1.0 | Current (since 2020) | JWT Best Current Practices -- security hardening |
| PyJWT (Python) | 2.8+ | Current | algorithms param required since 2.x (breaking from 1.x) |
| jsonwebtoken (Node) | 9.x | Current | algorithms option enforced by default |
| jose (Node) | 5.x | Current | Modern, standards-compliant; recommended for new projects |
| golang-jwt/jwt (Go) | v5.x | Current | Replaced dgrijalva/jwt-go; WithValidMethods() enforced |
| jjwt (Java) | 0.12.x | Current | Fluent builder API; explicit algorithm selection |
| Use When | Don't Use When | Use Instead |
|---|---|---|
| Stateless auth across microservices | Need immediate token revocation | Server-side sessions with Redis/DB |
| Short-lived access tokens (5-15 min) | Storing sensitive user data in token | Opaque tokens with server-side data store |
| SSO via OpenID Connect | Token needs to exceed 4KB | Reference tokens (opaque ID) |
| Service-to-service auth (M2M) | Client cannot securely store tokens | OAuth2 auth code with backend-for-frontend |
| API auth where latency matters | Need to track active sessions | Database-backed session tokens |
| Mobile app auth with offline verification | Regulatory server-side audit requirement | Database-backed session tokens |
| Signed data exchange between trusted parties | Token contents must be hidden from client | JWE (encrypted JWT) or opaque tokens |
jsonwebtoken npm package (v8.x and earlier) was vulnerable to algorithm confusion if algorithms was not specified explicitly. Always pin to v9+ and specify algorithms.alg: "none" by default; PyJWT 2.x requires explicit algorithms parameter. Ensure you are on PyJWT >= 2.0.jti values) if you need emergency revocation.