JWT Implementation Patterns: Complete Reference

Type: Software Reference Confidence: 0.92 Sources: 8 Verified: 2026-02-24 Freshness: 2026-02-24

TL;DR

Constraints

Quick Reference

Algorithm Comparison

AlgorithmTypeKey SizePerformanceUse CaseSecurity Level
HS256Symmetric (HMAC-SHA256)256-bit secretFastestSingle-service auth, internal APIsHigh (if secret is strong)
HS384Symmetric (HMAC-SHA384)384-bit secretFastHigher security symmetricHigh
HS512Symmetric (HMAC-SHA512)512-bit secretFastMaximum symmetric securityHigh
RS256Asymmetric (RSA-SHA256)2048-bit+ RSASlower sign, fast verifyMulti-service, public key distributionHigh
RS384Asymmetric (RSA-SHA384)2048-bit+ RSASlowerHigher security RSAHigh
RS512Asymmetric (RSA-SHA512)2048-bit+ RSASlowest RSAMaximum RSA securityHigh
ES256Asymmetric (ECDSA P-256)256-bit ECFast sign and verifyMobile, IoT, modern APIsHigh
ES384Asymmetric (ECDSA P-384)384-bit ECFastHigher security ECDSAHigh
ES512Asymmetric (ECDSA P-521)521-bit ECModerateMaximum ECDSA securityHigh
EdDSAAsymmetric (Ed25519)256-bit EdDSAFastest asymmetricModern systems, highest performanceHigh
PS256Asymmetric (RSASSA-PSS)2048-bit+ RSASimilar to RS256Compliance-driven, FIPSHigh
noneNoneN/AN/ANEVER use in productionNone

Token Lifetime Reference

Token TypeRecommended TTLStorageRevocable
Access token5-15 minutesMemory / httpOnly cookieNot without denylist
Refresh token7-30 dayshttpOnly secure cookie / server DBYes (delete from DB)
ID token (OIDC)5-60 minutesMemory onlyNot needed (one-time use)
Service-to-service5-60 minutesEnv variable / secrets managerRotate keys

Decision Tree

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

Step-by-Step Guide

1. Generate signing keys

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

2. Create and sign a JWT

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.

3. Validate and decode on the server

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

4. Implement refresh token rotation

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.

5. Set up a JWKS endpoint (asymmetric only)

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"

Code Examples

Python (PyJWT): Full Authentication Middleware

# 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

Node.js (jose): Token Creation and Verification

// 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",
});

Go (golang-jwt/jwt): Middleware Pattern

// 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)
        })
    }
}

Java (jjwt): Token Generation and Parsing

// 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();

Anti-Patterns

Wrong: Using decode() instead of verify()

# BAD -- decode() skips signature verification entirely
payload = jwt.decode(token, options={"verify_signature": False})
user_id = payload["sub"]  # Trusting unverified claims!

Correct: Always use verify() with full validation

# GOOD -- verify() checks signature + all claims
payload = jwt.decode(token, public_key,
    algorithms=["RS256"],
    audience="https://app.example.com",
    issuer="https://api.example.com")

Wrong: Trusting the alg header from the token

// 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!

Correct: Hard-code the expected algorithm

// GOOD -- algorithm is fixed server-side, ignoring token header
const decoded = jwt.verify(token, publicKey, { algorithms: ["RS256"] });

Wrong: Storing JWTs in localStorage

// BAD -- localStorage is accessible to any JS on the page (XSS vulnerable)
localStorage.setItem("token", jwt);
// Any XSS exploit can steal: localStorage.getItem("token")

Correct: Use httpOnly secure cookies

// GOOD -- httpOnly cookies are inaccessible to JavaScript
res.cookie("access_token", jwt, {
  httpOnly: true, secure: true, sameSite: "Strict",
  maxAge: 900000, path: "/api"
});

Wrong: Using symmetric keys in distributed systems

// BAD -- sharing HMAC secret with every microservice
const token = jwt.sign(payload, "shared-secret-across-10-services", { algorithm: "HS256" });

Correct: Use asymmetric keys with JWKS

// 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);

Wrong: JWTs as long-lived sessions without revocation

# BAD -- 30-day JWT with no revocation mechanism
payload = {"sub": "user-123", "exp": now + timedelta(days=30)}
token = jwt.encode(payload, key, algorithm="RS256")

Correct: Short-lived access + revocable refresh

# 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

Common Pitfalls

Diagnostic Commands

# 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"

Version History & Compatibility

Standard / LibraryVersionStatusKey Changes
RFC 7519 (JWT)1.0Current (since 2015)Original JWT specification
RFC 8725 (BCP)1.0Current (since 2020)JWT Best Current Practices -- security hardening
PyJWT (Python)2.8+Currentalgorithms param required since 2.x (breaking from 1.x)
jsonwebtoken (Node)9.xCurrentalgorithms option enforced by default
jose (Node)5.xCurrentModern, standards-compliant; recommended for new projects
golang-jwt/jwt (Go)v5.xCurrentReplaced dgrijalva/jwt-go; WithValidMethods() enforced
jjwt (Java)0.12.xCurrentFluent builder API; explicit algorithm selection

When to Use / When Not to Use

Use WhenDon't Use WhenUse Instead
Stateless auth across microservicesNeed immediate token revocationServer-side sessions with Redis/DB
Short-lived access tokens (5-15 min)Storing sensitive user data in tokenOpaque tokens with server-side data store
SSO via OpenID ConnectToken needs to exceed 4KBReference tokens (opaque ID)
Service-to-service auth (M2M)Client cannot securely store tokensOAuth2 auth code with backend-for-frontend
API auth where latency mattersNeed to track active sessionsDatabase-backed session tokens
Mobile app auth with offline verificationRegulatory server-side audit requirementDatabase-backed session tokens
Signed data exchange between trusted partiesToken contents must be hidden from clientJWE (encrypted JWT) or opaque tokens

Important Caveats

Related Units