1
0
Fork 0
docker-easy-haproxy/docs/plugins/jwt-validator.md
Joao Gilberto Magalhaes 56fc86d77d Add allow_anonymous option to JwtValidatorPlugin
- Introduced `allow_anonymous` configuration option to permit requests without an Authorization header.
- Updated HAProxy configuration to handle optional JWT validation for anonymous access.
- Enhanced documentation with use cases, examples, and configuration details for `allow_anonymous`.
- Adjusted tests and plugin logic to support anonymous access scenarios.
2025-12-01 17:43:05 -05:00

12 KiB

JWT Validator Plugin

Type: Domain Plugin Runs: Once for each discovered domain/host

Overview

The JWT Validator plugin validates JWT (JSON Web Token) authentication tokens using HAProxy's built-in JWT functionality.

Why Use It

Protect APIs and services with JWT authentication without needing application-level code.

Configuration Options

Option Description Default
enabled Enable/disable plugin true
algorithm JWT signing algorithm RS256
issuer Expected JWT issuer (optional, set to none/null to skip validation) (optional)
audience Expected JWT audience (optional, set to none/null to skip validation) (optional)
pubkey_path Path to public key file (required if pubkey not provided) (required)
pubkey Public key content as base64-encoded string (required if pubkey_path not provided) (optional)
paths List of paths that require JWT validation (optional) (all paths)
only_paths If true, only specified paths are accessible; if false, only specified paths require JWT false
allow_anonymous If true, allows requests without Authorization header (validates JWT if present) false

Path Validation Logic

  • No paths configured: ALL requests to the domain require JWT validation (default behavior)
  • Paths configured + only_paths=false: Only specified paths require JWT validation, other paths pass through without validation
  • Paths configured + only_paths=true: Only specified paths are accessible (with JWT validation), all other paths are denied

Anonymous Access Logic

  • allow_anonymous=false (default): Requests without Authorization header are denied with "Missing Authorization HTTP header"
  • allow_anonymous=true: Requests without Authorization header are allowed to pass through, but JWTs are validated if the header is present

Use Cases for allow_anonymous=true:

  • Optional authentication (show different content for authenticated vs anonymous users)
  • Mixed public/private content where some users have enhanced access with JWT
  • Gradual JWT authentication rollout
  • Public APIs that provide additional features to authenticated users

Configuration Examples

Docker/Docker Compose (Protect All Paths)

services:
  api:
    labels:
      easyhaproxy.http.host: api.example.com
      easyhaproxy.http.plugins: jwt_validator
      easyhaproxy.http.plugin.jwt_validator.algorithm: RS256
      easyhaproxy.http.plugin.jwt_validator.issuer: https://auth.example.com/
      easyhaproxy.http.plugin.jwt_validator.audience: https://api.example.com
      easyhaproxy.http.plugin.jwt_validator.pubkey_path: /etc/haproxy/jwt_keys/api_pubkey.pem
    volumes:
      - ./pubkey.pem:/etc/haproxy/jwt_keys/api_pubkey.pem:ro

Protect Specific Paths Only

labels:
  easyhaproxy.http.plugins: jwt_validator
  easyhaproxy.http.plugin.jwt_validator.pubkey_path: /etc/haproxy/jwt_keys/api_pubkey.pem
  easyhaproxy.http.plugin.jwt_validator.paths: /api/admin,/api/sensitive
  easyhaproxy.http.plugin.jwt_validator.only_paths: false
# /api/health, /api/docs, etc. remain publicly accessible

Only Allow Specific Paths

labels:
  easyhaproxy.http.plugins: jwt_validator
  easyhaproxy.http.plugin.jwt_validator.pubkey_path: /etc/haproxy/jwt_keys/api_pubkey.pem
  easyhaproxy.http.plugin.jwt_validator.paths: /api/public,/api/v1
  easyhaproxy.http.plugin.jwt_validator.only_paths: true
# All paths except /api/public and /api/v1 are denied

Skip Issuer/Audience Validation

labels:
  easyhaproxy.http.plugin.jwt_validator.issuer: none
  easyhaproxy.http.plugin.jwt_validator.audience: none
  easyhaproxy.http.plugin.jwt_validator.pubkey_path: /etc/haproxy/jwt_keys/api_pubkey.pem

Allow Anonymous Access (Optional JWT)

services:
  api:
    labels:
      easyhaproxy.http.host: api.example.com
      easyhaproxy.http.plugins: jwt_validator
      easyhaproxy.http.plugin.jwt_validator.pubkey_path: /etc/haproxy/jwt_keys/api_pubkey.pem
      easyhaproxy.http.plugin.jwt_validator.allow_anonymous: true
    volumes:
      - ./pubkey.pem:/etc/haproxy/jwt_keys/api_pubkey.pem:ro
# Requests without Authorization header are allowed
# Requests with Authorization header are validated
# Invalid JWTs are rejected

Kubernetes Annotations

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    easyhaproxy.plugins: "jwt_validator"
    easyhaproxy.plugin.jwt_validator.algorithm: "RS256"
    easyhaproxy.plugin.jwt_validator.issuer: "https://auth.example.com/"
    easyhaproxy.plugin.jwt_validator.audience: "https://api.example.com"
    easyhaproxy.plugin.jwt_validator.pubkey_path: "/etc/haproxy/jwt_keys/api_pubkey.pem"
    easyhaproxy.plugin.jwt_validator.paths: "/api/admin,/api/users"
    easyhaproxy.plugin.jwt_validator.only_paths: "false"
spec:
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /
            backend:
              service:
                name: api-service
                port:
                  number: 8080

Static YAML Configuration

# /etc/haproxy/static/config.yaml
easymapping:
  - host: api.example.com
    port: 443
    container: api-service:8080
    plugins:
      - jwt_validator
    plugin_config:
      jwt_validator:
        algorithm: RS256
        issuer: https://auth.example.com/
        audience: https://api.example.com
        pubkey_path: /etc/haproxy/jwt_keys/api_pubkey.pem

Generated HAProxy Configuration

All Paths Protected

# JWT Validator - Validate JWT tokens
http-request deny content-type 'text/html' string 'Missing Authorization HTTP header' unless { req.hdr(authorization) -m found }

# Extract JWT header and payload
http-request set-var(txn.alg) http_auth_bearer,jwt_header_query('$.alg')
http-request set-var(txn.iss) http_auth_bearer,jwt_payload_query('$.iss')
http-request set-var(txn.aud) http_auth_bearer,jwt_payload_query('$.aud')
http-request set-var(txn.exp) http_auth_bearer,jwt_payload_query('$.exp','int')

# Validate JWT
http-request deny content-type 'text/html' string 'Unsupported JWT signing algorithm' unless { var(txn.alg) -m str RS256 }
http-request deny content-type 'text/html' string 'Invalid JWT issuer' unless { var(txn.iss) -m str https://auth.example.com/ }
http-request deny content-type 'text/html' string 'Invalid JWT audience' unless { var(txn.aud) -m str https://api.example.com }
http-request deny content-type 'text/html' string 'Invalid JWT signature' unless { http_auth_bearer,jwt_verify(txn.alg,"/etc/haproxy/jwt_keys/api_pubkey.pem") -m int 1 }

# Validate expiration
http-request set-var(txn.now) date()
http-request deny content-type 'text/html' string 'JWT has expired' if { var(txn.exp),sub(txn.now) -m int lt 0 }

Specific Paths Only (only_paths=false)

# JWT Validator - Validate JWT tokens

# Define paths that require JWT validation
acl jwt_protected_path path_beg /api/admin
acl jwt_protected_path path_beg /api/sensitive

http-request deny content-type 'text/html' string 'Missing Authorization HTTP header' unless { req.hdr(authorization) -m found } if jwt_protected_path

# Extract JWT header and payload
http-request set-var(txn.alg) http_auth_bearer,jwt_header_query('$.alg') if jwt_protected_path
http-request set-var(txn.iss) http_auth_bearer,jwt_payload_query('$.iss') if jwt_protected_path
http-request set-var(txn.aud) http_auth_bearer,jwt_payload_query('$.aud') if jwt_protected_path
http-request set-var(txn.exp) http_auth_bearer,jwt_payload_query('$.exp','int') if jwt_protected_path

# Validate JWT (only on protected paths)
http-request deny content-type 'text/html' string 'Unsupported JWT signing algorithm' unless { var(txn.alg) -m str RS256 } if jwt_protected_path
http-request deny content-type 'text/html' string 'Invalid JWT signature' unless { http_auth_bearer,jwt_verify(txn.alg,"/etc/haproxy/jwt_keys/api_pubkey.pem") -m int 1 } if jwt_protected_path

# Validate expiration
http-request set-var(txn.now) date() if jwt_protected_path
http-request deny content-type 'text/html' string 'JWT has expired' if { var(txn.exp),sub(txn.now) -m int lt 0 } if jwt_protected_path

Specific Paths Only (only_paths=true)

# JWT Validator - Validate JWT tokens

# Define paths that require JWT validation
acl jwt_protected_path path_beg /api/public
acl jwt_protected_path path_beg /api/v1

# Deny access to paths not in the protected list
http-request deny content-type 'text/html' string 'Access denied' unless jwt_protected_path

http-request deny content-type 'text/html' string 'Missing Authorization HTTP header' unless { req.hdr(authorization) -m found }

# Extract JWT header and payload
http-request set-var(txn.alg) http_auth_bearer,jwt_header_query('$.alg')
http-request set-var(txn.iss) http_auth_bearer,jwt_payload_query('$.iss')
http-request set-var(txn.aud) http_auth_bearer,jwt_payload_query('$.aud')
http-request set-var(txn.exp) http_auth_bearer,jwt_payload_query('$.exp','int')

# Validate JWT (all requests at this point are on allowed paths)
http-request deny content-type 'text/html' string 'Unsupported JWT signing algorithm' unless { var(txn.alg) -m str RS256 }
http-request deny content-type 'text/html' string 'Invalid JWT signature' unless { http_auth_bearer,jwt_verify(txn.alg,"/etc/haproxy/jwt_keys/api_pubkey.pem") -m int 1 }

# Validate expiration
http-request set-var(txn.now) date()
http-request deny content-type 'text/html' string 'JWT has expired' if { var(txn.exp),sub(txn.now) -m int lt 0 }

Allow Anonymous Access (allow_anonymous=true)

# JWT Validator - Validate JWT tokens

# Allow anonymous access - validate JWT only if Authorization header is present

# Extract JWT header and payload
http-request set-var(txn.alg) http_auth_bearer,jwt_header_query('$.alg') if { req.hdr(authorization) -m found }
http-request set-var(txn.iss) http_auth_bearer,jwt_payload_query('$.iss') if { req.hdr(authorization) -m found }
http-request set-var(txn.aud) http_auth_bearer,jwt_payload_query('$.aud') if { req.hdr(authorization) -m found }
http-request set-var(txn.exp) http_auth_bearer,jwt_payload_query('$.exp','int') if { req.hdr(authorization) -m found }

# Validate JWT (only if Authorization header is present)
http-request deny content-type 'text/html' string 'Unsupported JWT signing algorithm' unless { var(txn.alg) -m str RS256 } if { req.hdr(authorization) -m found }
http-request deny content-type 'text/html' string 'Invalid JWT issuer' unless { var(txn.iss) -m str https://auth.example.com/ } if { req.hdr(authorization) -m found }
http-request deny content-type 'text/html' string 'Invalid JWT audience' unless { var(txn.aud) -m str https://api.example.com } if { req.hdr(authorization) -m found }
http-request deny content-type 'text/html' string 'Invalid JWT signature' unless { http_auth_bearer,jwt_verify(txn.alg,"/etc/haproxy/jwt_keys/api_pubkey.pem") -m int 1 } if { req.hdr(authorization) -m found }

# Validate expiration (only if Authorization header is present)
http-request set-var(txn.now) date() if { req.hdr(authorization) -m found }
http-request deny content-type 'text/html' string 'JWT has expired' if { var(txn.exp),sub(txn.now) -m int lt 0 } if { req.hdr(authorization) -m found }

What It Validates

  • Authorization header presence
  • JWT signing algorithm (RS256, RS512, etc.)
  • JWT issuer (if configured)
  • JWT audience (if configured)
  • JWT signature using public key
  • JWT expiration time

Important Notes

  • Required: HAProxy 2.5+ with JWT support
  • Mount public key file as read-only volume
  • The plugin runs once per domain during the discovery cycle
  • Test thoroughly with your JWT provider before deploying to production