Skip to main content
โšก Calmops

JWT Authentication Best Practices: Security, Tokens, and Implementation

JSON Web Tokens (JWT) are the standard for stateless authentication in modern applications. But improper JWT implementation leads to security vulnerabilities. This guide covers JWT best practices for secure authentication.

Understanding JWT

JWT Structure

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                   JWT Structure                              โ”‚
โ”‚                                                             โ”‚
โ”‚   eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9                       โ”‚
โ”‚   .                                                        โ”‚
โ”‚   eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0   โ”‚
โ”‚   IjoxNTE2MjM5MDIyfQ                                        โ”‚
โ”‚   .                                                        โ”‚
โ”‚   SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c              โ”‚
โ”‚                                                             โ”‚
โ”‚   Header.Payload.Signature                                  โ”‚
โ”‚                                                             โ”‚
โ”‚   Header: Algorithm and token type                          โ”‚
โ”‚   Payload: Claims (data)                                    โ”‚
โ”‚   Signature: Verifies integrity                            โ”‚
โ”‚                                                             โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

JWT Components

import jwt
import base64
import json

# JWT Header
header = {
    "alg": "HS256",  # Algorithm
    "typ": "JWT"     # Type
}

# Encoded header
# eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

# JWT Payload (claims)
payload = {
    # Registered claims
    "iss": "my-app",           # Issuer
    "sub": "1234567890",       # Subject (user ID)
    "aud": "my-api",          # Audience
    "exp": 1516239022,        # Expiration time
    "nbf": 1516239022,        # Not before
    "iat": 1516239022,        # Issued at
    "jti": "unique-id",       # JWT ID
    
    # Custom claims
    "name": "John Doe",
    "email": "[email protected]",
    "role": "admin",
    "permissions": ["read", "write"]
}

# Signature
# HMAC-SHA256 of: base64UrlEncode(header) + "." + base64UrlEncode(payload)
# With secret key

JWT Types: JWS vs JWE

JWS (JSON Web Signature)

# JWS - Signed tokens (integrity only)

# Header for signed token
jws_header = {
    "alg": "RS256",  # RS256, HS256, ES256
    "typ": "JWT"
}

# Creating JWS
token = jwt.encode(
    payload,
    private_key,
    algorithm="RS256"
)

# Verifying JWS
decoded = jwt.decode(
    token,
    public_key,
    algorithms=["RS256"],
    audience="my-api",
    issuer="my-app"
)

JWE (JSON Web Encryption)

# JWE - Encrypted tokens (confidentiality + integrity)

# Header for encrypted token
jwe_header = {
    "alg": "RSA-OAEP",      # Key encryption
    "enc": "A256GCM",       # Content encryption
    "typ": "JWT"
}

# Creating JWE (encrypted)
token = jwt.encode(
    payload,
    public_key,
    algorithm="RSA-OAEP",
    encryption="A256GCM"
)

# Decoding JWE
decoded = jwt.decode(
    token,
    private_key,
    algorithms=["RSA-OAEP"],
    encryption="A256GCM"
)

Secure JWT Implementation

Token Generation

import jwt
import time
from datetime import datetime, timedelta
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import rsa

# Generate key pair (in production, store securely!)
private_key = rsa.generate_private_key(
    public_exponent=65537,
    key_size=2048
)
public_key = private_key.public_key()

def generate_token(user_id, email, role):
    """Generate secure JWT token"""
    
    now = datetime.utcnow()
    
    payload = {
        # Registered claims
        "iss": "my-app",                    # Your app
        "sub": str(user_id),                # User ID
        "aud": "my-api",                   # Your API
        "iat": int(now.timestamp()),        # Issued at
        "exp": int((now + timedelta(hours=1)).timestamp()),  # 1 hour
        "jti": generate_unique_id(),        # Unique token ID
        
        # Custom claims
        "email": email,
        "role": role,
        "type": "access"                   # Access token
    }
    
    # Sign with RS256 (asymmetric)
    token = jwt.encode(
        payload,
        private_key,
        algorithm="RS256",
        headers={"kid": "key-id-1"}
    )
    
    return token

Token Validation

def validate_token(token):
    """Validate JWT token securely"""
    
    try:
        # Decode with public key
        payload = jwt.decode(
            token,
            public_key,
            algorithms=["RS256"],
            audience="my-api",
            issuer="my-app",
            options={
                # Security options
                "verify_signature": True,
                "verify_exp": True,
                "verify_nbf": True,
                "verify_iat": True,
                "verify_aud": True,
                "verify_iss": True,
                "require": ["exp", "iss", "sub"]  # Required claims
            }
        )
        
        # Additional checks
        if payload.get("type") != "access":
            raise ValueError("Invalid token type")
        
        # Check for token revocation (optional)
        if is_token_revoked(payload.get("jti")):
            raise ValueError("Token revoked")
        
        return payload
        
    except jwt.ExpiredSignatureError:
        raise AuthenticationError("Token expired")
    except jwt.InvalidTokenError as e:
        raise AuthenticationError(f"Invalid token: {e}")

Refresh Tokens

def generate_token_pair(user_id, email):
    """Generate access and refresh tokens"""
    
    now = datetime.utcnow()
    
    # Access token (short-lived: 15 min - 1 hour)
    access_payload = {
        "sub": str(user_id),
        "email": email,
        "type": "access",
        "iat": int(now.timestamp()),
        "exp": int((now + timedelta(hours=1)).timestamp())
    }
    
    access_token = jwt.encode(
        access_payload,
        private_key,
        algorithm="RS256"
    )
    
    # Refresh token (long-lived: 7-30 days)
    refresh_payload = {
        "sub": str(user_id),
        "type": "refresh",
        "iat": int(now.timestamp()),
        "exp": int((now + timedelta(days=7)).timestamp()),
        "jti": generate_unique_id()  # For revocation
    }
    
    refresh_token = jwt.encode(
        refresh_payload,
        private_key,
        algorithm="RS256"
    )
    
    # Store refresh token JTI for revocation
    store_refresh_token(refresh_payload["jti"], user_id)
    
    return access_token, refresh_token


def refresh_access_token(refresh_token):
    """Exchange refresh token for new access token"""
    
    # Validate refresh token
    payload = jwt.decode(
        refresh_token,
        public_key,
        algorithms=["RS256"],
        options={"require": ["type", "jti"]}
    )
    
    if payload.get("type") != "refresh":
        raise AuthenticationError("Invalid token type")
    
    # Check not revoked
    if is_token_revoked(payload.get("jti")):
        raise AuthenticationError("Token revoked")
    
    # Get user info
    user = get_user(payload.get("sub"))
    
    # Generate new access token
    return generate_token(user.id, user.email, user.role)

Security Best Practices

Algorithm Security

# Algorithm recommendations

secure_algorithms = {
    "recommended": [
        "RS256",  # RSA with SHA-256 (asymmetric)
        "ES256",  # ECDSA with SHA-256 (asymmetric)
        "EdDSA"   # Edwards-curve DSA (asymmetric)
    ],
    
    "acceptable": [
        "HS256"   # HMAC with SHA-256 (symmetric, needs strong key)
    ],
    
    "never_use": [
        "none"    # ALGORITHM NONE ATTACK!
    ]
}

# NEVER trust algorithm from token!
# Always specify allowed algorithms
jwt.decode(token, key, algorithms=["RS256"])

Key Management

# Key management best practices

key_management = {
    "rotation": {
        "description": "Rotate keys regularly",
        "practice": "Rotate every 3-6 months",
        "implementation": "Use kid header for key ID"
    },
    
    "storage": {
        "description": "Store keys securely",
        "practice": "HS256: strong random key",
        "practice": "RS256/ES256: key vault or HSM"
    },
    
    "key_ids": {
        "description": "Identify which key was used",
        "practice": "Use 'kid' header",
        "example": '{"alg":"RS256","kid":"key-id-1"}'
    }
}

# Multiple keys for rotation
def get_signing_key(kid):
    keys = {
        "key-id-1": current_private_key,
        "key-id-2": previous_private_key
    }
    return keys.get(kid)

def get_verification_key(kid):
    keys = {
        "key-id-1": current_public_key,
        "key-id-2": previous_public_key
    }
    return keys.get(kid)

Token Storage

# Token storage security

storage_recommendations:
  access_token:
    - "Memory (JavaScript variable)"
    - "Avoid: localStorage (XSS vulnerable)"
    - "Avoid: Cookies (CSRF vulnerable without protection)"
    
  refresh_token:
    - "HTTP-only cookie"
    - "Server-side storage"
    - "Encrypted if stored client-side"

security_headers:
  - "Set Secure flag on cookies"
  - "Set HttpOnly flag on cookies"
  - "Set SameSite=strict or lax"
  - "Use CSRF tokens with cookies"

Implementation Examples

Flask JWT Implementation

from flask import Flask, request, jsonify
import jwt
from functools import wraps

app = Flask(__name__)

# Configuration
app.config['JWT_SECRET'] = get_jwt_secret()  # From secure vault
app.config['JWT_ALGORITHM'] = 'RS256'

def create_token(user):
    """Create JWT for user"""
    now = datetime.utcnow()
    payload = {
        "sub": str(user.id),
        "email": user.email,
        "role": user.role,
        "iat": int(now.timestamp()),
        "exp": int((now + timedelta(hours=1)).timestamp())
    }
    return jwt.encode(payload, private_key, algorithm="RS256")

def token_required(f):
    """Decorator to require valid JWT"""
    @wraps(f)
    def decorated(*args, **kwargs):
        token = None
        
        # Get token from header
        if 'Authorization' in request.headers:
            auth_header = request.headers['Authorization']
            if auth_header.startswith('Bearer '):
                token = auth_header.split(' ')[1]
        
        if not token:
            return jsonify({'error': 'Token missing'}), 401
        
        try:
            payload = jwt.decode(
                token,
                public_key,
                algorithms=["RS256"],
                audience="my-api",
                issuer="my-app"
            )
            request.user = payload
            
        except jwt.ExpiredSignatureError:
            return jsonify({'error': 'Token expired'}), 401
        except jwt.InvalidTokenError:
            return jsonify({'error': 'Invalid token'}), 401
        
        return f(*args, **kwargs)
    
    return decorated

@app.route('/protected')
@token_required
def protected():
    return jsonify({
        'user': request.user['sub'],
        'email': request.user['email']
    })

FastAPI JWT Implementation

from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
import jwt

app = FastAPI()
security = HTTPBearer()

async def get_current_user(
    credentials: HTTPAuthorizationCredentials = Depends(security)
) -> dict:
    """Validate JWT and return user"""
    
    token = credentials.credentials
    
    try:
        payload = jwt.decode(
            token,
            public_key,
            algorithms=["RS256"],
            audience="my-api",
            issuer="my-app"
        )
        return payload
        
    except jwt.ExpiredSignatureError:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Token expired"
        )
    except jwt.InvalidTokenError:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid token"
        )

@app.get("/protected")
async def protected_route(user: dict = Depends(get_current_user)):
    return {"user_id": user["sub"], "email": user["email"]}

Common Vulnerabilities

Attacks and Mitigations

vulnerabilities = {
    "algorithm_none": {
        "attack": "Set alg: none in header",
        "mitigation": "Always specify allowed algorithms"
    },
    
    "key_confusion": {
        "attack": "Use RS token with HS and weak secret",
        "mitigation": "Use different keys for different algos"
    },
    
    "jwt_brute_force": {
        "attack": "Brute force weak HMAC secret",
        "mitigation": "Use strong secret (256+ bits)"
    },
    
    "jku_header": {
        "attack": "Use external key URL",
        "mitigation": "Disable header injection"
    },
    
    "kid_header": {
        "attack": "Path traversal in kid header",
        "mitigation": "Validate and sanitize kid"
    }
}

# Safe configuration
safe_options = {
    "verify_signature": True,
    "verify_exp": True,
    "verify_nbf": True,
    "verify_iat": True,
    "verify_aud": True,
    "verify_iss": True,
    "algorithms": ["RS256", "ES256"],  # Explicit!
    "require": ["exp", "iss", "sub"]    # Required claims
}

Conclusion

JWT security essentials:

  • Algorithm: Use RS256 or ES256, never “none”
  • Expiration: Short-lived access tokens (1 hour max)
  • Validation: Verify signature, expiration, issuer, audience
  • Storage: Access in memory, refresh in HTTP-only cookies
  • Key rotation: Regular key rotation with kid header
  • Revocation: Track revoked tokens for refresh tokens

Always use established libraries and keep dependencies updated.


Comments