jwtblacklist

package module
v1.0.7 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 6, 2025 License: Apache-2.0 Imports: 25 Imported by: 0

README

Caddy JWT Blacklist Plugin

codecov Go Report Card Go Reference

A comprehensive unified JWT authentication and blacklist middleware for Caddy that provides immediate token revocation capabilities using Redis. This plugin integrates full JWT authentication with Redis-based blacklist checking in a single, high-performance middleware.

[!NOTE] This plugin integrates JWT authentication functionality from ggicci/caddy-jwt with our Redis-based blacklist system, providing a unified solution that eliminates the need for separate JWT auth plugins.

[!NOTE]
This is not an official repository of the Caddy Web Server organization.

Features

🔐 Integrated JWT Authentication
  • Full JWT validation - Signature verification, expiration, issuer/audience validation
  • Multiple signing algorithms - HS256, HS384, HS512, RS256, RS384, RS512, ES256, ES384, ES512, EdDSA
  • JWK support - Fetch public keys from JWK URLs with caching and refresh
  • Flexible token extraction - Authorization header, custom headers, query parameters, cookies
  • Custom claims mapping - Extract user metadata from JWT claims
  • Skip verification mode - For development and testing
🚫 Redis-Based Blacklist
  • Immediate token revocation - O(1) Redis lookups for blacklisted tokens
  • Blacklist-first architecture - Check blacklist before expensive JWT validation
  • TTL support - Automatic expiration of blacklist entries
  • Detailed blacklist metadata - Store revocation reason and context
🛡️ Production-Ready Features
  • Fail-open/fail-closed - Configurable behavior when Redis is unavailable
  • Low latency - Optimized request processing (~0.1-0.5ms overhead)
  • Comprehensive logging - Detailed request and error logging
  • Graceful error handling - Specific error responses for different failure modes
  • User context population - Set Caddy placeholders for downstream handlers

Installation

Build Caddy with this plugin using xcaddy:

xcaddy build --with github.com/chalabi2/caddy-jwt-blacklist

Or add to your xcaddy.json:

{
  "dependencies": [
    {
      "module": "github.com/chalabi2/caddy-jwt-blacklist",
      "version": "latest"
    }
  ]
}

Quick Start

Basic Caddyfile configuration:

{
    admin localhost:2019
}

localhost:8080 {
    jwt_blacklist {
        # Redis configuration
        redis_addr {env.REDIS_URL}
        redis_password {env.REDIS_PASSWORD}
        redis_db 0
        blacklist_prefix "BLACKLIST:key:"

        # JWT authentication
        sign_key {env.JWT_SECRET}
        sign_alg HS256
        from_header Authorization X-API-Key
        from_query api_key access_token
        user_claims sub
        meta_claims "tier" "scope"

        # Optional settings
        timeout 50ms
        fail_open true
        log_blocked true
    }

    respond "Hello {http.auth.user.id}! Your tier: {http.auth.user.tier}"
}

Configuration

Note: Complete example configurations are available in the example-configs/ directory.

Basic Configuration
jwt_blacklist {
    # Redis settings (required)
    redis_addr {env.REDIS_URL}
    redis_password {env.REDIS_PASSWORD}
    redis_db 0
    blacklist_prefix "BLACKLIST:key:"

    # JWT authentication (required)
    sign_key {env.JWT_SECRET}
    sign_alg HS256

    # Token extraction
    from_header Authorization X-API-Key
    from_query api_key access_token token
    from_cookies session_token

    # Claims mapping
    user_claims sub uid user_id
    meta_claims "tier" "scope" "org_id->organization"

    # Operational settings
    timeout 100ms
    fail_open true
    log_blocked true
}
Advanced Configuration
jwt_blacklist {
    # Redis with TLS
    redis_addr {env.REDIS_URL}
    redis_password {env.REDIS_PASSWORD}
    redis_db 0
    tls {
        enabled true
        min_version "1.2"
    }

    # JWK for asymmetric keys
    jwk_url https://auth.example.com/.well-known/jwks.json
    sign_alg RS256

    # Validation rules
    issuer_whitelist https://auth.example.com
    audience_whitelist https://api.example.com

    # Custom token sources
    from_header Authorization X-Custom-Token
    from_query access_token

    # Advanced claims mapping
    user_claims sub email username
    meta_claims "role->user_role" "permissions->access_permissions"
}

Configuration Options

Redis Settings
Option Description Default Required
redis_addr Redis server address -
redis_password Redis password (empty)
redis_db Redis database number 0
blacklist_prefix Redis key prefix for blacklisted keys BLACKLIST:key:
timeout Redis operation timeout 50ms
fail_open Continue processing if Redis fails false
log_blocked Log blocked requests false
TLS Settings (for Redis)
Option Description Default Required
enabled Enable TLS false
server_name TLS server name -
cert_file Client certificate -
key_file Client private key -
ca_file CA certificate -
min_version Minimum TLS version 1.2
JWT Authentication Settings
Option Description Default Required
sign_key JWT signing key (base64 for HMAC) - ✅*
jwk_url JWK endpoint URL - ✅*
sign_alg Signing algorithm HS256
skip_verification Skip signature verification false
from_query Query parameter names ["api_key", "access_token", "token"]
from_header Header names ["Authorization", "X-API-Key", "X-Api-Token"]
from_cookies Cookie names ["session_token"]
user_claims JWT claims for user ID ["sub"]
meta_claims Additional claims mapping {}
issuer_whitelist Allowed issuers []
audience_whitelist Allowed audiences []

* Either sign_key or jwk_url is required

JWT Claims

The plugin expects JWT tokens with standard claims:

{
  "sub": "user_123", // Subject (user ID)
  "jti": "api_key_abc123", // JWT ID (used for blacklist lookup)
  "iss": "https://auth.example.com", // Issuer
  "aud": ["https://api.example.com"], // Audience
  "exp": 1640995200, // Expiration timestamp
  "iat": 1640991600, // Issued at timestamp
  "tier": "PREMIUM", // Custom: user tier
  "scope": "api_access", // Custom: access scope
  "org_id": "org_456" // Custom: organization ID
}

Critical: The jti (JWT ID) claim is used as the API key identifier for blacklist checks.

Redis Blacklist Format

Blacklisted tokens are stored in Redis with this key pattern:

{blacklist_prefix}{jti}

Example:

BLACKLIST:key:api_key_abc123

The value stores the revocation reason:

  • cancelled - Subscription cancelled
  • expired - Payment/subscription expired
  • downgraded - Subscription downgraded
  • security - Security incident
  • abuse - Terms of service violation
TTL Examples
# Temporary blacklist for downgrade (24 hours)
SETEX BLACKLIST:key:api_key_123 86400 "downgraded"

# Subscription cancelled (7 days)
SETEX BLACKLIST:key:api_key_456 604800 "cancelled"

# Permanent blacklist (security incident)
SET BLACKLIST:key:api_key_789 "security"

User Context & Placeholders

After successful authentication, the plugin populates Caddy placeholders:

# Basic user information
{http.auth.user.id}              # User ID from JWT
{http.auth.user.jti}             # JWT ID (API key ID)
{http.auth.user.authenticated}   # "true"

# Custom metadata (from meta_claims)
{http.auth.user.tier}            # User tier
{http.auth.user.scope}           # Access scope
{http.auth.user.organization}    # Organization ID

Example usage:

jwt_blacklist {
    user_claims sub username
    meta_claims "tier" "role->user_role" "org->organization"
}

# Use in responses
respond "Welcome {http.auth.user.username} (Role: {http.auth.user.user_role})"

# Use in logging
log {
    output file /var/log/api.log
    format single_field common_log
    level INFO
}

Error Responses

Blacklisted Token
{
  "error": "api_key_blacklisted",
  "message": "API key has been disabled due to subscription changes",
  "code": 401,
  "details": "Please check your subscription status or generate a new API key"
}
Invalid/Missing Token
{
  "error": "invalid_token",
  "message": "Invalid authentication token",
  "code": 401
}
Redis Unavailable (Fail Closed)
{
  "error": "internal_error",
  "message": "Authentication service unavailable",
  "code": 500
}

Integration Examples

Backend Integration (TypeScript/Node.js)
import Redis from "ioredis";

const redis = new Redis(process.env.REDIS_URL);

// Blacklist API key immediately on subscription cancellation
async function blacklistApiKey(
  apiKeyId: string,
  reason: string,
  ttlDays: number = 7
) {
  const ttlSeconds = ttlDays * 24 * 60 * 60;
  await redis.setex(`BLACKLIST:key:${apiKeyId}`, ttlSeconds, reason);
  console.log(`Blacklisted API key ${apiKeyId} for ${reason}`);
}

// Usage examples
await blacklistApiKey("api_key_123", "cancelled", 7); // 7 days
await blacklistApiKey("api_key_456", "expired", 30); // 30 days
await blacklistApiKey("api_key_789", "downgraded", 1); // 1 day

// Remove from blacklist (e.g., subscription reactivated)
async function unblacklistApiKey(apiKeyId: string) {
  await redis.del(`BLACKLIST:key:${apiKeyId}`);
}
Webhook Integration
// Subscription cancelled webhook
app.post("/webhooks/subscription-cancelled", async (req, res) => {
  const { userId, subscriptionId } = req.body;

  // Get all API keys for user
  const apiKeys = await db.apiKeys.findMany({ where: { userId } });

  // Blacklist all API keys
  const blacklistPromises = apiKeys.map((key) =>
    redis.setex(`BLACKLIST:key:${key.jti}`, 86400 * 7, "cancelled")
  );

  await Promise.all(blacklistPromises);
  res.json({ success: true, blacklisted: apiKeys.length });
});

Architecture

This plugin implements a blacklist-first architecture for optimal performance:

1. Extract JWT token from request
2. Parse JWT (without verification) to get `jti`
3. Check Redis blacklist (O(1) lookup)
4. If blacklisted → return 401 immediately
5. If not blacklisted → perform full JWT validation
6. If valid → populate user context and continue

This design ensures:

  • Fast rejection of blacklisted tokens (~0.1ms)
  • Expensive validation only for non-blacklisted tokens
  • Security - no way to bypass blacklist with valid signatures

Performance

  • Latency: ~0.1-0.5ms per request
  • Memory: Minimal overhead with connection pooling
  • Redis operations: Single EXISTS check per request
  • Throughput: Tested at >10,000 RPS with negligible impact

Development & Testing

Setup Development Environment
git clone https://github.com/chalabi2/caddy-jwt-blacklist
cd caddy-jwt-blacklist
make deps
Run Tests
# Start Redis for testing
make redis-start

# Run all tests
make test-all

# Run with coverage
make test-coverage

# Run benchmarks
make benchmark

# Stop Redis
make redis-stop
Integration Testing
# Build custom Caddy binary
make xcaddy-build

# Run integration test script
./test.sh

# Test with example configs
./caddy run --config example-configs/Caddyfile

Migration from Separate Modules

If you're currently using ggicci/caddy-jwt + this blacklist plugin separately:

Before (Two Modules)
{
    order jwt_blacklist before jwtauth
}

api.example.com {
    jwt_blacklist {
        redis_addr {env.REDIS_URL}
        # ... blacklist config
    }

    jwtauth {
        sign_key {env.JWT_SECRET}
        # ... jwt config
    }
}
After (Unified Module)
api.example.com {
    jwt_blacklist {
        # Redis settings
        redis_addr {env.REDIS_URL}

        # JWT settings (integrated)
        sign_key {env.JWT_SECRET}
        sign_alg HS256
        from_header Authorization
        user_claims sub
    }
}

Benefits:

  • ✅ Single module to manage
  • ✅ Better performance (blacklist-first)
  • ✅ No middleware ordering issues
  • ✅ Simplified configuration
  • ✅ Reduced build dependencies

Requirements

  • Caddy: v2.8.0 or higher
  • Go: 1.22 or higher
  • Redis: 6.0 or higher

License

MIT License - see LICENSE file.

Acknowledgments

This plugin integrates JWT authentication functionality from ggicci/caddy-jwt by @ggicci with our Redis-based blacklist system. We extend our gratitude to the original authors for their excellent JWT implementation.

Contributing

  1. Fork the repository
  2. Create a feature branch
  3. Add tests for new functionality
  4. Ensure all tests pass
  5. Submit a pull request

Bug Reports

When reporting bugs, please include:

  • Caddy version (./caddy version)
  • Plugin version
  • Configuration (Caddyfile or JSON)
  • Redis version and setup
  • JWT token format and claims
  • Steps to reproduce
  • Expected vs actual behavior
  • Relevant logs with debug level enabled

Documentation

Overview

Package jwtblacklist provides a Caddy middleware for integrated JWT authentication and blacklist validation using Redis. This module combines JWT token validation with Redis-based blacklist checking in a single middleware.

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrMissingKeys          = errors.New("missing sign_key and jwk_url")
	ErrInvalidPublicKey     = errors.New("invalid PEM-formatted public key")
	ErrInvalidSignAlgorithm = errors.New("invalid sign_alg")
	ErrInvalidIssuer        = errors.New("invalid issuer")
	ErrInvalidAudience      = errors.New("invalid audience")
	ErrEmptyUserClaim       = errors.New("user claim is empty")
)

JWT error constants

Functions

This section is empty.

Types

type Claims

type Claims struct {
	UserID   string `json:"sub"`
	APIKeyID string `json:"jti"` // This is the API key ID we check against blacklist
	Tier     string `json:"tier"`
	Scope    string `json:"scope"`
}

Claims represents the JWT claims we're interested in

type Config

type Config struct {
	// Redis connection settings
	RedisAddr     string     `json:"redis_addr,omitempty"`
	RedisPassword string     `json:"redis_password,omitempty"`
	RedisDB       int        `json:"redis_db,omitempty"`
	RedisTLS      *TLSConfig `json:"redis_tls,omitempty"`

	// JWT settings (for backward compatibility)
	JWTSecret string `json:"jwt_secret,omitempty"`

	// Advanced JWT configuration
	JWT *JWTConfig `json:"jwt,omitempty"`

	// Blacklist settings
	BlacklistPrefix string `json:"blacklist_prefix,omitempty"`

	// Behavior settings
	FailOpen   bool           `json:"fail_open,omitempty"`
	Timeout    caddy.Duration `json:"timeout,omitempty"`
	LogBlocked bool           `json:"log_blocked,omitempty"`
}

Config holds the configuration for the JWT blacklist plugin

type JWTBlacklist

type JWTBlacklist struct {
	Config *Config `json:"config,omitempty"`
	// contains filtered or unexported fields
}

JWTBlacklist is the main middleware struct

func (JWTBlacklist) CaddyModule

func (JWTBlacklist) CaddyModule() caddy.ModuleInfo

CaddyModule returns the Caddy module information

func (*JWTBlacklist) Cleanup

func (jb *JWTBlacklist) Cleanup() error

Cleanup closes the Redis connection

func (*JWTBlacklist) Provision

func (jb *JWTBlacklist) Provision(ctx caddy.Context) error

Provision sets up the module

func (*JWTBlacklist) ServeHTTP

func (jb *JWTBlacklist) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error

ServeHTTP implements the integrated JWT authentication and blacklist validation CRITICAL: Blacklist check happens BEFORE full JWT authentication for performance and security

func (*JWTBlacklist) UnmarshalCaddyfile

func (jb *JWTBlacklist) UnmarshalCaddyfile(d *caddyfile.Dispenser) error

UnmarshalCaddyfile implements caddyfile.Unmarshaler

func (*JWTBlacklist) Validate

func (jb *JWTBlacklist) Validate() error

Validate ensures the configuration is valid

type JWTConfig added in v1.0.6

type JWTConfig struct {
	// SignKey is the key used by the signing algorithm to verify the signature
	SignKey string `json:"sign_key"`

	// JWKURL is the URL where a provider publishes their JWKs
	JWKURL string `json:"jwk_url"`

	// SignAlgorithm is the signing algorithm used
	SignAlgorithm string `json:"sign_alg"`

	// SkipVerification disables the verification of the JWT token signature
	SkipVerification bool `json:"skip_verification"`

	// FromQuery defines a list of names to get tokens from query parameters
	FromQuery []string `json:"from_query"`

	// FromHeader defines a list of names to get tokens from HTTP headers
	FromHeader []string `json:"from_header"`

	// FromCookies defines a list of names to get tokens from HTTP cookies
	FromCookies []string `json:"from_cookies"`

	// IssuerWhitelist defines a list of allowed issuers
	IssuerWhitelist []string `json:"issuer_whitelist"`

	// AudienceWhitelist defines a list of allowed audiences
	AudienceWhitelist []string `json:"audience_whitelist"`

	// UserClaims defines a list of names to find the ID of the authenticated user
	UserClaims []string `json:"user_claims"`

	// MetaClaims defines a map to populate user metadata placeholders
	MetaClaims map[string]string `json:"meta_claims"`
	// contains filtered or unexported fields
}

JWTConfig holds the JWT authentication configuration

type RedisClient

type RedisClient struct {
	// contains filtered or unexported fields
}

RedisClient wraps the Redis client with blacklist-specific functionality

func NewRedisClient

func NewRedisClient(addr, password string, db int, tlsConfig *TLSConfig, logger *zap.Logger) (*RedisClient, error)

NewRedisClient creates a new Redis client with optional TLS support

func (*RedisClient) Close

func (rc *RedisClient) Close() error

Close closes the Redis connection

func (*RedisClient) GetBlacklistInfo

func (rc *RedisClient) GetBlacklistInfo(ctx context.Context, apiKeyID string, prefix string) (string, time.Duration, error)

GetBlacklistInfo retrieves additional information about a blacklisted key

func (*RedisClient) IsBlacklisted

func (rc *RedisClient) IsBlacklisted(ctx context.Context, apiKeyID string, prefix string) (bool, error)

IsBlacklisted checks if an API key is blacklisted

type TLSConfig added in v1.0.1

type TLSConfig struct {
	Enabled            bool   `json:"enabled,omitempty"`
	InsecureSkipVerify bool   `json:"insecure_skip_verify,omitempty"`
	ServerName         string `json:"server_name,omitempty"`
	MinVersion         string `json:"min_version,omitempty"`
	CertFile           string `json:"cert_file,omitempty"`
	KeyFile            string `json:"key_file,omitempty"`
	CAFile             string `json:"ca_file,omitempty"`
}

TLSConfig holds TLS configuration options

type Token added in v1.0.6

type Token = jwt.Token

Token represents a JWT token

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL