Skip to content

[SECURITY FEATURE]: Make JWT Token Expiration Mandatory when REQUIRE_TOKEN_EXPIRATION=true (depends on #87) #425

@crivetimihai

Description

@crivetimihai

Feature: Configurable JWT Token Expiration Requirement

🎯 Overview

Summary

Add configurable enforcement of JWT token expiration claims, allowing administrators to choose between strict security (mandatory expiration) or flexibility (optional expiration with warnings).

Problem Statement

Currently, tokens created with --exp 0 have no expiration and remain valid forever. While this is sometimes necessary for long-running automations, it poses a security risk. There's no way to enforce expiration requirements or get warnings about non-expiring tokens.

Solution

Introduce a REQUIRE_TOKEN_EXPIRATION configuration setting that:

  • When true: Rejects tokens without expiration (strict mode)
  • When false: Accepts tokens without expiration but logs warnings (flexible mode)
  • Provides clear audit trail for security compliance

Dependencies

📊 Architecture

flowchart TD
    subgraph "Token Validation Flow"
        T[Token] --> CHECK{Has exp claim?}
        CHECK -->|Yes| VALIDATE[Validate expiration]
        CHECK -->|No| CONFIG{REQUIRE_TOKEN_EXPIRATION?}
        
        CONFIG -->|true| REJECT[❌ HTTP 401]
        CONFIG -->|false| WARN[⚠️ Log warning]
        
        VALIDATE -->|Valid| OK[✅ Authorized]
        VALIDATE -->|Expired| EXPIRED[❌ Token expired]
        WARN --> OK
    end
    
    subgraph "Configuration"
        ENV[.env] --> SETTING[REQUIRE_TOKEN_EXPIRATION=true/false]
    end
    
    style REJECT fill:#FFB6C1
    style WARN fill:#FFA500
    style OK fill:#90EE90
    style EXPIRED fill:#FFB6C1
Loading

🏗️ Technical Design

Configuration

# In settings.py
require_token_expiration: bool = Field(
    default=False,  # Default to flexible mode for backward compatibility
    description="Require all JWT tokens to have expiration claims"
)

Environment Variable

# .env
REQUIRE_TOKEN_EXPIRATION=false  # Allow non-expiring tokens with warnings
# or
REQUIRE_TOKEN_EXPIRATION=true   # Strict mode - reject non-expiring tokens

Implementation

async def verify_jwt_token(token: str) -> dict:
    """Verify and decode a JWT token with configurable expiration requirement."""
    try:
        # First decode to check claims
        unverified = jwt.decode(token, options={"verify_signature": False})
        
        # Check for expiration claim
        if "exp" not in unverified and settings.require_token_expiration:
            raise jwt.MissingRequiredClaimError("exp")
        
        # Log warning for non-expiring tokens
        if "exp" not in unverified:
            logger.warning(
                "JWT token without expiration accepted. "
                "Consider enabling REQUIRE_TOKEN_EXPIRATION for better security. "
                f"Token sub: {unverified.get('sub', 'unknown')}"
            )
        
        # Full validation
        options = {}
        if settings.require_token_expiration:
            options["require"] = ["exp"]
            
        payload = jwt.decode(
            token,
            settings.jwt_secret_key,
            algorithms=[settings.jwt_algorithm],
            options=options
        )
        return payload
        
    except jwt.MissingRequiredClaimError:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Token is missing required expiration claim. Set REQUIRE_TOKEN_EXPIRATION=false to allow.",
            headers={"WWW-Authenticate": "Bearer"},
        )
    except jwt.ExpiredSignatureError:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Token has expired",
            headers={"WWW-Authenticate": "Bearer"},
        )

CLI Warning Enhancement

# In create_jwt_token.py
def _create_jwt_token(...):
    """Create JWT with optional expiration warning."""
    payload = data.copy()
    
    if expires_in_minutes > 0:
        expire = _dt.datetime.now(_dt.timezone.utc) + _dt.timedelta(minutes=expires_in_minutes)
        payload["exp"] = int(expire.timestamp())
    else:
        # Warn about non-expiring token
        print(
            "⚠️  WARNING: Creating token without expiration. This is a security risk!\n"
            "   Consider using --exp with a value > 0 for production use.\n"
            "   Once JWT API (#425) is available, use it for automatic token renewal.",
            file=sys.stderr
        )
    
    return jwt.encode(payload, secret, algorithm=algorithm)

👥 User Story

As a security-conscious API administrator
I want to configure whether tokens must have expiration
So that I can balance security requirements with operational needs

Acceptance Criteria:

  • Configuration via REQUIRE_TOKEN_EXPIRATION environment variable
  • When enabled: tokens without exp return HTTP 401
  • When disabled: tokens without exp work but log warnings
  • CLI warns when creating non-expiring tokens
  • Clear documentation on security implications

🛠️ Implementation Plan

Phase 1: Core Configuration

  • Add require_token_expiration to settings with default false
  • Update verify_jwt_token() to check configuration
  • Implement conditional expiration requirement
  • Add warning logs for non-expiring tokens

Phase 2: Enhanced Logging

  • Log warnings with token metadata (sub, iat)
  • Add metrics/monitoring for non-expiring tokens
  • Create audit trail for compliance reporting

Phase 3: CLI Enhancement

Phase 4: Documentation

📋 Test Scenarios

Configuration Testing

# Test 1: Flexible mode (default)
export REQUIRE_TOKEN_EXPIRATION=false
export NO_EXP_TOKEN=$(python3 -m mcpgateway.utils.create_jwt_token --username test --exp 0)
curl -H "Authorization: Bearer $NO_EXP_TOKEN" http://localhost:4444/version
# Returns: 200 OK (with warning in logs)

# Test 2: Strict mode
export REQUIRE_TOKEN_EXPIRATION=true
curl -H "Authorization: Bearer $NO_EXP_TOKEN" http://localhost:4444/version
# Returns: 401 Unauthorized
# {"detail": "Token is missing required expiration claim. Set REQUIRE_TOKEN_EXPIRATION=false to allow."}

# Test 3: Token with expiration works in both modes
export EXP_TOKEN=$(python3 -m mcpgateway.utils.create_jwt_token --username test --exp 60)
curl -H "Authorization: Bearer $EXP_TOKEN" http://localhost:4444/version
# Returns: 200 OK (regardless of REQUIRE_TOKEN_EXPIRATION)

CLI Warning Test

# Creating non-expiring token shows warning
$ python3 -m mcpgateway.utils.create_jwt_token --username admin --exp 0
⚠️  WARNING: Creating token without expiration. This is a security risk!
   Consider using --exp with a value > 0 for production use.
   Once JWT API (#425) is available, use it for automatic token renewal.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

📚 Documentation Template

## JWT Token Expiration Configuration

### Setting: REQUIRE_TOKEN_EXPIRATION
Controls whether JWT tokens must include an expiration claim.

- `false` (default): Accepts tokens without expiration, logs warnings
- `true`: Rejects tokens without expiration (recommended for production)

### Security Considerations
Non-expiring tokens pose security risks:
- Cannot be invalidated without rotating secrets
- Remain valid if compromised
- Fail security audits

### Migration Path
1. **Current State**: Use non-expiring tokens with `REQUIRE_TOKEN_EXPIRATION=false`
2. **Transition**: Monitor warnings, identify non-expiring token usage
3. **Future State**: Once JWT API (#425) is available:
   - Enable `REQUIRE_TOKEN_EXPIRATION=true`
   - Use API for automatic token renewal
   - Implement token rotation policies

### Examples
# Development - Allow flexibility
REQUIRE_TOKEN_EXPIRATION=false

# Production - Enforce security
REQUIRE_TOKEN_EXPIRATION=true

# Until #425 is implemented, long-lived tokens may be necessary:
python3 -m mcpgateway.utils.create_jwt_token --username automation --exp 43200  # 30 days

🔄 Migration Strategy

Before JWT API (#425)

  • Allow non-expiring tokens for critical automations
  • Use long expiration (30-90 days) where possible
  • Monitor and log all non-expiring token usage

After JWT API (#425)

  • Enable REQUIRE_TOKEN_EXPIRATION=true
  • Migrate to shorter expiration (1-7 days)
  • Implement automatic token renewal via API
  • Phase out all non-expiring tokens

📊 Success Metrics

  • % of tokens with expiration (target: increase over time)
  • Warning log frequency (target: decrease to zero)
  • Security audit compliance score
  • Zero service disruptions during migration

🔗 Standards Compliance

  • ✅ Configurable to meet various security policies
  • ✅ Provides path to JWT best practices
  • ✅ Backward compatible by default
  • ✅ Clear audit trail for compliance

📝 Notes

Metadata

Metadata

Labels

enhancementNew feature or requestsecurityImproves securitytriageIssues / Features awaiting triage

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions