diff --git a/.traefik.yml b/.traefik.yml index 9c450f2..f4d8e52 100644 --- a/.traefik.yml +++ b/.traefik.yml @@ -4,1963 +4,66 @@ type: middleware import: github.com/lukaszraczylo/traefikoidc summary: | - Universal OpenID Connect (OIDC) authentication middleware for Traefik. + OpenID Connect authentication middleware for Traefik. Replaces forward-auth + + oauth2-proxy with a single plugin that auto-detects all major OIDC + providers (Google, Azure AD, Auth0, Okta, Keycloak, AWS Cognito, GitLab, + generic) and OAuth 2.0 for GitHub. - This middleware replaces the need for forward-auth and oauth2-proxy when using Traefik as a reverse proxy. - It provides a complete OIDC authentication solution with features including domain restrictions, - role-based access control, session management, comprehensive security headers, automatic token refresh, - and support for all major OIDC providers with automatic configuration. + Features: ID-token validation with auto-discovery, session encryption, + proactive token refresh, RBAC via roles/groups claims, domain restriction, + templated downstream headers, security headers (CSP, HSTS, CORS), rate + limiting, PKCE, opaque-token introspection (RFC 7662), back/front-channel + logout, Dynamic Client Registration (RFC 7591), and Redis-backed shared + state for multi-replica deployments. - 🎯 SUPPORTED PROVIDERS (Auto-Detection): - ✅ Google - Full OIDC, auto-configured for Workspace - ✅ Azure AD - Enterprise OIDC with tenant/group support - ✅ Auth0 - Flexible OIDC with custom claims - ✅ Okta - Enterprise SSO with MFA support - ✅ Keycloak - Self-hosted OIDC with full customization - ✅ AWS Cognito - Managed OIDC with regional endpoints - ✅ GitLab - Both GitLab.com and self-hosted instances - ⚠️ GitHub - OAuth 2.0 only (limited: API access, no user claims) - ✅ Generic OIDC - Any RFC-compliant OIDC provider - - 🔧 KEY FEATURES: - - Automatic provider detection and configuration - - Comprehensive security headers (CSP, HSTS, CORS, custom profiles) - - Domain restrictions and role-based access control - - Automatic token refresh and session management - - Rate limiting and brute force protection - - Flexible configuration with multiple deployment scenarios - - Memory-efficient operation with automatic cleanup - - Extensive logging and debugging capabilities - - Redis cache support for multi-replica deployments with automatic failover - It supports various authentication scenarios including: - - - Basic authentication with customizable callback and logout URLs - - Email domain restrictions to limit access to specific organizations - - Role and group-based access control based on OIDC claims - - Public URLs that bypass authentication (excluded paths) - - Secure session management with encrypted cookies - - Automatic token validation and refresh - - Comprehensive security headers with multiple security profiles - - Rate limiting to prevent brute force attacks - - Custom headers using templated values from OIDC claims - - Flexible CORS configuration for API endpoints - - Configurable logging levels for debugging and monitoring + Full documentation: https://github.com/lukaszraczylo/traefikoidc testData: - # Required parameters - providerURL: https://accounts.google.com # Base URL of the OIDC provider - clientID: 1234567890.apps.googleusercontent.com # OAuth 2.0 client identifier - clientSecret: secret # OAuth 2.0 client secret - callbackURL: /oauth2/callback # Path where the OIDC provider will redirect after authentication - sessionEncryptionKey: potato-secret-is-at-least-32-bytes-long # Key used to encrypt session data (must be at least 32 bytes) + # Required + providerURL: https://accounts.google.com + clientID: 1234567890.apps.googleusercontent.com + clientSecret: your-client-secret + sessionEncryptionKey: potato-secret-is-at-least-32-bytes-long + callbackURL: /oauth2/callback - # Optional parameters with defaults - logoutURL: /oauth2/logout # Path for handling logout requests (if not provided, it will be set to callbackURL + "/logout") - postLogoutRedirectURI: /oidc/different-logout # URL to redirect to after logout (default: "/") + # Common production knobs + logoutURL: /oauth2/logout + postLogoutRedirectURI: / + forceHTTPS: true # default; only set false for plaintext HTTP local dev + logLevel: info + rateLimit: 100 - scopes: # Additional scopes to append to defaults ["openid", "profile", "email"] - - roles # Result: ["openid", "profile", "email", "roles"] - - allowedUserDomains: # Restricts access to specific email domains (if not provided, relies on OIDC provider) + # Access control + allowedUserDomains: - company.com - - subsidiary.com - - allowedUsers: # Restricts access to specific email addresses regardless of domain - - specific-user@company.com - - another-user@gmail.com - - allowedRolesAndGroups: # Restricts access to users with specific roles or groups (if not provided, no role/group restrictions) - - guest-endpoints + allowedRolesAndGroups: - admin - developer - - # Custom claim names for Auth0 and other providers with namespaced claims - roleClaimName: roles # JWT claim name for extracting user roles (default: "roles") - groupClaimName: groups # JWT claim name for extracting user groups (default: "groups") - userIdentifierClaim: email # JWT claim for user identification (default: "email", alternatives: "sub", "oid", "upn", "preferred_username") - - # ⚠️ CRITICAL for TLS termination scenarios (AWS ALB, Cloud Load Balancers, etc.) - # When NOT specified in config: defaults to FALSE (Go zero value) - # When running behind load balancer that terminates TLS: MUST set to TRUE - # See: https://github.com/lukaszraczylo/traefikoidc/issues/82 - forceHTTPS: true # Forces HTTPS scheme for redirect URIs (default when not specified: false) - logLevel: debug # Sets logging verbosity: debug, info, error (default: info) - rateLimit: 100 # Maximum number of requests per second (default: 100, minimum: 10) - - excludedURLs: # Lists paths that bypass authentication - - /login # covers /login, /login/me, /login/reminder etc. - - /public + excludedURLs: - /health - /metrics - - headers: # Custom headers to set with templated values from claims and tokens - # NOTE: Use double curly braces to escape template expressions in YAML - # See the headers section in configuration below for details - - name: "X-User-Email" + + # Scopes are appended to the defaults ["openid", "profile", "email"] + scopes: + - roles + + # Templated headers forwarded to backends. + # NOTE: use quadruple braces — the YAML parser collapses {{{{ → {{ so the + # Go template engine receives the correct expression. + headers: + - name: X-User-Email value: "{{{{.Claims.email}}}}" - - name: "X-User-ID" - value: "{{{{.Claims.sub}}}}" - - name: "Authorization" + - name: Authorization value: "Bearer {{{{.AccessToken}}}}" - - name: "X-User-Roles" - value: "{{{{range $i, $e := .Claims.roles}}}}{{{{if $i}}}},{{{{end}}}}{{{{$e}}}}{{{{end}}}}" - # Advanced parameters (usually discovered automatically from provider metadata) - revocationURL: https://accounts.google.com/revoke # Endpoint for revoking tokens - oidcEndSessionURL: https://accounts.google.com/logout # Provider's end session endpoint - enablePKCE: false # Enables PKCE (Proof Key for Code Exchange) for additional security - cookieDomain: "" # Explicit domain for session cookies (e.g., ".example.com" for multi-subdomain setups) - cookiePrefix: "" # Custom prefix for cookie names (e.g., "_oidc_myapp_" for session isolation between middleware instances) - sessionMaxAge: 86400 # Maximum session age in seconds (default: 86400 = 24 hours, 0 = use default) - overrideScopes: false # When true, replaces default scopes instead of appending (default: false) - refreshGracePeriodSeconds: 60 # Seconds before token expiry to attempt proactive refresh (default: 60) - - # Auth0 / Custom API Audience Configuration - audience: "" # Custom audience for access token validation (default: clientID) - strictAudienceValidation: false # Reject sessions with audience mismatch (prevents token confusion attacks) - allowOpaqueTokens: false # Enable opaque (non-JWT) access token support via RFC 7662 introspection - requireTokenIntrospection: false # Force introspection for opaque tokens (requires introspection endpoint) - disableReplayDetection: false # Disable JTI replay detection for multi-replica deployments (default: false) - allowPrivateIPAddresses: false # Allow private IP addresses in provider URLs for internal networks (default: false) - minimalHeaders: false # Reduce forwarded headers to prevent 431 errors (default: false) - stripAuthCookies: false # Strip OIDC session cookies before forwarding to backend (default: false) - - # Security Headers Configuration (enabled by default with 'default' profile) + # Security headers (default profile is enabled out of the box) securityHeaders: enabled: true - profile: "default" # Options: default, strict, development, api, custom - - # CORS configuration for API endpoints - corsEnabled: false - corsAllowedOrigins: - - "https://your-frontend.com" - - "https://*.example.com" - corsAllowCredentials: true - - # Cross-origin policies - permissionsPolicy: "geolocation=(), camera=(), microphone=()" - crossOriginEmbedderPolicy: "require-corp" - crossOriginOpenerPolicy: "same-origin" - crossOriginResourcePolicy: "same-origin" - - # Custom headers - customHeaders: - X-Custom-Header: "production" - X-API-Version: "v1" - -# Example with Redis cache for multi-replica deployments -testDataWithRedis: - # Required OIDC parameters (same as standard configuration) - providerURL: https://auth.example.com - clientID: your-client-id - clientSecret: your-client-secret - callbackURL: /oauth2/callback - sessionEncryptionKey: your-64-character-encryption-key-at-least-32-bytes - - # Standard optional parameters - logLevel: info - allowedUserDomains: - - company.com - - # Redis cache configuration for multi-replica support - redis: - enabled: true # Enable Redis caching - address: "redis:6379" # Redis server address - password: "redis-password" # Redis authentication password - db: 0 # Redis database number (0-15) - keyPrefix: "traefikoidc:" # Prefix for all Redis keys - cacheMode: "hybrid" # Cache mode: redis, hybrid, or memory - poolSize: 20 # Maximum number of connections - connectTimeout: 5 # Connection timeout in seconds - readTimeout: 3 # Read operation timeout - writeTimeout: 3 # Write operation timeout - enableTLS: false # Use TLS for Redis connection - tlsSkipVerify: false # Skip TLS certificate verification - hybridL1Size: 500 # L1 cache size for hybrid mode - hybridL1MemoryMB: 10 # L1 memory limit for hybrid mode - enableCircuitBreaker: true # Enable circuit breaker - circuitBreakerThreshold: 5 # Failures before opening circuit - circuitBreakerTimeout: 60 # Timeout before retry (seconds) - enableHealthCheck: true # Enable periodic health checks - healthCheckInterval: 30 # Health check interval (seconds) - -# --- Common Configuration Examples --- -# -# 🔒 HIGH-SECURITY CONFIGURATION -# testDataHighSecurity: -# providerURL: https://login.microsoftonline.com/your-tenant-id/v2.0 -# clientID: your-azure-client-id -# clientSecret: your-azure-client-secret -# callbackURL: /oauth2/callback -# sessionEncryptionKey: "maximum-security-key-at-least-32-bytes-long" -# rateLimit: 50 # Restrictive rate limiting -# allowedUserDomains: ["company.com"] # Domain restriction -# allowedRolesAndGroups: ["admin", "security-team"] # Role restriction -# securityHeaders: -# enabled: true -# profile: "strict" # Maximum security headers -# corsEnabled: false # No CORS in high-security mode -# logLevel: info - -# 🧑‍💻 DEVELOPMENT CONFIGURATION -# testDataDevelopment: -# providerURL: https://your-dev-provider.com -# clientID: dev-client-id -# clientSecret: dev-client-secret -# callbackURL: /oauth2/callback -# sessionEncryptionKey: "development-key-at-least-32-bytes-long" -# forceHTTPS: false # Allow HTTP in development -# excludedURLs: ["/health", "/metrics", "/debug"] -# securityHeaders: -# enabled: true -# profile: "development" # Relaxed security for development -# corsEnabled: true -# corsAllowedOrigins: ["http://localhost:*", "http://127.0.0.1:*"] -# logLevel: debug - -# 🌐 API CONFIGURATION -# testDataAPI: -# providerURL: https://your-auth0-domain.auth0.com -# clientID: api-client-id -# clientSecret: api-client-secret -# callbackURL: /oauth2/callback -# sessionEncryptionKey: "api-gateway-key-at-least-32-bytes-long" -# refreshGracePeriodSeconds: 120 -# securityHeaders: -# enabled: true -# profile: "api" -# corsEnabled: true -# corsAllowedOrigins: ["https://app.example.com"] -# corsAllowedMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"] -# corsAllowedHeaders: ["Authorization", "Content-Type", "X-API-Key"] -# headers: # Custom headers with OIDC claims (use double curly braces) -# - name: "X-User-Email" -# value: "{{{{.Claims.email}}}}" -# - name: "X-User-ID" -# value: "{{{{.Claims.sub}}}}" - -# --- Provider Specific Configuration Examples --- -# -# This middleware supports 9+ OIDC providers with automatic detection: -# ✅ Google - Full OIDC with auto-configuration -# ✅ Azure AD - Enterprise OIDC with tenant support -# ✅ Auth0 - Flexible OIDC with custom claims -# ✅ Okta - Enterprise OIDC with MFA support -# ✅ Keycloak - Self-hosted OIDC with full customization -# ✅ AWS Cognito - Managed OIDC with regional endpoints -# ✅ GitLab - Both GitLab.com and self-hosted -# ⚠️ GitHub - OAuth 2.0 only (not OIDC, limited functionality) -# ✅ Generic OIDC - Any RFC-compliant OIDC provider -# -# Uncomment and adapt the relevant section for your provider. -# Remember to replace placeholder values with your actual credentials. -# For all providers, ensure claims like email, roles, and groups are -# configured to be included in the ID TOKEN (this plugin validates ID tokens). - -# --- Keycloak Example --- -# testDataKeycloak: -# providerURL: https://your-keycloak-domain/realms/your-realm # e.g., http://localhost:8080/realms/master -# clientID: your-keycloak-client-id -# clientSecret: your-keycloak-client-secret # Store securely, e.g., urn:k8s:secret:namespace:secret-name:key -# callbackURL: /oauth2/callback -# sessionEncryptionKey: "a-very-secure-key-at-least-32-bytes-long-for-keycloak" -# scopes: # Default ["openid", "profile", "email"] are usually sufficient. Add others if mappers depend on them. -# - roles # Example: if you mapped Keycloak roles to a 'roles' claim in the ID token -# - groups # Example: if you mapped Keycloak groups to a 'groups' claim in the ID token -# allowedRolesAndGroups: # Corresponds to 'Token Claim Name' in Keycloak mappers -# - admin -# - editor -# # For internal Keycloak deployments with private IPs (Docker/Kubernetes internal): -# # allowPrivateIPAddresses: true # Enable for private IP addresses like 192.168.x.x, 10.x.x.x -# # Ensure Keycloak client mappers add 'email', 'roles', 'groups' etc. to the ID Token. -# # See README.md "Provider Configuration Recommendations" for Keycloak. - -# --- Azure AD (Microsoft Entra ID) Example --- -# testDataAzureAD: -# providerURL: https://login.microsoftonline.com/your-tenant-id/v2.0 # Replace your-tenant-id -# clientID: your-azure-ad-client-id -# clientSecret: your-azure-ad-client-secret # Store securely -# callbackURL: /oauth2/callback -# sessionEncryptionKey: "a-very-secure-key-at-least-32-bytes-long-for-azure" -# scopes: # Defaults ["openid", "profile", "email"] are good. -# # Azure AD may require specific scopes for certain graph API permissions if you were to use the access token, -# # but for ID token claims, defaults are often enough. -# # Group claims need to be configured in Azure AD App Registration -> Token Configuration -> Add groups claim. -# allowedUserDomains: -# - yourcompany.com -# allowedRolesAndGroups: # If you configured group claims (typically 'groups') or app roles in Azure AD -# - "group-object-id-1" # Azure AD group claims can be Object IDs by default -# - "AppRoleName" -# # See README.md "Provider Configuration Recommendations" for Azure AD. - -# --- Azure AD Users Without Email Example (Issue #95) --- -# testDataAzureADNoEmail: -# providerURL: https://login.microsoftonline.com/your-tenant-id/v2.0 -# clientID: your-azure-ad-client-id -# clientSecret: your-azure-ad-client-secret -# callbackURL: /oauth2/callback -# sessionEncryptionKey: "a-very-secure-key-at-least-32-bytes-long-for-azure" -# # Use 'sub' claim instead of 'email' for user identification -# userIdentifierClaim: sub # or "oid", "upn", "preferred_username" -# overrideScopes: true # Remove email scope if not needed -# scopes: -# - openid -# - profile -# - groups # For group-based access control -# # When using non-email identifiers, allowedUsers matches against the claim value -# allowedUsers: -# - "abc12345-6789-0abc-def0-123456789abc" # Azure AD user object ID (sub or oid claim) -# # NOTE: allowedUserDomains is ignored when userIdentifierClaim is not "email" -# # See: https://github.com/lukaszraczylo/traefikoidc/issues/95 - -# --- Google Workspace / Google Cloud Identity Example --- -# testDataGoogle: -# providerURL: https://accounts.google.com # Standard Google OIDC endpoint -# clientID: your-google-client-id.apps.googleusercontent.com -# clientSecret: your-google-client-secret # Store securely -# callbackURL: /oauth2/callback -# sessionEncryptionKey: "a-very-secure-key-at-least-32-bytes-long-for-google" -# scopes: # Auto-detects Google and applies proper configuration -# # Do NOT add 'offline_access' - plugin automatically handles Google-specific parameters -# allowedUserDomains: # Useful for Google Workspace domain restriction -# - your-gsuite-domain.com -# refreshGracePeriodSeconds: 300 # Optional: Refresh 5 min before expiry -# # Google auto-config: Uses access_type=offline, prompt=consent, filters unsupported scopes -# # Available claims: email, sub, name, given_name, family_name, picture, hd (hosted domain) - -# --- Okta Example --- -# testDataOkta: -# providerURL: https://your-tenant.okta.com/oauth2/default # Use your Okta domain and auth server -# clientID: your-okta-client-id -# clientSecret: your-okta-client-secret # Store securely -# callbackURL: /oauth2/callback -# sessionEncryptionKey: "a-very-secure-key-at-least-32-bytes-long-for-okta" -# scopes: -# - groups # Include for group-based access control -# allowedRolesAndGroups: -# - admin -# - developer -# - "Everyone" # Default Okta group -# # Okta config: Create OIDC Web App in admin console, configure Groups claim -# # Available claims: email, sub, name, groups, custom attributes - -# --- AWS Cognito Example --- -# testDataCognito: -# providerURL: https://cognito-idp.us-east-1.amazonaws.com/us-east-1_YourUserPool # Regional endpoint -# clientID: your-cognito-client-id -# clientSecret: your-cognito-client-secret # Store securely -# callbackURL: /oauth2/callback -# sessionEncryptionKey: "a-very-secure-key-at-least-32-bytes-long-for-cognito" -# scopes: -# - aws.cognito.signin.user.admin # Cognito-specific scope -# allowedRolesAndGroups: -# - admin -# - user -# # Cognito config: Create User Pool, App Client with authorization code grant -# # Available claims: email, sub, cognito:username, cognito:groups, custom attributes - -# --- GitLab Example --- -# testDataGitLab: -# providerURL: https://gitlab.com # For GitLab.com, or use your self-hosted URL -# clientID: your-gitlab-client-id -# clientSecret: your-gitlab-client-secret # Store securely -# callbackURL: /oauth2/callback -# sessionEncryptionKey: "a-very-secure-key-at-least-32-bytes-long-for-gitlab" -# scopes: -# - read_user -# - read_api # For GitLab API access -# allowedUserDomains: -# - yourcompany.com # Optional domain restriction -# # GitLab config: Create application in GitLab Admin Area > Applications -# # Available claims: email, sub, name, nickname, preferred_username - -# --- GitHub OAuth 2.0 Example (⚠️ Limited Functionality) --- -# testDataGitHub: -# providerURL: https://github.com/login/oauth # GitHub OAuth endpoint (NOT OIDC) -# clientID: your-github-client-id -# clientSecret: your-github-client-secret # Store securely -# callbackURL: /oauth2/callback -# sessionEncryptionKey: "a-very-secure-key-at-least-32-bytes-long-for-github" -# scopes: -# - user:email -# - read:user -# # ⚠️ IMPORTANT: GitHub uses OAuth 2.0, NOT OpenID Connect -# # - No ID tokens available (access tokens only) -# # - No refresh tokens (users must re-authenticate when tokens expire) -# # - No standard OIDC claims -# # - Use only for GitHub API access, not for user authentication with claims -# # GitHub config: Create OAuth App in GitHub Settings > Developer settings - -# --- Auth0 Example --- -# testDataAuth0: -# providerURL: https://your-auth0-domain.auth0.com # Replace with your Auth0 domain -# clientID: your-auth0-client-id -# clientSecret: your-auth0-client-secret # Store securely -# callbackURL: /oauth2/callback -# sessionEncryptionKey: "a-very-secure-key-at-least-32-bytes-long-for-auth0" -# -# # Auth0 Audience Configuration (for custom APIs) -# # Scenario 1 (Recommended): Custom API with JWT access tokens -# audience: "https://my-api.example.com" # Your API identifier from Auth0 dashboard -# strictAudienceValidation: true # Enforce proper audience validation for security -# -# # Scenario 2 (Backward Compatible): Default audience (uses client_id) -# # audience: "" # Leave empty or omit to use client_id as audience -# # strictAudienceValidation: false # Allows fallback to ID token validation (logs warnings) -# -# # Scenario 3: Opaque (non-JWT) access tokens -# # allowOpaqueTokens: true # Enable opaque token support -# # requireTokenIntrospection: true # Require RFC 7662 token introspection -# -# scopes: # Defaults ["openid", "profile", "email"]. Add custom scopes if your Auth0 Rules/Actions require them. -# - read:custom_data # Example custom scope -# allowedRolesAndGroups: # Based on claims added via Auth0 Rules or Actions (e.g. namespaced claims) -# - "https://your-app.com/roles:admin" -# - editor -# # Use Auth0 Rules or Actions to add custom claims (roles, permissions) to the ID Token. -# # Ensure postLogoutRedirectURI is in Auth0 app's "Allowed Logout URLs". -# # For detailed Auth0 audience configuration, see AUTH0_AUDIENCE_GUIDE.md - -# --- Generic OIDC Provider Example --- -# testDataGenericOIDC: -# providerURL: https://your-generic-oidc-provider.com/oidc # Issuer URL for your provider -# clientID: your-generic-client-id -# clientSecret: your-generic-client-secret # Store securely -# callbackURL: /oauth2/callback -# sessionEncryptionKey: "a-very-secure-key-at-least-32-bytes-long-for-generic" -# scopes: # Must include "openid". "profile" and "email" are common. -# - openid -# - profile -# - email -# - custom_scope_for_claims # If your provider needs specific scopes for ID token claims -# allowedRolesAndGroups: -# - user_role_from_id_token -# # Consult your provider's documentation on how to map attributes/roles/groups to ID Token claims. -# # Verify ID Token contents (e.g. jwt.io) to see available claims. -# # See README.md "Provider Configuration Recommendations" for Generic OIDC. - -# Configuration documentation -configuration: - providerURL: - type: string - description: | - The base URL of the OIDC provider. This is the issuer URL that will be used to discover - OIDC endpoints like authorization, token, and JWKS URIs. - - Supported providers (auto-detected from URL): - - https://accounts.google.com (Google) - - https://login.microsoftonline.com/tenant-id/v2.0 (Azure AD) - - https://your-auth0-domain.auth0.com (Auth0) - - https://your-tenant.okta.com/oauth2/default (Okta) - - https://your-keycloak/auth/realms/your-realm (Keycloak) - - https://cognito-idp.region.amazonaws.com/pool-id (AWS Cognito) - - https://gitlab.com (GitLab) - - https://github.com/login/oauth (GitHub - OAuth 2.0 only) - - Any RFC-compliant OIDC provider (Generic) - required: true - - clientID: - type: string - description: | - The OAuth 2.0 client identifier obtained from your OIDC provider. - This is the public identifier for your application. - required: true - - clientSecret: - type: string - description: | - The OAuth 2.0 client secret obtained from your OIDC provider. - This should be kept confidential and not exposed in client-side code. - - For Kubernetes deployments, you can use the secret reference format: - urn:k8s:secret:namespace:secret-name:key - required: true - - callbackURL: - type: string - description: | - The path where the OIDC provider will redirect after authentication. - This must match one of the redirect URIs configured in your OIDC provider. - - The full redirect URI will be constructed as: - [scheme]://[host][callbackURL] - - Example: /oauth2/callback - required: true - - sessionEncryptionKey: - type: string - description: | - Key used to encrypt session data stored in cookies. - Must be at least 32 bytes long for security. - - Example: potato-secret-is-at-least-32-bytes-long - required: true - - logoutURL: - type: string - description: | - The path for handling logout requests. - If not provided, it will be set to callbackURL + "/logout". - - Example: /oauth2/logout - required: false - - postLogoutRedirectURI: - type: string - description: | - The URL to redirect to after logout. - Default: "/" - - Example: /logged-out-page - required: false - - scopes: - type: array - description: | - Additional OAuth 2.0 scopes to append to the default scopes. - Default scopes are always included: ["openid", "profile", "email"] - - User-provided scopes are appended to defaults with automatic deduplication. - For example, specifying ["roles", "custom_scope"] results in: - ["openid", "profile", "email", "roles", "custom_scope"] - - Include "roles" or similar scope if you need role/group information. - Note: For Google OAuth, the middleware automatically handles the - proper authentication parameters and does NOT require the "offline_access" - scope (which Google rejects as invalid). See documentation for details. - required: false - items: - type: string - - logLevel: - type: string - description: | - Sets the logging verbosity. - Valid values: "debug", "info", "error" - Default: "info" - required: false - enum: - - debug - - info - - error - - forceHTTPS: - type: boolean - description: | - Forces HTTPS scheme for redirect URIs regardless of request headers or TLS state. - - ⚠️ CRITICAL CONFIGURATION for TLS Termination Scenarios: - - When running Traefik behind a load balancer that terminates TLS (AWS ALB, - Google Cloud Load Balancer, Azure Application Gateway, etc.), you MUST set - this to true. Without it, redirect URIs will use http:// instead of https://, - causing OAuth callback failures. - - How it works: - - When true: Always uses https:// for redirect URIs (highest priority) - - When false: Detects scheme from X-Forwarded-Proto header or TLS state - - When NOT specified: Defaults to false (Go zero value for bool) - - Default: false (when not specified in configuration) - Recommended: true (for production environments and TLS termination scenarios) - - See: https://github.com/lukaszraczylo/traefikoidc/issues/82 - required: false - - rateLimit: - type: integer - description: | - Sets the maximum number of requests per second. - This helps prevent brute force attacks. - Default: 100 - Minimum: 10 - required: false - - excludedURLs: - type: array - description: | - Lists paths that bypass authentication. - These paths will be accessible without OIDC authentication. - - The middleware uses prefix matching, so "/public" will match - "/public", "/public/page", "/public-data", etc. - - Examples: ["/health", "/metrics", "/public"] - required: false - items: - type: string - - allowedUserDomains: - type: array - description: | - Restricts access to users with email addresses from specific domains. - If not provided, the middleware relies entirely on the OIDC provider - for authentication decisions. - - Examples: ["company.com", "subsidiary.com"] - required: false - items: - type: string - - allowedUsers: - type: array - description: | - Restricts access to specific email addresses. - If provided, only users with these exact email addresses will be allowed access, - in addition to any domain-level restrictions set by allowedUserDomains. - - This provides fine-grained control over individual access and can be used - together with allowedUserDomains for flexible access control strategies. - - Examples: ["user1@example.com", "admin@company.com"] - required: false - items: - type: string - - allowedRolesAndGroups: - type: array - description: | - Restricts access to users with specific roles or groups. - If not provided, no role/group restrictions are applied. - - The middleware checks both the "roles" and "groups" claims in the ID token. - - Examples: ["admin", "developer"] - required: false - items: - type: string - - userIdentifierClaim: - type: string - description: | - Specifies the JWT claim to use as the user identifier for authentication and authorization. - - This allows authentication for users without email addresses, such as Azure AD service - accounts or organizational accounts that don't have email attributes configured. - - When set to a non-email claim (e.g., "sub", "oid", "upn"): - - AllowedUsers will match against this claim value instead of email - - AllowedUserDomains validation is skipped (domains only apply to email addresses) - - The session stores this identifier as the user's identity - - If the configured claim is missing, falls back to "sub" (required by OIDC spec) - - Common values by provider: - - Default: "email" (standard email-based identification) - - Azure AD: "sub", "oid" (object ID), "upn" (User Principal Name), "preferred_username" - - Generic OIDC: "sub" (always present per OIDC specification) - - Keycloak: "sub", "preferred_username" - - Example for Azure AD users without email: - ```yaml - userIdentifierClaim: sub - allowedUsers: - - "abc123-user-object-id" - - "xyz789-another-user-id" - ``` - - Default: "email" - See: https://github.com/lukaszraczylo/traefikoidc/issues/95 - required: false - - revocationURL: - type: string - description: | - The endpoint for revoking tokens. - If not provided, it will be discovered from provider metadata. - - Example: https://accounts.google.com/revoke - required: false - - oidcEndSessionURL: - type: string - description: | - The provider's end session endpoint. - If not provided, it will be discovered from provider metadata. - - Example: https://accounts.google.com/logout - required: false - - enablePKCE: - type: boolean - description: | - Enables PKCE (Proof Key for Code Exchange) for the OAuth 2.0 authorization code flow. - PKCE adds an extra layer of security to protect against authorization code interception attacks. - - Not all OIDC providers support PKCE, so this should only be enabled if your provider supports it. - If enabled, the middleware will generate and use a code verifier/challenge pair during authentication. - - Default: false - required: false - - cookieDomain: - type: string - description: | - Explicit domain for session cookies. This is important for multi-subdomain setups - and reverse proxy deployments to ensure consistent cookie handling. - - When set, all session cookies will use this domain. When not set, the domain - is auto-detected from the request headers (X-Forwarded-Host or Host). - - Use a leading dot for subdomain-wide cookies (e.g., ".example.com" allows - cookies to be shared between app.example.com, api.example.com, etc.). - - Use a specific domain for host-only cookies (e.g., "app.example.com" restricts - cookies to that exact domain). - - This setting is crucial to prevent authentication issues like "CSRF token missing - in session" errors that can occur when cookies are created with inconsistent domains. - - Examples: - - ".example.com" - Allows all subdomains to share cookies - - "app.example.com" - Restricts cookies to this specific host - - Default: "" (auto-detected from request headers) - required: false - - cookiePrefix: - type: string - description: | - Custom prefix for session cookie names. This is essential for running multiple - middleware instances with different authorization requirements on the same domain. - - By default, all middleware instances use the same cookie names (_oidc_raczylo_m, - _oidc_raczylo_a, etc.), which means they share session state. When you have - multiple instances with different access restrictions (e.g., one for general users - and one for admins), this session sharing can lead to authorization bypass issues. - - Setting a unique cookiePrefix for each middleware instance ensures complete - session isolation, preventing users authenticated via one middleware from - automatically gaining access to routes protected by a different middleware. - - The prefix is prepended to all session cookie names: - - Main session cookie: {prefix}m - - Access token cookie: {prefix}a - - Refresh token cookie: {prefix}r - - ID token cookie: {prefix}id - - Examples: - - "_oidc_userauth_" - For general user authentication middleware - - "_oidc_adminauth_" - For admin-only authentication middleware - - "_oidc_api_" - For API-specific authentication middleware - - Security Note: Use different cookie prefixes AND different sessionEncryptionKey - values for each middleware instance to ensure complete isolation. - - Default: "_oidc_raczylo_" (standard prefix for backward compatibility) - - See: https://github.com/lukaszraczylo/traefikoidc/issues/87 - required: false - - sessionMaxAge: - type: integer - description: | - Maximum session age in seconds before requiring re-authentication. - - This setting controls how long a user's authentication session remains valid - before they must authenticate again through the OIDC provider. The session - age is tracked from the initial authentication time (created_at). - - When a session exceeds this age: - - The session is cleared and invalidated - - The user is redirected to re-authenticate - - All session cookies are removed - - Use Cases: - - High-security applications: Use shorter durations (e.g., 3600 = 1 hour) - - Standard applications: Default 24 hours balances security and UX - - Long-lived sessions: Extend for applications accessed infrequently - (e.g., 604800 = 7 days, 2592000 = 30 days) - - Security Considerations: - - Shorter sessions provide better security but require more frequent logins - - Longer sessions improve user experience but increase security risk - - Consider your application's security requirements and user access patterns - - This is independent of token refresh - tokens can be refreshed during the session - - Common Values: - - 3600 (1 hour) - High security applications - - 28800 (8 hours) - Working day session - - 86400 (24 hours) - Default, balances security and convenience - - 604800 (7 days) - Weekly session for less frequently accessed apps - - 2592000 (30 days) - Monthly session for infrequently used applications - - Default: 86400 (24 hours) - Minimum: 0 (uses default of 24 hours) - - See: https://github.com/lukaszraczylo/traefikoidc/issues/91 - required: false - - overrideScopes: - type: boolean - description: | - When set to true, the scopes you provide will completely replace the default scopes - (openid, profile, email) instead of being appended to them. - - This is useful when you need precise control over the scopes sent to the OIDC provider, - such as when a provider requires specific scopes or when you want to minimize the - requested permissions. - - Default: false (appends user scopes to defaults) - required: false - - refreshGracePeriodSeconds: - type: integer - description: | - The number of seconds before a token expires to attempt proactive refresh. - - When a request is made and the access token will expire within this grace period, - the middleware will attempt to refresh the token proactively. This helps prevent - authentication interruptions for active users. - - Setting this to 0 disables proactive refresh (tokens are only refreshed after expiry). - - Default: 60 (1 minute before expiry) - required: false - - audience: - type: string - description: | - Custom audience value for access token validation. - - This configures the expected audience claim in access tokens. Per OAuth 2.0 and OIDC - specifications: - - ID tokens always have aud=client_id (per OIDC Core 1.0) - - Access tokens can have custom audiences (e.g., API identifiers) - - Auth0 Scenarios: - 1. Custom API audience (recommended): Set to your API identifier from Auth0 - Example: "https://my-api.example.com" - Result: Access tokens will contain this audience - - 2. Default audience: Leave empty or omit (uses client_id) - Result: Access tokens may not contain client_id, triggering warnings - - 3. Opaque tokens: Use with allowOpaqueTokens=true for non-JWT tokens - - When configured and different from client_id, the middleware automatically adds - the audience parameter to the authorize endpoint request. - - Default: "" (uses client_id as audience) - See: AUTH0_AUDIENCE_GUIDE.md for detailed configuration - required: false - - strictAudienceValidation: - type: boolean - description: | - Enforce strict audience validation for access tokens. - - When enabled, sessions are rejected if access token validation fails due to - audience mismatch. This prevents falling back to ID token validation, addressing - potential token confusion attacks where tokens intended for different APIs could - be used to grant access. - - Auth0 Scenario 2 Protection: - - When true: Rejects sessions with mismatched access token audience - - When false: Logs security warnings but allows fallback to ID token (backward compatible) - - Security Recommendation: - - Production environments: Set to true for maximum security - - Development/testing: Can use false with monitoring of security warnings - - This setting addresses security concerns where access tokens without proper - audience claims could bypass API-specific authorization checks. - - Default: false (backward compatible) - See: https://github.com/lukaszraczylo/traefikoidc/issues/74 - required: false - - allowOpaqueTokens: - type: boolean - description: | - Enable acceptance of opaque (non-JWT) access tokens. - - When enabled, the middleware accepts access tokens that are not in JWT format - (3-part base64 structure). Opaque tokens are validated using OAuth 2.0 Token - Introspection (RFC 7662) if the provider exposes an introspection endpoint. - - Auth0 Scenario 3: - Some Auth0 configurations issue opaque access tokens when no default API is - configured. This setting allows those tokens to be validated. - - Requirements: - - Provider must support introspection_endpoint in OIDC discovery - - Client must have appropriate introspection permissions - - Validation Process: - 1. Detects opaque token (not 3-part JWT structure) - 2. Calls provider's introspection endpoint with client credentials - 3. Validates response (active status, expiration, audience if present) - 4. Caches result for 5 minutes or token expiry (whichever is shorter) - 5. Falls back to ID token validation if introspection unavailable - (unless requireTokenIntrospection=true) - - Default: false (only JWT access tokens accepted) - See: AUTH0_AUDIENCE_GUIDE.md for configuration examples - required: false - - requireTokenIntrospection: - type: boolean - description: | - Require token introspection for all opaque access tokens. - - When enabled with allowOpaqueTokens=true, opaque tokens are rejected if: - - Introspection endpoint is not available from provider metadata - - Introspection request fails - - Introspection response indicates token is not active - - Security Levels: - - requireTokenIntrospection=true + allowOpaqueTokens=true: - Maximum security - rejects opaque tokens without successful introspection - - - requireTokenIntrospection=false + allowOpaqueTokens=true: - Backward compatible - falls back to ID token validation if introspection fails - - - requireTokenIntrospection=true + allowOpaqueTokens=false: - No effect - opaque tokens are already rejected - - Recommended Configuration: - When accepting opaque tokens, always set this to true for maximum security: - ```yaml - allowOpaqueTokens: true - requireTokenIntrospection: true - ``` - - Default: false (allows fallback to ID token) - See: RFC 7662 OAuth 2.0 Token Introspection specification - required: false - - disableReplayDetection: - type: boolean - description: | - Disable JTI-based replay attack detection for multi-replica deployments. - - When running multiple Traefik replicas, each instance maintains its own in-memory - JTI (JWT Token ID) cache. This causes false positives when the same valid token - hits different replicas: - - Request → Replica A → JTI added to cache → OK - - Request → Replica B → JTI not in Replica B's cache → OK - - Request → Replica A again → JTI found → FALSE POSITIVE "replay detected" - - Security Impact: - When disabled, the following validations remain active: - - RSA/ECDSA signature verification - - Token expiration (exp claim) - - Issuer validation (iss claim) - - Audience validation (aud claim) - - Not-before validation (nbf claim) - - Issued-at validation (iat claim) - - Only the JTI replay check is skipped. - - Recommendations: - - Single-instance deployment: false (default, enables replay protection) - - Multi-replica deployment: true (prevents false positives) - - Production with shared cache: false (use Redis/Memcached for shared JTI cache) - - Default: false (replay detection enabled) - required: false - - allowPrivateIPAddresses: - type: boolean - description: | - Allow private IP addresses in OIDC provider URLs for internal network deployments. - - By default, the plugin blocks URLs containing private IP address ranges - (10.x.x.x, 172.16-31.x.x, 192.168.x.x) to prevent SSRF attacks and ensure - OIDC providers are publicly accessible. - - Enable this option when: - - Your OIDC provider (e.g., Keycloak) runs on an internal network with private IPs - - You don't have DNS resolution available for internal services - - Your entire stack runs in a Docker network or Kubernetes cluster with private addressing - - When enabled, the plugin will accept provider URLs like: - - https://192.168.1.100:8443/auth/realms/your-realm - - https://10.0.0.50:8080/realms/master - - https://172.16.0.10/auth - - Security Warning: - Enabling this option reduces SSRF protection. Only use in trusted network - environments where the OIDC provider is known and controlled. Loopback - addresses (127.0.0.1, localhost, ::1) remain blocked even with this option enabled. - - Default: false (private IPs are blocked for security) - See: https://github.com/lukaszraczylo/traefikoidc/issues/97 - required: false - - minimalHeaders: - type: boolean - description: | - Reduce forwarded headers to prevent "431 Request Header Fields Too Large" errors. - - When enabled, the middleware only forwards the X-Forwarded-User header and skips - the larger authentication headers that can cause downstream services to reject - requests due to header size limits (typically 8KB). - - Headers when disabled (default): - - X-Forwarded-User: User's email address (always set) - - X-Auth-Request-Redirect: Original request URI - - X-Auth-Request-User: User's email address - - X-Auth-Request-Token: Full ID token (can be very large with many claims) - - X-User-Groups: Comma-separated user groups (if configured) - - X-User-Roles: Comma-separated user roles (if configured) - - Headers when enabled: - - X-Forwarded-User: User's email address (always set) - - X-User-Groups: Comma-separated user groups (if configured, still forwarded) - - X-User-Roles: Comma-separated user roles (if configured, still forwarded) - - Custom templated headers (still processed) - - Use this option when: - - Downstream services return "431 Request Header Fields Too Large" errors - - Your ID tokens are large (many claims, long group lists) - - You don't need the full ID token forwarded to backend services - - You want to reduce request overhead - - Default: false (all headers forwarded for backward compatibility) - See: https://github.com/lukaszraczylo/traefikoidc/issues/64 - required: false - - stripAuthCookies: - type: boolean - description: | - Strip OIDC session cookies from the request before forwarding to backend services. - - When enabled, the middleware removes all cookies with the OIDC prefix (default: _oidc_raczylo_) - from the Cookie header before the request is forwarded to the backend. The cookies remain - in the browser and are still sent to Traefik for session management — they are only removed - from the Traefik-to-backend hop. - - This prevents "431 Request Header Fields Too Large" errors caused by large OIDC session - cookies (which can reach ~28KB with token chunking) being forwarded to backend services - that have limited header buffer sizes. - - Non-OIDC cookies (application sessions, preferences, etc.) are always passed through - untouched. - - Use this option when: - - Backend services return "431 Request Header Fields Too Large" errors - - OIDC session cookies are large due to token chunking - - Backend services don't need OIDC session cookies - - You want to reduce Cookie header overhead on backend requests - - Can be combined with minimalHeaders for maximum header size reduction. - - Default: false (all cookies forwarded for backward compatibility) - See: https://github.com/lukaszraczylo/traefikoidc/issues/122 - required: false - - enableBackchannelLogout: - type: boolean - description: | - Enable OIDC Back-Channel Logout (IdP-initiated logout via server-to-server POST). - - When enabled, the middleware accepts logout tokens at the configured backchannelLogoutURL. - The IdP sends a signed JWT (logout_token) to notify the application that a user's session - should be terminated. - - This implements the OIDC Back-Channel Logout 1.0 specification. - See: https://openid.net/specs/openid-connect-backchannel-1_0.html - - Requirements: - - backchannelLogoutURL must be configured - - The IdP must be configured to send logout tokens to your backchannel URL - - Logout tokens are validated using the IdP's JWKS - - Default: false - required: false - - backchannelLogoutURL: - type: string - description: | - Path for receiving backchannel logout tokens from the IdP. - - This endpoint receives POST requests with a logout_token JWT in the request body. - The token is validated against the IdP's JWKS and contains the session ID (sid) - and/or subject (sub) to invalidate. - - Example: /backchannel-logout - - The full URL to configure in your IdP would be: - https://your-app.example.com/backchannel-logout - - Note: This path should be unique and not conflict with your application routes. - required: false - - enableFrontchannelLogout: - type: boolean - description: | - Enable OIDC Front-Channel Logout (IdP-initiated logout via iframe). - - When enabled, the middleware accepts logout requests at the configured frontchannelLogoutURL. - The IdP embeds an iframe pointing to this URL when the user logs out, allowing the - application to clear the user's session. - - This implements the OIDC Front-Channel Logout 1.0 specification. - See: https://openid.net/specs/openid-connect-frontchannel-1_0.html - - Requirements: - - frontchannelLogoutURL must be configured - - The IdP must be configured with your front-channel logout URL - - Your CSP headers must allow being embedded in an iframe from the IdP - - Default: false - required: false - - frontchannelLogoutURL: - type: string - description: | - Path for receiving front-channel logout requests from the IdP. - - This endpoint receives GET requests with optional sid (session ID) and iss (issuer) - query parameters. When called, it invalidates the user's session. - - Example: /frontchannel-logout - - The full URL to configure in your IdP would be: - https://your-app.example.com/frontchannel-logout - - Note: This path should be unique and not conflict with your application routes. - required: false - - headers: - type: array - description: | - Custom HTTP headers to set with templated values derived from OIDC claims and tokens. - Each header has a name and a value template that can access: - - {{.Claims.field}} - Access ID token claims (e.g., email, sub, name) - - {{.AccessToken}} - The raw access token string - - {{.IdToken}} - The raw ID token string - - {{.RefreshToken}} - The raw refresh token string - - Templates support Go template syntax including conditionals and iteration. - Variable names are case-sensitive - use .Claims not .claims. - - IMPORTANT: Template Escaping - If you encounter the error "can't evaluate field AccessToken in type bool" when - starting Traefik, this means Traefik is trying to evaluate the template expressions - before passing them to the plugin. - - SOLUTION: You must escape the template expressions using double curly braces: - - headers: - - name: "Authorization" - value: "Bearer {{{{.AccessToken}}}}" - - This is the only reliable method that works consistently. Here's why: - - The YAML parser converts {{{{ → {{ and }}}} → }} - - Result: Bearer {{.AccessToken}} reaches the Go template engine correctly - - Other methods (YAML literal style, single quotes) do NOT work reliably - - Examples: - - name: "X-User-Email", value: "{{{{.Claims.email}}}}" - - name: "Authorization", value: "Bearer {{{{.AccessToken}}}}" - - name: "X-User-Roles", value: "{{{{range $i, $e := .Claims.roles}}}}{{{{if $i}}}},{{{{end}}}}{{{{$e}}}}{{{{end}}}}" - required: false - items: - type: object - properties: - name: - type: string - description: The HTTP header name to set - value: - type: string - description: Template string for the header value - - securityHeaders: - type: object - description: | - Configuration for security headers to protect against common web vulnerabilities. - Security headers are applied to all authenticated responses. - - The middleware includes comprehensive security headers support with multiple profiles: - - default: Balanced security for standard web applications - - strict: Maximum security for high-security applications - - development: Relaxed policies for local development - - api: API-friendly configuration with CORS support - - custom: Full control over all security header settings - - Security features include: - - Content Security Policy (CSP) to prevent XSS attacks - - HTTP Strict Transport Security (HSTS) to enforce HTTPS - - Frame Options to prevent clickjacking - - XSS Protection for browser-level filtering - - Content Type Options to prevent MIME sniffing - - CORS headers for cross-origin resource sharing - - Custom headers for additional security requirements - - Example configurations: - - Basic security (recommended): - securityHeaders: - enabled: true - profile: "default" - - API with CORS: - securityHeaders: - enabled: true - profile: "api" - corsEnabled: true - corsAllowedOrigins: ["https://app.example.com"] - - Custom configuration: - securityHeaders: - enabled: true - profile: "custom" - contentSecurityPolicy: "default-src 'self'" - corsEnabled: true - corsAllowedOrigins: ["https://*.example.com"] - customHeaders: - X-Security-Level: "high" - required: false - properties: - enabled: - type: boolean - description: | - Enable or disable security headers. - When disabled, only basic fallback headers are applied. - Default: true - required: false - - profile: - type: string - description: | - Security profile to use. Each profile provides a different balance of security and functionality: - - - default: Balanced security suitable for most web applications - - strict: Maximum security with very restrictive policies - - development: Relaxed policies for local development (enables localhost CORS) - - api: API-friendly configuration with configurable CORS - - custom: No defaults, use only explicitly configured settings - - Default: "default" - required: false - enum: - - default - - strict - - development - - api - - custom - - contentSecurityPolicy: - type: string - description: | - Content Security Policy header value to prevent XSS and code injection attacks. - Only applied when using "custom" profile or to override profile defaults. - - Examples: - - "default-src 'self'" (strict) - - "default-src 'self'; script-src 'self' 'unsafe-inline'" (moderate) - - "default-src 'self' 'unsafe-inline' 'unsafe-eval'" (permissive) - required: false - - strictTransportSecurity: - type: boolean - description: | - Enable HTTP Strict Transport Security (HSTS) to force HTTPS connections. - Only applied when HTTPS is detected (via TLS or X-Forwarded-Proto header). - Default: true - required: false - - strictTransportSecurityMaxAge: - type: integer - description: | - HSTS max-age value in seconds. Determines how long browsers should enforce HTTPS. - Common values: - - 31536000 (1 year) - recommended for production - - 86400 (1 day) - for testing - Default: 31536000 - required: false - - strictTransportSecuritySubdomains: - type: boolean - description: | - Include subdomains in HSTS policy. - When true, HSTS applies to all subdomains of the current domain. - Default: true - required: false - - strictTransportSecurityPreload: - type: boolean - description: | - Enable HSTS preload list eligibility. - Allows the domain to be included in browser HSTS preload lists. - Default: true - required: false - - frameOptions: - type: string - description: | - X-Frame-Options header value to prevent clickjacking attacks. - - Options: - - DENY: Prevents framing completely - - SAMEORIGIN: Allows framing only from the same origin - - ALLOW-FROM uri: Allows framing from specific URI - - Default: "DENY" - required: false - - contentTypeOptions: - type: string - description: | - X-Content-Type-Options header value to prevent MIME type sniffing. - Should typically be set to "nosniff". - Default: "nosniff" - required: false - - xssProtection: - type: string - description: | - X-XSS-Protection header value for browser XSS filtering. - Recommended value: "1; mode=block" - Default: "1; mode=block" - required: false - - referrerPolicy: - type: string - description: | - Referrer-Policy header value to control referrer information sharing. - - Common values: - - strict-origin-when-cross-origin (recommended) - - no-referrer (most restrictive) - - same-origin (moderate) - - Default: "strict-origin-when-cross-origin" - required: false - - corsEnabled: - type: boolean - description: | - Enable Cross-Origin Resource Sharing (CORS) headers. - Essential for API endpoints that need to be accessed from web browsers. - Default: false - required: false - - corsAllowedOrigins: - type: array - description: | - List of allowed origins for CORS requests. - Supports wildcards for flexible origin matching: - - - "https://example.com" (exact match) - - "https://*.example.com" (subdomain wildcard) - - "http://localhost:*" (port wildcard, useful for development) - - "*" (allow all origins - not recommended for production) - - Examples: ["https://app.example.com", "https://*.api.example.com"] - required: false - items: - type: string - - corsAllowedMethods: - type: array - description: | - HTTP methods allowed for CORS requests. - Default: ["GET", "POST", "OPTIONS"] - - Common additions: ["PUT", "DELETE", "PATCH"] - required: false - items: - type: string - - corsAllowedHeaders: - type: array - description: | - HTTP headers allowed for CORS requests. - Default: ["Authorization", "Content-Type"] - - Common additions: ["X-Requested-With", "X-API-Key"] - required: false - items: - type: string - - corsAllowCredentials: - type: boolean - description: | - Allow credentials (cookies, authorization headers) in CORS requests. - Required for authenticated API requests from browsers. - Default: false - required: false - - corsMaxAge: - type: integer - description: | - Maximum age in seconds for CORS preflight cache. - Reduces preflight request frequency for better performance. - Default: 86400 (24 hours) - required: false - - customHeaders: - type: object - description: | - Additional custom headers to include in responses. - Useful for application-specific security requirements. - - Examples: - X-Security-Level: "high" - X-API-Version: "v1" - X-Environment: "production" - required: false - - disableServerHeader: - type: boolean - description: | - Remove the Server header to hide server information. - Recommended for security through obscurity. - Default: true - required: false - - disablePoweredByHeader: - type: boolean - description: | - Remove the X-Powered-By header to hide technology stack information. - Default: true - required: false - - permissionsPolicy: - type: string - description: | - Permissions-Policy header to control browser feature permissions. - This header allows you to control which features and APIs can be used. - - Examples: - - "geolocation=(), camera=(), microphone=()" (deny all) - - "geolocation=(self), camera=()" (allow geolocation for same origin only) - - Common directives: accelerometer, camera, geolocation, gyroscope, - magnetometer, microphone, payment, usb - required: false - - crossOriginEmbedderPolicy: - type: string - description: | - Cross-Origin-Embedder-Policy (COEP) header to prevent untrusted - resources from being loaded. - - Options: - - "require-corp": Resources must explicitly grant permission - - "credentialless": Load without credentials for cross-origin resources - - "unsafe-none": No restrictions (default) - - Required for certain browser features like SharedArrayBuffer. - required: false - - crossOriginOpenerPolicy: - type: string - description: | - Cross-Origin-Opener-Policy (COOP) header to isolate browsing context - from cross-origin windows. - - Options: - - "same-origin": Isolate from cross-origin documents - - "same-origin-allow-popups": Allow popups that don't set COOP - - "unsafe-none": No isolation (default) - - Helps prevent cross-origin attacks and Spectre-like vulnerabilities. - required: false - - crossOriginResourcePolicy: - type: string - description: | - Cross-Origin-Resource-Policy (CORP) header to control which origins - can load this resource. - - Options: - - "same-origin": Only same-origin requests can load the resource - - "same-site": Only same-site requests can load the resource - - "cross-origin": Any origin can load the resource (default) - - Prevents your resources from being embedded on other sites. - required: false - - redis: - type: object - description: | - Optional Redis cache configuration for multi-replica deployments. - - When running multiple Traefik instances, Redis provides shared caching to: - - Prevent JTI replay detection false positives across replicas - - Share token verification results between instances - - Maintain consistent session state across the cluster - - Improve performance by reducing redundant OIDC provider calls - - Features: - - Automatic failover to memory-only mode when Redis is unavailable - - Circuit breaker pattern for resilience against Redis failures - - Health checking with automatic recovery - - Multiple cache modes: redis-only, hybrid (L1 memory + L2 Redis), memory-only - - Configurable timeouts and connection pooling - - TLS support for secure Redis connections - - The middleware gracefully handles Redis failures by falling back to in-memory - caching, ensuring your authentication flow continues even during Redis outages. - - Example configuration: - ```yaml - redis: - enabled: true - address: "redis:6379" - cacheMode: "hybrid" - enableCircuitBreaker: true - ``` - required: false - properties: - enabled: - type: boolean - description: | - Enable Redis caching for distributed session and token management. - When enabled, the middleware will attempt to connect to Redis and use it - for shared state across multiple Traefik instances. - - Default: false - required: false - - address: - type: string - description: | - Redis server address in host:port format. - - Examples: - - "redis:6379" (Docker/Kubernetes service) - - "localhost:6379" (local Redis) - - "redis.example.com:6380" (custom host/port) - - "redis-cluster.default.svc.cluster.local:6379" (Kubernetes) - - Required when Redis is enabled. - required: false - - password: - type: string - description: | - Password for Redis authentication. - Leave empty if Redis doesn't require authentication. - - For Kubernetes deployments, you can use secret references: - urn:k8s:secret:namespace:secret-name:key - - Default: "" (no authentication) - required: false - - db: - type: integer - description: | - Redis database number to use (0-15). - Different databases can be used to isolate data between environments. - - Default: 0 - required: false - - keyPrefix: - type: string - description: | - Prefix for all Redis keys created by this middleware. - Useful for: - - Avoiding key collisions with other applications - - Identifying keys for monitoring/debugging - - Supporting multiple environments in the same Redis instance - - Default: "traefikoidc:" - required: false - - cacheMode: - type: string - description: | - Determines the caching strategy: - - - "redis": Redis-only caching. All cache operations go directly to Redis. - Best for: Consistent state across all replicas, minimal memory usage. - - - "hybrid": Two-tier caching with in-memory L1 and Redis L2. - Best for: High performance with shared state, reduced Redis load. - L1 provides fast local cache, L2 provides shared state. - - - "memory": Memory-only caching (Redis disabled even if configured). - Best for: Single instance deployments, development/testing. - - Default: "redis" (when Redis is enabled) - required: false - enum: - - redis - - hybrid - - memory - - poolSize: - type: integer - description: | - Maximum number of socket connections to Redis. - Higher values allow more concurrent operations but consume more resources. - - Recommendations: - - Small deployments: 10-20 - - Medium deployments: 20-50 - - Large deployments: 50-100 - - Default: 10 - required: false - - connectTimeout: - type: integer - description: | - Timeout in seconds for establishing new connections to Redis. - Should be higher than network latency but low enough to fail fast. - - Default: 5 seconds - required: false - - readTimeout: - type: integer - description: | - Timeout in seconds for Redis read operations. - Includes the time to send the command, wait for Redis to process it, - and receive the response. - - Default: 3 seconds - required: false - - writeTimeout: - type: integer - description: | - Timeout in seconds for Redis write operations. - Should account for network latency and Redis persistence settings. - - Default: 3 seconds - required: false - - enableTLS: - type: boolean - description: | - Enable TLS encryption for Redis connections. - Required when connecting to Redis instances that enforce TLS, - such as AWS ElastiCache with encryption in transit. - - Default: false - required: false - - tlsSkipVerify: - type: boolean - description: | - Skip TLS certificate verification for Redis connections. - - ⚠️ WARNING: Only use in development environments. - This option bypasses certificate validation and should never be used - in production as it's vulnerable to man-in-the-middle attacks. - - Default: false - required: false - - hybridL1Size: - type: integer - description: | - Maximum number of items in the L1 (in-memory) cache for hybrid mode. - Controls how many cache entries are kept in local memory before eviction. - - Only applies when cacheMode is "hybrid". - - Default: 500 - required: false - - hybridL1MemoryMB: - type: integer - description: | - Maximum memory in megabytes for L1 cache in hybrid mode. - The cache will start evicting items when this limit is approached. - - Only applies when cacheMode is "hybrid". - - Default: 10 MB - required: false - - enableCircuitBreaker: - type: boolean - description: | - Enable circuit breaker pattern for Redis connection failures. - - When enabled, the middleware will: - 1. Track Redis operation failures - 2. Open the circuit after threshold failures (stop trying Redis) - 3. Fall back to in-memory caching - 4. Periodically attempt to reconnect (half-open state) - 5. Resume Redis operations when connection recovers - - This prevents cascading failures and improves resilience. - - Default: true - required: false - - circuitBreakerThreshold: - type: integer - description: | - Number of consecutive Redis failures before opening the circuit. - Lower values make the system more sensitive to Redis issues, - higher values tolerate more failures before switching to fallback. - - Default: 5 - required: false - - circuitBreakerTimeout: - type: integer - description: | - Time in seconds to wait before attempting to close the circuit. - After this timeout, the circuit breaker will allow one test request - to Redis. If successful, normal operations resume. - - Default: 60 seconds - required: false - - enableHealthCheck: - type: boolean - description: | - Enable periodic health checks for Redis connection. - - Health checks: - - Run in the background at regular intervals - - Detect Redis availability without affecting request processing - - Automatically reconnect when Redis becomes available - - Update circuit breaker state based on health status - - Default: true - required: false - - healthCheckInterval: - type: integer - description: | - Interval in seconds between Redis health checks. - Lower values detect issues faster but increase Redis load. - Higher values reduce overhead but delay failure detection. - - Default: 30 seconds - required: false - - dynamicClientRegistration: - type: object - description: | - Configuration for OIDC Dynamic Client Registration (RFC 7591/7592). - - Dynamic Client Registration allows the middleware to automatically register - itself as an OAuth 2.0 client with the OIDC provider, eliminating the need - to manually create and manage client credentials. - - This is particularly useful for: - - Automated deployments where manual client creation is impractical - - Multi-tenant scenarios requiring per-deployment client isolation - - Development and testing environments - - Kubernetes environments with multiple replicas - - For multi-replica deployments (Kubernetes), enable Redis storage to share - credentials across all instances and prevent registration race conditions. - - Example configuration: - ```yaml - dynamicClientRegistration: - enabled: true - persistCredentials: true - storageBackend: "redis" # Use Redis for distributed storage - clientMetadata: - redirect_uris: - - https://app.example.com/oauth2/callback - client_name: "My Application" - application_type: "web" - ``` - required: false - properties: - enabled: - type: boolean - description: | - Enable dynamic client registration with the OIDC provider. - When enabled and clientID is not set, the middleware will automatically - register itself with the provider using the configuration in clientMetadata. - - Default: false - required: false - - persistCredentials: - type: boolean - description: | - Enable persistence of client credentials after registration. - When enabled, credentials are saved to the configured storage backend - and reloaded on restart to avoid re-registration. - - Default: false - required: false - - storageBackend: - type: string - description: | - Storage backend for persisting DCR credentials. - - Options: - - "file": Store credentials in a local file (default for backward compatibility) - - "redis": Store credentials in Redis (recommended for multi-replica deployments) - - "auto": Use Redis if available, fall back to file storage - - For Kubernetes deployments with multiple replicas, use "redis" to ensure - all instances share the same client credentials and prevent registration - race conditions where each replica registers its own client. - - Default: "auto" - required: false - enum: - - file - - redis - - auto - - credentialsFile: - type: string - description: | - Path to store client credentials when using file-based storage. - The file will be created with restrictive permissions (0600). - - Default: "/tmp/oidc-client-credentials.json" - required: false - - redisKeyPrefix: - type: string - description: | - Prefix for Redis keys when using Redis storage. - Useful for isolating credentials between different applications - or environments sharing the same Redis instance. - - Default: "dcr:creds:" - required: false - - registrationEndpoint: - type: string - description: | - Override the registration endpoint URL. - If not specified, the endpoint will be discovered from provider metadata. - - Some providers may not advertise their registration endpoint in metadata, - in which case you need to specify it explicitly. - - Example: "https://auth.example.com/oauth/register" - required: false - - initialAccessToken: - type: string - description: | - Initial Access Token for protected registration endpoints. - Some providers require an access token to authorize client registration. - - If your provider requires authentication for registration, obtain an - initial access token from the provider and configure it here. - - For Kubernetes, you can use secret references: - urn:k8s:secret:namespace:secret-name:key - required: false - - clientMetadata: - type: object - description: | - Client metadata to include in the registration request (RFC 7591). - This defines the properties of the OAuth 2.0 client to be registered. - required: false - properties: - redirect_uris: - type: array - description: | - Array of redirect URIs for the client. Required for registration. - These must match the callback URLs that will be used in authentication flows. - - Example: ["https://app.example.com/oauth2/callback"] - required: true - items: - type: string - - client_name: - type: string - description: | - Human-readable name of the client. - This is typically displayed in consent screens. - - Example: "My Application" - required: false - - application_type: - type: string - description: | - Type of application. Affects security defaults. - - Options: - - "web": Server-side web application (default) - - "native": Native/mobile application - - Default: "web" - required: false - - grant_types: - type: array - description: | - OAuth 2.0 grant types the client will use. - - Default: ["authorization_code", "refresh_token"] - required: false - items: - type: string - - response_types: - type: array - description: | - OAuth 2.0 response types the client will use. - - Default: ["code"] - required: false - items: - type: string - - token_endpoint_auth_method: - type: string - description: | - Authentication method for the token endpoint. - - Options: - - "client_secret_basic": HTTP Basic authentication (default) - - "client_secret_post": Client credentials in POST body - - "none": Public client (no authentication) - - Default: "client_secret_basic" - required: false - - scope: - type: string - description: | - Space-separated list of scopes the client is authorized to request. - - Example: "openid profile email" - required: false - - contacts: - type: array - description: | - Array of contact email addresses for the client administrator. - - Example: ["admin@example.com"] - required: false - items: - type: string - - logo_uri: - type: string - description: | - URL to the client's logo image for consent screens. - required: false - - client_uri: - type: string - description: | - URL to the client's home page. - required: false - - policy_uri: - type: string - description: | - URL to the client's privacy policy. - required: false - - tos_uri: - type: string - description: | - URL to the client's terms of service. - required: false + profile: default + + # Optional: Redis for multi-replica deployments. See docs/REDIS.md. + # redis: + # enabled: true + # address: redis:6379 + # password: urn:k8s:secret:redis:password + # cacheMode: hybrid diff --git a/README.md b/README.md index f74e761..f3b5cf8 100644 --- a/README.md +++ b/README.md @@ -1,75 +1,39 @@ # Traefik OIDC Middleware -This middleware replaces the need for forward-auth and oauth2-proxy when using Traefik as a reverse proxy to support OpenID Connect (OIDC) authentication. +OpenID Connect authentication middleware for Traefik. Replaces forward-auth + +oauth2-proxy. Auto-detects all major OIDC providers, validates ID tokens, +manages sessions, and forwards user identity to downstream services. -## Overview +## Documentation -The Traefik OIDC middleware provides a complete OIDC authentication solution with these key features: +- [Configuration reference](docs/CONFIGURATION.md) — every parameter +- [Provider guide](docs/PROVIDERS.md) — Google, Azure, Auth0, Okta, Keycloak, Cognito, GitLab, GitHub, generic +- [Auth0 audience guide](docs/AUTH0_AUDIENCE_GUIDE.md) — custom APIs, opaque tokens, token confusion +- [Redis cache](docs/REDIS.md) — multi-replica deployments +- [Dynamic Client Registration](docs/DCR.md) — RFC 7591 +- [Development](docs/DEVELOPMENT.md) · [Testing](docs/TESTING.md) -- **Universal provider support**: Works with 9+ OIDC providers including Google, Azure AD, Auth0, Okta, Keycloak, AWS Cognito, GitLab, and more -- **Automatic provider detection**: Automatically detects and configures provider-specific settings -- **Dynamic Client Registration (RFC 7591)**: Automatic client registration with OIDC providers without manual pre-registration, with Redis storage support for multi-replica deployments -- **Automatic scope filtering**: Intelligently filters OAuth scopes based on provider capabilities declared in OIDC discovery documents, preventing authentication failures with unsupported scopes -- **Security headers**: Comprehensive security headers with CORS, CSP, HSTS, and custom profiles -- **Domain restrictions**: Limit access to specific email domains or individual users -- **Role-based access control**: Restrict access based on roles and groups from OIDC claims -- **Session management**: Secure session handling with automatic token refresh -- **Rate limiting**: Protection against brute force attacks -- **Excluded paths**: Configure public URLs that bypass authentication -- **Custom headers**: Template-based headers using OIDC claims and tokens -- **Comprehensive logging**: Configurable log levels for debugging and monitoring +## Provider support -## Supported OIDC Providers +| Provider | OIDC | Refresh | Auto-detected by | +|----------|------|---------|------------------| +| Google | Full | Yes | `accounts.google.com` | +| Azure AD | Full | Yes | `login.microsoftonline.com`, `sts.windows.net` | +| Auth0 | Full | Yes | `*.auth0.com` | +| Okta | Full | Yes | `*.okta.com`, `*.oktapreview.com`, `*.okta-emea.com` | +| Keycloak | Full | Yes | host containing `keycloak`, or `/realms/` in path (covers KC <17 `/auth/realms/` and 17+ `/realms/`) | +| AWS Cognito | Full | Yes | `cognito-idp.*.amazonaws.com` | +| GitLab | Full | Yes | `gitlab.com` | +| GitHub | OAuth 2.0 only — no ID token, no refresh | No | `github.com` | +| Generic | Full | Yes | any RFC-compliant `.well-known/openid-configuration` | -| Provider | Support Level | Refresh Tokens | Auto-Detection | Key Features | -|----------|---------------|----------------|---------------|--------------| -| **Google** | ✅ Full OIDC | ✅ Yes | ✅ `accounts.google.com` | Auto-config, Workspace support | -| **Azure AD** | ✅ Full OIDC | ✅ Yes | ✅ `login.microsoftonline.com` | Multi-tenant, group claims | -| **Auth0** | ✅ Full OIDC | ✅ Yes | ✅ `*.auth0.com` | Custom claims, flexible rules | -| **Okta** | ✅ Full OIDC | ✅ Yes | ✅ `*.okta.com` | Enterprise SSO, MFA support | -| **Keycloak** | ✅ Full OIDC | ✅ Yes | ✅ `/auth/realms/` path | Self-hosted, full customization | -| **AWS Cognito** | ✅ Full OIDC | ✅ Yes | ✅ `cognito-idp.*.amazonaws.com` | Managed service, regional | -| **GitLab** | ✅ Full OIDC | ✅ Yes | ✅ `gitlab.com` | Self-hosted support | -| **GitHub** | ⚠️ OAuth 2.0 Only | ❌ No | ✅ `github.com` | API access only, no claims | -| **Generic OIDC** | ✅ Full OIDC | ✅ Yes | ✅ Any endpoint | RFC-compliant providers | +> Authentication and claim extraction use the **ID token**. Ensure your +> provider includes required claims (email, roles, groups) in the ID token, +> not just the access token or UserInfo endpoint. -### Provider Capabilities Matrix +## Install -| Feature | Google | Azure AD | Auth0 | Okta | Keycloak | Cognito | GitLab | GitHub | Generic | -|---------|--------|----------|-------|------|----------|---------|--------|--------|---------| -| **ID Tokens** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | -| **Refresh Tokens** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | -| **Auto-Configuration** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| **Custom Claims** | Limited | ✅ | ✅ | ✅ | ✅ | ✅ | Limited | ❌ | Varies | -| **Group/Role Claims** | Limited | ✅ | ✅ | ✅ | ✅ | ✅ | Limited | ❌ | Varies | -| **Domain Restriction** | ✅ (hd claim) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | Varies | -| **Self-Hosted** | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ✅ | ❌ | ✅ | -| **Enterprise Features** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | Varies | - -> **Important**: GitHub uses OAuth 2.0 (not OpenID Connect) and only provides access tokens. Use it for API access only, not for user authentication with claims. All other providers support full OIDC with ID tokens and user claims. - -**Important Note on Token Validation:** This middleware performs authentication and claim extraction based on the **ID Token** provided by the OIDC provider. It does not primarily use the Access Token for these purposes (though the Access Token is available for templated headers if needed). Therefore, ensure that all necessary claims (e.g., email, roles, custom attributes) are included in the ID Token by your OIDC provider's configuration. - -The middleware has been tested with Google, Azure AD, Auth0, Okta, Keycloak, AWS Cognito, GitLab, GitHub (OAuth 2.0), and other standard OIDC providers. It includes automatic provider detection and special handling for provider-specific requirements. - -### Performance and Memory Management - -This middleware includes advanced memory management features to ensure stable operation under high load: -- **Bounded caches**: All internal caches (metadata, sessions, tokens) have configurable size limits with LRU eviction -- **Automatic cleanup**: Background goroutines periodically clean up expired sessions and tokens -- **Memory monitoring**: Built-in memory leak detection and prevention -- **Graceful degradation**: Continues operating safely even under memory pressure -- **Zero goroutine leaks**: All background tasks are properly managed and terminated on shutdown - -## Traefik Version Compatibility - -This middleware follows closely the current Traefik helm chart versions. If the plugin fails to load, it's time to update to the latest version of the Traefik helm chart. - -## Installation - -### As a Traefik Plugin - -1. Enable the plugin in your Traefik static configuration: +Enable the plugin in Traefik's static configuration: ```yaml # traefik.yml @@ -77,17 +41,21 @@ experimental: plugins: traefikoidc: moduleName: github.com/lukaszraczylo/traefikoidc - version: v0.7.10 # Use the latest version + version: v0.7.10 ``` -2. Configure the middleware in your dynamic configuration (see examples below). +Then attach the middleware in your dynamic configuration (see +[Quickstart](#quickstart) below). -### Verifying Release Signatures +This middleware tracks the current Traefik helm chart release. If it fails to +load, update Traefik first. -All release checksums are signed with [cosign](https://github.com/sigstore/cosign) using keyless signing. To verify: +### Verify release signatures + +Release checksums are signed with [cosign](https://github.com/sigstore/cosign) +keyless signing: ```bash -# Download the checksum file and its sigstore bundle from the release cosign verify-blob \ --certificate-identity-regexp "https://github.com/lukaszraczylo/traefikoidc/.*" \ --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \ @@ -95,2002 +63,207 @@ cosign verify-blob \ traefikoidc_v_checksums.txt ``` -### Local Development with Docker Compose - -For local development or testing, you can use the provided Docker Compose setup: - -```bash -cd docker -docker-compose up -d -``` - -This will start Traefik with the OIDC middleware and two test services. - -## Configuration Options - -The middleware supports the following configuration options: - -### Required Parameters - -| Parameter | Description | Example | -|-----------|-------------|---------| -| `providerURL` | The base URL of the OIDC provider | `https://accounts.google.com` | -| `clientID` | The OAuth 2.0 client identifier | `1234567890.apps.googleusercontent.com` | -| `clientSecret` | The OAuth 2.0 client secret | `your-client-secret` | -| `sessionEncryptionKey` | Key used to encrypt session data (must be at least 32 bytes long) | `potato-secret-is-at-least-32-bytes-long` | -| `callbackURL` | The path where the OIDC provider will redirect after authentication | `/oauth2/callback` | - -### Optional Parameters - -| Parameter | Description | Default | Example | -|-----------|-------------|---------|---------| -| `logoutURL` | The path for handling logout requests | `callbackURL + "/logout"` | `/oauth2/logout` | -| `postLogoutRedirectURI` | The URL to redirect to after logout | `/` | `/logged-out-page` | -| `scopes` | OAuth 2.0 scopes to use for authentication | `["openid", "profile", "email"]` (always included by default) | `["roles", "custom_scope"]` (appended to defaults) | -| `overrideScopes` | When true, replaces default scopes with provided scopes instead of appending | `false` | `true` (use only the scopes explicitly provided) | -| `logLevel` | Sets the logging verbosity | `info` | `debug`, `info`, `error` | -| `forceHTTPS` | Forces HTTPS scheme for redirect URIs (**REQUIRED** for TLS termination at load balancer like AWS ALB) | `false` (when not specified) | `true`, `false` | -| `rateLimit` | Sets the maximum number of requests per second | `100` | `500` | -| `excludedURLs` | Lists paths that bypass authentication | none | `["/health", "/metrics", "/public"]` | -| `allowedUserDomains` | Restricts access to specific email domains | none | `["company.com", "subsidiary.com"]` | -| `allowedUsers` | A list of specific email addresses that are allowed access | none | `["user1@example.com", "user2@another.org"]` | -| `allowedRolesAndGroups` | Restricts access to users with specific roles or groups | none | `["admin", "developer"]` | -| `roleClaimName` | JWT claim name for extracting user roles (supports namespaced claims for Auth0) | `"roles"` | `"https://myapp.com/roles"`, `"user_roles"` | -| `groupClaimName` | JWT claim name for extracting user groups (supports namespaced claims for Auth0) | `"groups"` | `"https://myapp.com/groups"`, `"user_groups"` | -| `userIdentifierClaim` | JWT claim to use as user identifier (for users without email, e.g., Azure AD service accounts) | `"email"` | `"sub"`, `"oid"`, `"upn"`, `"preferred_username"` | -| `revocationURL` | The endpoint for revoking tokens | auto-discovered | `https://accounts.google.com/revoke` | -| `oidcEndSessionURL` | The provider's end session endpoint | auto-discovered | `https://accounts.google.com/logout` | -| `enablePKCE` | Enables PKCE (Proof Key for Code Exchange) for authorization code flow | `false` | `true`, `false` | -| `refreshGracePeriodSeconds` | Seconds before token expiry to attempt proactive refresh | `60` | `120` | -| `cookieDomain` | Explicit domain for session cookies (important for multi-subdomain setups) | auto-detected | `.example.com`, `app.example.com` | -| `cookiePrefix` | Custom prefix for session cookie names (for isolating multiple middleware instances) | `_oidc_raczylo_` | `_oidc_userauth_`, `_oidc_admin_` | -| `sessionMaxAge` | Maximum session age in seconds before requiring re-authentication | `86400` (24 hours) | `3600` (1 hour), `604800` (7 days) | -| `audience` | Custom audience for access token validation (for Auth0 custom APIs, etc.) | `clientID` | `https://my-api.example.com` | -| `strictAudienceValidation` | Reject sessions with access token audience mismatch (prevents token confusion attacks) | `false` | `true` | -| `allowOpaqueTokens` | Enable opaque (non-JWT) access token support via RFC 7662 introspection | `false` | `true` | -| `requireTokenIntrospection` | Require introspection for opaque tokens (force validation, no fallback) | `false` | `true` | -| `headers` | Custom HTTP headers with templates that can access OIDC claims and tokens | none | See "Templated Headers" section | -| `securityHeaders` | Configure security headers including CSP, HSTS, CORS, and custom headers | enabled with default profile | See "Security Headers Configuration" section | -| `disableReplayDetection` | Disable JTI-based replay attack detection for multi-replica deployments | `false` | `true` | -| `allowPrivateIPAddresses` | Allow private IP addresses in provider URLs (for internal networks with Keycloak, etc.) | `false` | `true` | -| `minimalHeaders` | Reduce forwarded headers to prevent "431 Request Header Fields Too Large" errors | `false` | `true` | -| `stripAuthCookies` | Strip OIDC session cookies before forwarding to backend services | `false` | `true` | -| `enableBackchannelLogout` | Enable OIDC Back-Channel Logout (IdP-initiated logout via server-to-server POST) | `false` | `true` | -| `backchannelLogoutURL` | The path for receiving backchannel logout tokens from the IdP | none | `/backchannel-logout` | -| `enableFrontchannelLogout` | Enable OIDC Front-Channel Logout (IdP-initiated logout via iframe) | `false` | `true` | -| `frontchannelLogoutURL` | The path for receiving front-channel logout requests from the IdP | none | `/frontchannel-logout` | -| `redis` | Redis cache configuration for distributed deployments | disabled | See "Redis Cache" section | - -> **⚠️ IMPORTANT - TLS Termination at Load Balancer:** -> -> If you're running Traefik behind a load balancer (AWS ALB, Google Cloud Load Balancer, Azure Application Gateway, etc.) that terminates TLS: -> - **You MUST set `forceHTTPS: true`** in your configuration -> - Without this setting, redirect URIs will use `http://` instead of `https://`, causing OAuth callback failures -> - This is especially critical for AWS ALB which may overwrite the `X-Forwarded-Proto` header -> -> **Default behavior:** -> - When `forceHTTPS` is **not specified** in your config → defaults to `false` (Go zero value) -> - When `forceHTTPS: true` is explicitly set → always uses `https://` for redirect URIs -> - When `forceHTTPS: false` is explicitly set → scheme detection based on headers/TLS -> -> See [GitHub Issue #82](https://github.com/lukaszraczylo/traefikoidc/issues/82) for details. - -## Scope Configuration - -### Scope Behavior - -The middleware supports two modes for handling OAuth 2.0 scopes, controlled by the `overrideScopes` parameter: - -#### Default Append Mode (`overrideScopes: false`) - -By default, the middleware uses an **append** behavior for OAuth 2.0 scopes: - -- **Default scopes** are always included: `["openid", "profile", "email"]` -- **User-provided scopes** are appended to the defaults with automatic deduplication -- The final scope list maintains the order: defaults first, then user scopes - -#### Override Mode (`overrideScopes: true`) - -When `overrideScopes` is set to `true`, the middleware uses **replacement** behavior: - -- Default scopes are **not** automatically included -- Only the scopes explicitly provided in the `scopes` field are used -- You must include all required scopes explicitly, including `openid` if needed - -### Examples: - -**Default behavior (no custom scopes):** -```yaml -# No scopes field specified -# Result: ["openid", "profile", "email"] -``` - -**Default append behavior:** -```yaml -scopes: - - roles - - custom_scope -# Result: ["openid", "profile", "email", "roles", "custom_scope"] -``` - -**Overlapping scopes with append (automatic deduplication):** -```yaml -scopes: - - openid # Duplicate - will be deduplicated - - roles - - profile # Duplicate - will be deduplicated - - permissions -# Result: ["openid", "profile", "email", "roles", "permissions"] -``` - -**Using override mode:** -```yaml -overrideScopes: true -scopes: - - openid - - profile - - custom_scope -# Result: ["openid", "profile", "custom_scope"] -``` - -**Empty scopes list with default behavior:** -```yaml -scopes: [] -# Result: ["openid", "profile", "email"] -``` - -**Empty scopes list with override mode:** -```yaml -overrideScopes: true -scopes: [] -# Result: [] (Warning: empty scopes may cause authentication to fail) -``` - -The default append behavior ensures essential OIDC scopes are always present, while the override mode gives you complete control over the exact scopes requested from the provider. - -## Auth0 Audience Validation & Security - -The middleware provides comprehensive support for Auth0 audience validation to prevent token confusion attacks. Auth0 can issue tokens in three different scenarios, each requiring specific configuration. - -### Understanding Token Audiences - -Per OAuth 2.0 and OIDC specifications: -- **ID Tokens**: MUST have `aud = client_id` (OIDC Core 1.0 spec) -- **Access Tokens**: Can have custom audiences (e.g., API identifiers) - -Proper audience validation prevents **token confusion attacks** where a token intended for one API is used to access another API. - -### Auth0 Scenarios - -#### Scenario 1: Custom API Audience ✅ (RECOMMENDED) - -**Configuration:** -```yaml -audience: "https://my-api.example.com" # Your API identifier from Auth0 -strictAudienceValidation: true # Enforce strict validation -``` - -**Result**: Fully secure, OIDC compliant with proper access token audience validation. - -#### Scenario 2: Default Audience ⚠️ (USE WITH CAUTION) - -**Configuration:** -```yaml -# audience not specified (defaults to client_id) -strictAudienceValidation: true # Recommended: reject mismatched tokens -``` - -**Behavior**: Access tokens may not contain client_id in audience, triggering security warnings. Set `strictAudienceValidation: true` to reject such sessions. - -#### Scenario 3: Opaque Access Tokens ✅ (SUPPORTED) - -**Configuration:** -```yaml -allowOpaqueTokens: true # Enable opaque token support -requireTokenIntrospection: true # Require introspection (recommended) -``` - -**Result**: Secure with OAuth 2.0 Token Introspection (RFC 7662). - -### Security Configuration Options - -| Option | Purpose | Recommended Value | -|--------|---------|-------------------| -| `audience` | Expected audience for access tokens | Your API identifier or leave empty | -| `strictAudienceValidation` | Reject sessions with audience mismatch | `true` for production | -| `allowOpaqueTokens` | Accept non-JWT access tokens | `true` if provider issues opaque tokens | -| `requireTokenIntrospection` | Force introspection for opaque tokens | `true` when `allowOpaqueTokens=true` | - -### Complete Auth0 Configuration Examples - -**Production Configuration (Scenario 1):** -```yaml -apiVersion: traefik.io/v1alpha1 -kind: Middleware -metadata: - name: oidc-auth0-secure -spec: - plugin: - traefikoidc: - providerURL: https://your-auth0-domain.auth0.com - clientID: your-auth0-client-id - clientSecret: your-auth0-client-secret - sessionEncryptionKey: your-secure-encryption-key-min-32-chars - callbackURL: /oauth2/callback - audience: "https://my-api.example.com" - strictAudienceValidation: true - allowedRolesAndGroups: - - "https://your-app.com/roles:admin" - - editor -``` - -**Opaque Token Configuration (Scenario 3):** -```yaml -apiVersion: traefik.io/v1alpha1 -kind: Middleware -metadata: - name: oidc-auth0-opaque -spec: - plugin: - traefikoidc: - providerURL: https://your-auth0-domain.auth0.com - clientID: your-auth0-client-id - clientSecret: your-auth0-client-secret - sessionEncryptionKey: your-secure-encryption-key-min-32-chars - callbackURL: /oauth2/callback - allowOpaqueTokens: true - requireTokenIntrospection: true - strictAudienceValidation: true -``` - -For detailed Auth0 configuration including all three scenarios, troubleshooting, and security best practices, see **[AUTH0_AUDIENCE_GUIDE.md](docs/AUTH0_AUDIENCE_GUIDE.md)**. - -## Security Headers Configuration - -The middleware includes comprehensive security headers support to protect your applications against common web vulnerabilities. Security headers are applied to all authenticated responses. - -### Security Features - -- **Content Security Policy (CSP)** - Prevents XSS and code injection -- **HTTP Strict Transport Security (HSTS)** - Forces HTTPS connections -- **Frame Options** - Protects against clickjacking attacks -- **XSS Protection** - Browser-level XSS filtering -- **Content Type Options** - Prevents MIME type sniffing -- **Referrer Policy** - Controls referrer information sharing -- **CORS Headers** - Complete Cross-Origin Resource Sharing support -- **Custom Headers** - Add any additional security headers - -### Security Profiles - -Choose from predefined security profiles or create custom configurations: - -| Profile | Use Case | Security Level | CORS Enabled | -|---------|----------|----------------|--------------| -| `default` | Standard web applications | High | Disabled | -| `strict` | Maximum security applications | Very High | Disabled | -| `development` | Local development | Medium | Enabled (localhost) | -| `api` | API endpoints | High | Configurable | -| `custom` | Custom requirements | Configurable | Configurable | - -### Configuration Examples - -#### Default Security (Recommended) -```yaml -securityHeaders: - enabled: true - profile: "default" -``` - -#### Strict Security -```yaml -securityHeaders: - enabled: true - profile: "strict" -``` - -#### API with CORS -```yaml -securityHeaders: - enabled: true - profile: "api" - corsEnabled: true - corsAllowedOrigins: - - "https://your-frontend.com" - - "https://*.example.com" - corsAllowCredentials: true -``` - -#### Custom Configuration -```yaml -securityHeaders: - enabled: true - profile: "custom" - - # Content Security Policy - contentSecurityPolicy: "default-src 'self'; script-src 'self' 'unsafe-inline'" - - # HSTS Settings - strictTransportSecurity: true - strictTransportSecurityMaxAge: 31536000 # 1 year - strictTransportSecuritySubdomains: true - strictTransportSecurityPreload: true - - # Frame and Content Protection - frameOptions: "DENY" - contentTypeOptions: "nosniff" - xssProtection: "1; mode=block" - referrerPolicy: "strict-origin-when-cross-origin" - - # CORS Configuration - corsEnabled: true - corsAllowedOrigins: ["https://app.example.com"] - corsAllowedMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"] - corsAllowedHeaders: ["Authorization", "Content-Type", "X-Requested-With"] - corsAllowCredentials: true - corsMaxAge: 86400 - - # Custom Headers - customHeaders: - X-Custom-Header: "custom-value" - X-API-Version: "v1" - - # Server Identification - disableServerHeader: true - disablePoweredByHeader: true -``` - -### Security Headers Parameters - -| Parameter | Description | Default | Example | -|-----------|-------------|---------|---------| -| `enabled` | Enable/disable security headers | `true` | `true`, `false` | -| `profile` | Security profile to use | `default` | `default`, `strict`, `development`, `api`, `custom` | -| `contentSecurityPolicy` | CSP header value | Profile-based | `"default-src 'self'"` | -| `strictTransportSecurity` | Enable HSTS | `true` | `true`, `false` | -| `strictTransportSecurityMaxAge` | HSTS max age in seconds | `31536000` | `86400` | -| `strictTransportSecuritySubdomains` | Include subdomains in HSTS | `true` | `true`, `false` | -| `strictTransportSecurityPreload` | Enable HSTS preload | `true` | `true`, `false` | -| `frameOptions` | X-Frame-Options header | `DENY` | `DENY`, `SAMEORIGIN`, `ALLOW-FROM uri` | -| `contentTypeOptions` | X-Content-Type-Options header | `nosniff` | `nosniff` | -| `xssProtection` | X-XSS-Protection header | `1; mode=block` | `1; mode=block` | -| `referrerPolicy` | Referrer-Policy header | `strict-origin-when-cross-origin` | `no-referrer` | -| `corsEnabled` | Enable CORS headers | `false` | `true`, `false` | -| `corsAllowedOrigins` | Allowed CORS origins | `[]` | `["https://app.com", "https://*.example.com"]` | -| `corsAllowedMethods` | Allowed CORS methods | `["GET", "POST", "OPTIONS"]` | `["GET", "POST", "PUT", "DELETE"]` | -| `corsAllowedHeaders` | Allowed CORS headers | `["Authorization", "Content-Type"]` | `["X-Custom-Header"]` | -| `corsAllowCredentials` | Allow credentials in CORS | `false` | `true`, `false` | -| `corsMaxAge` | CORS preflight cache time | `86400` | `3600` | -| `customHeaders` | Additional custom headers | `{}` | `{"X-Custom": "value"}` | -| `disableServerHeader` | Remove Server header | `true` | `true`, `false` | -| `disablePoweredByHeader` | Remove X-Powered-By header | `true` | `true`, `false` | -| `permissionsPolicy` | Permissions-Policy header | `` | `"geolocation=(), camera=(), microphone=()"` | -| `crossOriginEmbedderPolicy` | Cross-Origin-Embedder-Policy header | `` | `"require-corp"`, `"credentialless"`, `"unsafe-none"` | -| `crossOriginOpenerPolicy` | Cross-Origin-Opener-Policy header | `` | `"same-origin"`, `"same-origin-allow-popups"`, `"unsafe-none"` | -| `crossOriginResourcePolicy` | Cross-Origin-Resource-Policy header | `` | `"same-origin"`, `"same-site"`, `"cross-origin"` | - -### CORS Wildcard Support - -The middleware supports flexible CORS origin patterns: - -```yaml -corsAllowedOrigins: - - "https://example.com" # Exact match - - "https://*.example.com" # Subdomain wildcard - - "http://localhost:*" # Port wildcard (development) - - "*" # Allow all (not recommended) -``` - -## Advanced Configuration - -The middleware provides several advanced configuration options for production environments. - -### Provider-Specific Optimizations - -The middleware automatically optimizes for each OIDC provider: -- **Google**: Automatically configures `access_type=offline` and `prompt=consent` for refresh tokens -- **Azure AD**: Optimized multi-tenant support and group claim handling -- **Auth0**: Enhanced custom claim processing and namespace support -- **Keycloak**: Self-hosted deployment optimizations -- **AWS Cognito**: Regional endpoint handling and user pool integration - -### Token Management - -- **Automatic token refresh**: Proactively refreshes tokens before expiration -- **Token validation**: Comprehensive JWT validation with security checks -- **Grace period**: Configurable time window for token refresh -- **Session handling**: Secure session management with encrypted storage - -### Configuration Examples - -#### High-Throughput Configuration -```yaml -# Optimized for high-traffic environments -rateLimit: 1000 -refreshGracePeriodSeconds: 300 -securityHeaders: - enabled: true - profile: "api" - corsEnabled: true - corsMaxAge: 86400 -``` - -#### High-Security Configuration -```yaml -# Maximum security for sensitive environments -rateLimit: 50 -allowedUserDomains: ["company.com"] -allowedRolesAndGroups: ["admin", "developer"] -securityHeaders: - enabled: true - profile: "strict" - corsEnabled: false -``` - -#### Development Configuration -```yaml -# Development-friendly settings -logLevel: "debug" -forceHTTPS: false -securityHeaders: - enabled: true - profile: "development" - corsEnabled: true - corsAllowedOrigins: ["http://localhost:*"] -``` - -### Multi-Replica Deployment Configuration - -When running multiple Traefik replicas with the OIDC plugin, you may encounter false positive replay detection errors. Each replica maintains its own in-memory JTI (JWT Token ID) cache, causing legitimate token reuse to be flagged as replay attacks. - -**Problem**: When the same valid token hits different replicas: -- Request → Replica A → JTI added to Replica A's cache ✓ -- Request → Replica B → JTI NOT in Replica B's cache ✓ -- Request → Replica A → ❌ **FALSE POSITIVE**: "token replay detected" - -**Solution 1 (Simple)**: Disable replay detection for distributed deployments: - -```yaml -disableReplayDetection: true # Disable JTI replay detection for multi-replica setups -``` - -**Solution 2 (Recommended)**: Use Redis cache backend for shared state (see [Redis Cache](#redis-cache-optional) section) - -**Security Note**: When `disableReplayDetection: true`: -- ✅ Token signatures still validated -- ✅ Expiration still checked -- ✅ All other claims still verified -- ❌ JTI replay check **skipped** - -**Example Configuration**: -```yaml -apiVersion: traefik.io/v1alpha1 -kind: Middleware -metadata: - name: oidc-multi-replica - namespace: traefik -spec: - plugin: - traefikoidc: - providerURL: https://accounts.google.com - clientID: your-client-id - clientSecret: your-client-secret - sessionEncryptionKey: your-secure-encryption-key-min-32-chars - callbackURL: /oauth2/callback - disableReplayDetection: true # Required for multi-replica deployments without Redis -``` - -**Recommendation**: For single-instance deployments, leave this setting at `false` (default) to maintain replay attack protection. For multi-replica deployments, use the Redis cache backend for proper replay detection across all instances. - -## Redis Cache (Optional) - -The plugin supports optional Redis caching for multi-replica deployments. This solves issues with JTI replay detection and session management when running multiple Traefik instances behind a load balancer. - -> **✨ Yaegi Compatible**: Redis support is implemented using a pure-Go RESP protocol client that works seamlessly with Traefik's Yaegi interpreter (no `unsafe` package). Full Redis functionality is available for both dynamic plugin loading and pre-compiled deployments. - -### Why Use Redis Cache? - -When running multiple Traefik replicas, each instance maintains its own in-memory cache for: -- JTI (JWT Token ID) replay detection -- Session data -- Token metadata - -Without a shared cache, you may experience: -- False positive replay detection errors -- Session inconsistencies between replicas -- Users needing to re-authenticate when hitting different instances - -### Basic Configuration - -Redis is configured through Traefik's dynamic configuration (YAML, labels, etc.): - -```yaml -# Enable Redis cache in your middleware configuration -redis: - enabled: true - address: "localhost:6379" - password: "your-password" # Optional - db: 0 - keyPrefix: "traefikoidc:" -``` - -### Configuration Priority - -The plugin uses the following priority for Redis configuration: - -1. **Traefik Dynamic Configuration** (PRIMARY) - Configure via YAML files or Docker/Kubernetes labels -2. **Environment Variables** (FALLBACK) - Used only when not set in Traefik config - -This approach allows you to manage all settings through Traefik's configuration system while maintaining backward compatibility with environment variables. - -### Configuration Options - -| Parameter | Description | Default | Example | -|-----------|-------------|---------|---------| -| `enabled` | Enable Redis caching | `false` | `true` | -| `address` | Redis server address | - | `redis:6379` | -| `password` | Redis password | - | `YOUR_PASSWORD` | -| `db` | Database number | `0` | `1` | -| `keyPrefix` | Key prefix for namespacing | `traefikoidc:` | `myapp:` | -| `cacheMode` | Cache mode: `redis`, `hybrid`, `memory` | `redis` | `hybrid` | -| `poolSize` | Connection pool size | `10` | `20` | -| `connectTimeout` | Connection timeout (seconds) | `5` | `10` | -| `readTimeout` | Read timeout (seconds) | `3` | `5` | -| `writeTimeout` | Write timeout (seconds) | `3` | `5` | -| `enableTLS` | Enable TLS | `false` | `true` | -| `tlsSkipVerify` | Skip TLS verification | `false` | `true` | -| `enableCircuitBreaker` | Circuit breaker for failures | `true` | `true` | -| `circuitBreakerThreshold` | Failures before circuit opens | `5` | `10` | -| `circuitBreakerTimeout` | Circuit reset timeout (seconds) | `60` | `30` | -| `enableHealthCheck` | Periodic health checks | `true` | `true` | -| `healthCheckInterval` | Health check interval (seconds) | `30` | `60` | - -### Environment Variables (Fallback) - -If not configured through Traefik, these environment variables can be used as fallback: - -- `REDIS_ENABLED` - Enable Redis cache -- `REDIS_ADDRESS` - Redis server address -- `REDIS_PASSWORD` - Redis password -- `REDIS_DB` - Database number -- `REDIS_KEY_PREFIX` - Key prefix -- `REDIS_CACHE_MODE` - Cache mode -- `REDIS_POOL_SIZE` - Connection pool size -- `REDIS_CONNECT_TIMEOUT` - Connection timeout -- `REDIS_READ_TIMEOUT` - Read timeout -- `REDIS_WRITE_TIMEOUT` - Write timeout -- `REDIS_ENABLE_TLS` - Enable TLS -- `REDIS_TLS_SKIP_VERIFY` - Skip TLS verification - -### Cache Modes - -The plugin supports three cache modes: - -- **memory** (default): In-memory cache only, suitable for single-instance deployments -- **redis**: Redis-only cache, all data stored in Redis -- **hybrid**: Two-tier caching with local memory cache + Redis backend for optimal performance - -### Example Configurations - -#### Docker Compose with Redis - -```yaml -services: - redis: - image: redis:alpine - command: redis-server --requirepass yourpassword - - traefik: - image: traefik:v3.2 - # ... rest of your Traefik configuration - labels: - # Configure the OIDC middleware with Redis - - "traefik.http.middlewares.oidc.plugin.traefikoidc.clientID=your-client-id" - - "traefik.http.middlewares.oidc.plugin.traefikoidc.clientSecret=your-secret" - - "traefik.http.middlewares.oidc.plugin.traefikoidc.providerURL=https://auth.example.com" - - "traefik.http.middlewares.oidc.plugin.traefikoidc.callbackURL=/oauth2/callback" - - "traefik.http.middlewares.oidc.plugin.traefikoidc.sessionEncryptionKey=your-64-char-key" - # Redis configuration via labels - - "traefik.http.middlewares.oidc.plugin.traefikoidc.redis.enabled=true" - - "traefik.http.middlewares.oidc.plugin.traefikoidc.redis.address=redis:6379" - - "traefik.http.middlewares.oidc.plugin.traefikoidc.redis.password=yourpassword" - - "traefik.http.middlewares.oidc.plugin.traefikoidc.redis.cacheMode=hybrid" -``` - -#### Kubernetes with Redis +## Quickstart ```yaml apiVersion: traefik.io/v1alpha1 kind: Middleware metadata: - name: oidc-with-redis -spec: - plugin: - traefikoidc: - providerURL: https://accounts.google.com - clientID: your-client-id - clientSecret: your-client-secret - sessionEncryptionKey: your-encryption-key - callbackURL: /oauth2/callback - redis: - enabled: true - address: "redis-service.redis-namespace:6379" - password: "urn:k8s:secret:redis-secret:password" - db: 0 - keyPrefix: "traefikoidc" - cacheMode: "hybrid" -``` - -### Advanced Redis Configuration - -See [Redis Cache Documentation](docs/REDIS_CACHE.md) for: -- Detailed architecture overview -- High availability setup with Redis Sentinel -- Redis Cluster configuration -- Performance tuning guidelines -- Monitoring and observability -- Troubleshooting guide -- Migration from memory-only cache - -## Dynamic Client Registration (RFC 7591) - -The middleware supports **OIDC Dynamic Client Registration** (RFC 7591), allowing automatic client registration with OIDC providers without manual pre-registration. This is useful for: - -- **Multi-tenant deployments**: Automatically register clients per tenant -- **Development environments**: Quick setup without manual OAuth app creation -- **Self-service integrations**: Allow applications to self-register - -### How It Works - -1. When enabled, the middleware discovers the `registration_endpoint` from the provider's `.well-known/openid-configuration` -2. If no `clientID` is configured, it automatically registers a new client with the provider -3. The registered `client_id` and `client_secret` are cached and optionally persisted to a file -4. Subsequent requests use the registered credentials - -### Configuration - -```yaml -apiVersion: traefik.io/v1alpha1 -kind: Middleware -metadata: - name: oidc-dynamic-registration - namespace: traefik -spec: - plugin: - traefikoidc: - providerURL: https://your-oidc-provider.com - # clientID and clientSecret are NOT required when using DCR - sessionEncryptionKey: your-secure-encryption-key-min-32-chars - callbackURL: /oauth2/callback - - dynamicClientRegistration: - enabled: true - - # Optional: Initial access token for protected registration endpoints - initialAccessToken: "your-initial-access-token" - - # Optional: Override the registration endpoint (auto-discovered by default) - registrationEndpoint: "https://your-provider.com/register" - - # Optional: Persist credentials to file for reuse across restarts - persistCredentials: true - credentialsFile: "/tmp/oidc-client-credentials.json" - - # Client metadata for registration - clientMetadata: - redirect_uris: - - "https://your-app.com/oauth2/callback" - client_name: "My Application" - application_type: "web" - grant_types: - - "authorization_code" - - "refresh_token" - response_types: - - "code" - token_endpoint_auth_method: "client_secret_basic" - contacts: - - "admin@your-app.com" -``` - -### DCR Configuration Parameters - -| Parameter | Description | Required | Default | -|-----------|-------------|----------|---------| -| `enabled` | Enable dynamic client registration | Yes | `false` | -| `initialAccessToken` | Bearer token for protected registration endpoints | No | - | -| `registrationEndpoint` | Override auto-discovered registration endpoint | No | From discovery | -| `persistCredentials` | Save registered credentials to file | No | `false` | -| `credentialsFile` | Path to store/load credentials | No | `/tmp/oidc-client-credentials.json` | -| `clientMetadata.redirect_uris` | **REQUIRED** - Redirect URIs for OAuth flow | Yes | - | -| `clientMetadata.client_name` | Human-readable client name | No | - | -| `clientMetadata.application_type` | `web` or `native` | No | `web` | -| `clientMetadata.grant_types` | OAuth grant types | No | `["authorization_code", "refresh_token"]` | -| `clientMetadata.response_types` | OAuth response types | No | `["code"]` | -| `clientMetadata.token_endpoint_auth_method` | Authentication method | No | `client_secret_basic` | -| `clientMetadata.contacts` | Contact email addresses | No | - | -| `clientMetadata.logo_uri` | URL to client logo | No | - | -| `clientMetadata.client_uri` | URL to client homepage | No | - | -| `clientMetadata.policy_uri` | URL to privacy policy | No | - | -| `clientMetadata.tos_uri` | URL to terms of service | No | - | -| `clientMetadata.scope` | Space-separated scopes | No | - | - -### Provider Support - -DCR support varies by provider: - -| Provider | DCR Support | Notes | -|----------|-------------|-------| -| Keycloak | ✅ Full | Enable in realm settings | -| Auth0 | ✅ Full | Requires Management API token | -| Okta | ✅ Full | Enable Dynamic Client Registration | -| Azure AD | ⚠️ Limited | App Registration API instead | -| Google | ❌ No | Manual registration required | -| AWS Cognito | ❌ No | Manual registration required | - -### Security Considerations - -1. **HTTPS Required**: Registration endpoints must use HTTPS (except localhost for development) -2. **Initial Access Token**: Recommended for production to prevent unauthorized registrations -3. **Credential Persistence**: If enabled, ensure the credentials file has appropriate permissions (0600) -4. **Secret Expiration**: Monitor `client_secret_expires_at` and handle rotation if needed - -### Example: Keycloak with DCR - -```yaml -dynamicClientRegistration: - enabled: true - clientMetadata: - redirect_uris: - - "https://myapp.example.com/oauth2/callback" - client_name: "My App - Production" - application_type: "web" - grant_types: - - "authorization_code" - - "refresh_token" -``` - -## Usage Examples - -### Basic Configuration - -```yaml -apiVersion: traefik.io/v1alpha1 -kind: Middleware -metadata: - name: oidc-basic + name: oidc-auth namespace: traefik spec: plugin: traefikoidc: providerURL: https://accounts.google.com clientID: 1234567890.apps.googleusercontent.com - clientSecret: your-client-secret - sessionEncryptionKey: potato-secret-is-at-least-32-bytes-long + clientSecret: urn:k8s:secret:traefik-oidc:CLIENT_SECRET + sessionEncryptionKey: urn:k8s:secret:traefik-oidc:SESSION_KEY callbackURL: /oauth2/callback logoutURL: /oauth2/logout - scopes: - - roles # Appended to defaults: ["openid", "profile", "email", "roles"] + postLogoutRedirectURI: / + # forceHTTPS defaults to true (secure-by-default). Only set false if you + # serve OIDC over plaintext HTTP for local dev. + allowedUserDomains: [company.com] + allowedRolesAndGroups: [admin, developer] + excludedURLs: [/health, /metrics] ``` -### With Excluded URLs (Public Access Paths) +More example configs in [`examples/`](examples/). + +## Required parameters + +| Parameter | Description | +|-----------|-------------| +| `providerURL` | Issuer URL (used for OIDC discovery). | +| `clientID` | OAuth 2.0 client ID. | +| `clientSecret` | OAuth 2.0 client secret. Supports `urn:k8s:secret:ns:name:key`. | +| `sessionEncryptionKey` | Cookie encryption key, **min 32 bytes**. | +| `callbackURL` | Callback path, e.g. `/oauth2/callback`. | + +## Common optional parameters + +Full reference in [docs/CONFIGURATION.md](docs/CONFIGURATION.md). + +| Parameter | Default | Purpose | +|-----------|---------|---------| +| `forceHTTPS` | `true` | Forces `https://` in redirect URIs. Leave at default behind any TLS-terminating LB (AWS ALB, GCP LB, Azure App Gateway). Set `false` only for plaintext HTTP local dev. | +| `logoutURL` | `callbackURL + "/logout"` | RP-initiated logout path. | +| `postLogoutRedirectURI` | `/` | Where to send users after logout. | +| `scopes` | appended to `openid profile email` | Extra OAuth scopes. Set `overrideScopes: true` to replace defaults. | +| `excludedURLs` | none | Prefix-matched paths that bypass auth. | +| `allowedUserDomains` | none | Restrict to email domains. | +| `allowedUsers` | none | Restrict to specific addresses (or claim values when `userIdentifierClaim != email`). | +| `allowedRolesAndGroups` | none | Require any of these roles/groups from ID-token claims. | +| `roleClaimName` / `groupClaimName` | `roles` / `groups` | For namespaced claims (Auth0). | +| `userIdentifierClaim` | `email` | Use `sub`, `oid`, `upn`, or `preferred_username` for users without email. | +| `enablePKCE` | `false` | PKCE on the auth code flow. | +| `cookieDomain` | auto | Set explicitly for multi-subdomain setups (`.example.com`). | +| `cookiePrefix` | `_oidc_raczylo_` | Unique prefix per middleware instance to isolate sessions. | +| `sessionMaxAge` | `86400` | Session lifetime in seconds. | +| `refreshGracePeriodSeconds` | `60` | Proactively refresh tokens this many seconds before expiry. | +| `rateLimit` | `100` | Requests/sec. Min `10`. | +| `logLevel` | `info` | `debug`, `info`, `error`. | +| `audience` | `clientID` | Custom access-token audience (Auth0 custom APIs). | +| `strictAudienceValidation` | `false` | Reject mismatched audiences. **Set `true` in production.** | +| `allowOpaqueTokens` / `requireTokenIntrospection` | `false` | Accept opaque access tokens via RFC 7662. | +| `disableReplayDetection` | `false` | Disable JTI cache. Use Redis instead for multi-replica. | +| `allowPrivateIPAddresses` | `false` | Permit private-IP `providerURL` (internal Keycloak, etc.). | +| `minimalHeaders` | `false` | Reduce forwarded headers (mitigates HTTP 431). | +| `stripAuthCookies` | `false` | Strip OIDC cookies from backend hop (mitigates HTTP 431). | +| `caCertPath` / `caCertPEM` | none | Trust an internal CA for the provider's TLS. | +| `insecureSkipVerify` | `false` | **Local dev only.** Disables TLS verification, logs a security warning. | +| `enableBackchannelLogout` / `backchannelLogoutURL` | `false` / none | OIDC Back-Channel Logout (server-to-server). | +| `enableFrontchannelLogout` / `frontchannelLogoutURL` | `false` / none | OIDC Front-Channel Logout (iframe). | +| `redis` | disabled | See [docs/REDIS.md](docs/REDIS.md). | +| `dynamicClientRegistration` | disabled | See [docs/DCR.md](docs/DCR.md). | + +## Production gotchas + +### TLS termination at a load balancer + +`forceHTTPS` defaults to `true`, so redirect URIs always use `https://`. This is +the right default behind AWS ALB, GCP LB, Azure App Gateway, or any LB that +terminates TLS — `X-Forwarded-Proto` is unreliable (ALB may overwrite it). + +Only set `forceHTTPS: false` when you actually serve OIDC over plaintext HTTP +(local dev). See [issue #82](https://github.com/lukaszraczylo/traefikoidc/issues/82). + +### Multi-replica deployments + +Each replica keeps its own in-memory JTI cache → false positive "token replay +detected" when the same token hits different replicas. Two options: + +1. Set `disableReplayDetection: true` (loses replay protection). +2. Enable Redis for shared state (recommended) — see [docs/REDIS.md](docs/REDIS.md). + +For IdP-initiated logout (back/front-channel) in multi-replica setups, Redis is +**required** so a logout on one instance invalidates sessions on the others. + +### Multiple middleware instances on the same host + +Each instance must use a unique `cookiePrefix` **and** `sessionEncryptionKey`, +otherwise a session minted by one instance can grant access through another. +See [issue #87](https://github.com/lukaszraczylo/traefikoidc/issues/87). + +### HTTP 431 from backends + +Either the ID token or the chunked OIDC cookies overflow your backend's header +buffer. Combine these as needed: ```yaml -apiVersion: traefik.io/v1alpha1 -kind: Middleware -metadata: - name: oidc-with-open-urls - namespace: traefik -spec: - plugin: - traefikoidc: - providerURL: https://accounts.google.com - clientID: 1234567890.apps.googleusercontent.com - clientSecret: your-client-secret - sessionEncryptionKey: potato-secret-is-at-least-32-bytes-long - callbackURL: /oauth2/callback - logoutURL: /oauth2/logout - scopes: - - roles # Appended to defaults: ["openid", "profile", "email", "roles"] - excludedURLs: - - /login # covers /login, /login/me, /login/reminder etc. - - /public-data - - /health - - /metrics +minimalHeaders: true # drop X-Auth-Request-Token et al. +stripAuthCookies: true # strip _oidc_raczylo_* cookies on the backend hop ``` -### With Email Domain Restrictions +Cookies remain in the browser; only the Traefik→backend hop is affected. See +[#64](https://github.com/lukaszraczylo/traefikoidc/issues/64), +[#122](https://github.com/lukaszraczylo/traefikoidc/issues/122). + +### Internal CA for the provider + +If the provider's TLS cert is signed by a private CA (self-hosted GitLab, +internal Keycloak, ADFS): ```yaml -apiVersion: traefik.io/v1alpha1 -kind: Middleware -metadata: - name: oidc-domain-restricted - namespace: traefik -spec: - plugin: - traefikoidc: - providerURL: https://accounts.google.com - clientID: 1234567890.apps.googleusercontent.com - clientSecret: your-client-secret - sessionEncryptionKey: potato-secret-is-at-least-32-bytes-long - callbackURL: /oauth2/callback - logoutURL: /oauth2/logout - scopes: - - roles # Appended to defaults: ["openid", "profile", "email", "roles"] - allowedUserDomains: - - company.com - - subsidiary.com +caCertPath: /etc/ssl/certs/internal-ca.pem +# or, inline: +caCertPEM: | + -----BEGIN CERTIFICATE----- + ... + -----END CERTIFICATE----- ``` -### With Specific User Access +Both can be combined. An unparseable bundle fails the plugin at startup. +See [#125](https://github.com/lukaszraczylo/traefikoidc/issues/125). -```yaml -apiVersion: traefik.io/v1alpha1 -kind: Middleware -metadata: - name: oidc-specific-users - namespace: traefik -spec: - plugin: - traefikoidc: - providerURL: https://accounts.google.com - clientID: 1234567890.apps.googleusercontent.com - clientSecret: your-client-secret - sessionEncryptionKey: potato-secret-is-at-least-32-bytes-long - callbackURL: /oauth2/callback - logoutURL: /oauth2/logout - scopes: - - roles # Appended to defaults: ["openid", "profile", "email", "roles"] - allowedUsers: - - user1@example.com - - user2@another.org -``` +### Environment variable names containing `API` -### With Both Domain and Specific User Access +Traefik reserves `TRAEFIK_API_*`. User vars whose name contains `API` (e.g. +`OIDC_ENCRYPTION_SECRET_API`) make the plugin fail with +`invalid handler type: `. Rename to anything without the literal `API` +substring. See [#98](https://github.com/lukaszraczylo/traefikoidc/issues/98). -```yaml -apiVersion: traefik.io/v1alpha1 -kind: Middleware -metadata: - name: oidc-domain-and-users - namespace: traefik -spec: - plugin: - traefikoidc: - providerURL: https://accounts.google.com - clientID: 1234567890.apps.googleusercontent.com - clientSecret: your-client-secret - sessionEncryptionKey: potato-secret-is-at-least-32-bytes-long - callbackURL: /oauth2/callback - logoutURL: /oauth2/logout - scopes: - - roles # Appended to defaults: ["openid", "profile", "email", "roles"] - allowedUserDomains: - - company.com - allowedUsers: - - special-user@gmail.com - - contractor@external.org -``` +## Templated headers -When configuring access control: -- If only `allowedUsers` is set, only the specified email addresses will be granted access -- If only `allowedUserDomains` is set, only users with email addresses from those domains will be granted access -- If both are set, access is granted if the user's email is in `allowedUsers` OR their email's domain is in `allowedUserDomains` -- If neither is set, any authenticated user will be granted access -- Email matching is case-insensitive - -### With Role-Based Access Control - -```yaml -apiVersion: traefik.io/v1alpha1 -kind: Middleware -metadata: - name: oidc-rbac - namespace: traefik -spec: - plugin: - traefikoidc: - providerURL: https://accounts.google.com - clientID: 1234567890.apps.googleusercontent.com - clientSecret: your-client-secret - sessionEncryptionKey: potato-secret-is-at-least-32-bytes-long - callbackURL: /oauth2/callback - logoutURL: /oauth2/logout - scopes: - - roles # Appended to defaults: ["openid", "profile", "email", "roles"] - allowedRolesAndGroups: - - admin - - developer -``` - -### With Cookie Domain Configuration (Multi-Subdomain Setup) - -```yaml -apiVersion: traefik.io/v1alpha1 -kind: Middleware -metadata: - name: oidc-multi-subdomain - namespace: traefik -spec: - plugin: - traefikoidc: - providerURL: https://accounts.google.com - clientID: 1234567890.apps.googleusercontent.com - clientSecret: your-client-secret - sessionEncryptionKey: potato-secret-is-at-least-32-bytes-long - callbackURL: /oauth2/callback - logoutURL: /oauth2/logout - cookieDomain: .example.com # Allows cookies to be shared across all subdomains - scopes: - - roles # Appended to defaults: ["openid", "profile", "email", "roles"] -``` - -**Important**: The `cookieDomain` parameter is crucial when running behind a reverse proxy or when your application serves multiple subdomains. Without it, cookies may be created with inconsistent domains, leading to authentication issues like "CSRF token missing in session" errors. - -### With Multiple Middleware Instances (Session Isolation) - -When running multiple middleware instances with different authorization requirements (e.g., one for general users and one for admins), you must use different `cookiePrefix` values to prevent session sharing between instances: - -```yaml -# Middleware for general user authentication -apiVersion: traefik.io/v1alpha1 -kind: Middleware -metadata: - name: oidc-userauth - namespace: traefik -spec: - plugin: - traefikoidc: - providerURL: https://auth.example.com - clientID: your-client-id - clientSecret: your-client-secret - sessionEncryptionKey: user-key-at-least-32-bytes-long - callbackURL: /oauth2/callback - cookiePrefix: "_oidc_userauth_" # Unique prefix for this instance ---- -# Middleware for admin authentication with stricter requirements -apiVersion: traefik.io/v1alpha1 -kind: Middleware -metadata: - name: oidc-adminauth - namespace: traefik -spec: - plugin: - traefikoidc: - providerURL: https://auth.example.com - clientID: your-client-id - clientSecret: your-client-secret - sessionEncryptionKey: admin-key-at-least-32-bytes-long # Different encryption key - callbackURL: /oauth2/admin/callback # Different callback URL - cookiePrefix: "_oidc_adminauth_" # Different prefix for isolation - allowedUsers: # Restricted to specific admin users - - admin@example.com - - superadmin@example.com -``` - -**Security Note**: When running multiple instances, ensure you use: -1. **Different `cookiePrefix`** values to prevent cookie name collisions -2. **Different `sessionEncryptionKey`** values for complete session isolation -3. **Different `callbackURL`** paths to avoid routing conflicts - -This configuration prevents authorization bypass issues where a user authenticated via the general middleware could access admin-protected routes. See [issue #87](https://github.com/lukaszraczylo/traefikoidc/issues/87) for more details. - -### With Extended Session Duration - -For applications that users access infrequently (weekly or monthly), you can extend the session duration beyond the default 24 hours to reduce authentication friction: - -```yaml -apiVersion: traefik.io/v1alpha1 -kind: Middleware -metadata: - name: oidc-long-session - namespace: traefik -spec: - plugin: - traefikoidc: - providerURL: https://auth.example.com - clientID: your-client-id - clientSecret: your-client-secret - sessionEncryptionKey: your-key-at-least-32-bytes-long - callbackURL: /oauth2/callback - sessionMaxAge: 604800 # 7 days (in seconds) - # Other common values: - # 259200 - 3 days - # 604800 - 7 days - # 1209600 - 14 days - # 2592000 - 30 days -``` - -**Security Note**: Longer session durations improve user experience but increase security risk. Consider your application's security requirements: -- **High-security apps**: Use shorter sessions (3600 = 1 hour) -- **Standard apps**: Default 24 hours balances security and UX -- **Low-frequency access apps**: Extend to 7-30 days for better UX - -See [issue #91](https://github.com/lukaszraczylo/traefikoidc/issues/91) for more details. - -### With Custom Logging and Rate Limiting - -```yaml -apiVersion: traefik.io/v1alpha1 -kind: Middleware -metadata: - name: oidc-custom-settings - namespace: traefik -spec: - plugin: - traefikoidc: - providerURL: https://accounts.google.com - clientID: 1234567890.apps.googleusercontent.com - clientSecret: your-client-secret - sessionEncryptionKey: potato-secret-is-at-least-32-bytes-long - callbackURL: /oauth2/callback - logoutURL: /oauth2/logout - logLevel: debug # Options: debug, info, error (default: info) - rateLimit: 500 # Requests per second (default: 100) - forceHTTPS: false # Default is true for security - scopes: - - roles # Appended to defaults: ["openid", "profile", "email", "roles"] -``` - -### With Custom Post-Logout Redirect - -```yaml -apiVersion: traefik.io/v1alpha1 -kind: Middleware -metadata: - name: oidc-custom-logout - namespace: traefik -spec: - plugin: - traefikoidc: - providerURL: https://accounts.google.com - clientID: 1234567890.apps.googleusercontent.com - clientSecret: your-client-secret - sessionEncryptionKey: potato-secret-is-at-least-32-bytes-long - callbackURL: /oauth2/callback - logoutURL: /oauth2/logout - postLogoutRedirectURI: /logged-out-page # Where to redirect after logout - scopes: - - roles # Appended to defaults: ["openid", "profile", "email", "roles"] -``` - -### With IdP-Initiated Logout (Backchannel & Front-Channel) - -This plugin supports [OIDC Back-Channel Logout](https://openid.net/specs/openid-connect-backchannel-1_0.html) and [OIDC Front-Channel Logout](https://openid.net/specs/openid-connect-frontchannel-1_0.html) for IdP-initiated single logout. - -**Backchannel Logout** (recommended): The IdP sends a server-to-server POST request with a signed `logout_token` JWT when a user logs out. - -**Front-Channel Logout**: The IdP loads an iframe with the logout URL to invalidate the session in the browser. - -```yaml -apiVersion: traefik.io/v1alpha1 -kind: Middleware -metadata: - name: oidc-with-idp-logout - namespace: traefik -spec: - plugin: - traefikoidc: - providerURL: https://auth.example.com - clientID: your-client-id - clientSecret: your-client-secret - sessionEncryptionKey: potato-secret-is-at-least-32-bytes-long - callbackURL: /oauth2/callback - logoutURL: /oauth2/logout # RP-initiated logout - - # Backchannel Logout (server-to-server) - enableBackchannelLogout: true - backchannelLogoutURL: /backchannel-logout - - # Front-Channel Logout (iframe-based) - enableFrontchannelLogout: true - frontchannelLogoutURL: /frontchannel-logout - - # For multi-replica deployments, use Redis to share session invalidations - redis: - enabled: true - address: redis:6379 -``` - -> **Note**: For multi-replica deployments, you **must** enable Redis to share session invalidation state across all instances. Otherwise, a logout on one instance won't invalidate sessions on other instances. - -**IdP Configuration**: Configure your IdP to send logout requests to: -- **Backchannel**: `https://your-app.example.com/backchannel-logout` (POST with `logout_token`) -- **Front-Channel**: `https://your-app.example.com/frontchannel-logout?sid=SESSION_ID&iss=ISSUER` (GET in iframe) - -### With Templated Headers - -```yaml -apiVersion: traefik.io/v1alpha1 -kind: Middleware -metadata: - name: oidc-with-headers - namespace: traefik -spec: - plugin: - traefikoidc: - providerURL: https://accounts.google.com - clientID: 1234567890.apps.googleusercontent.com - clientSecret: your-client-secret - sessionEncryptionKey: potato-secret-is-at-least-32-bytes-long - callbackURL: /oauth2/callback - logoutURL: /oauth2/logout - scopes: - - roles # Appended to defaults: ["openid", "profile", "email", "roles"] - headers: - # Using double curly braces to escape template expressions - - name: "X-User-Email" - value: "{{{{.Claims.email}}}}" - - name: "X-User-ID" - value: "{{{{.Claims.sub}}}}" - - name: "Authorization" - value: "Bearer {{{{.AccessToken}}}}" - - name: "X-User-Roles" - value: "{{{{range $i, $e := .Claims.roles}}}}{{{{if $i}}}},{{{{end}}}}{{{{$e}}}}{{{{end}}}}" - - name: "X-Is-Admin" - value: "{{{{if eq .Claims.role \"admin\"}}}}true{{{{else}}}}false{{{{end}}}}" -``` - -### With PKCE Enabled - -```yaml -apiVersion: traefik.io/v1alpha1 -kind: Middleware -metadata: - name: oidc-with-pkce - namespace: traefik -spec: - plugin: - traefikoidc: - providerURL: https://accounts.google.com - clientID: 1234567890.apps.googleusercontent.com - clientSecret: your-client-secret - sessionEncryptionKey: potato-secret-is-at-least-32-bytes-long - callbackURL: /oauth2/callback - logoutURL: /oauth2/logout - enablePKCE: true # Enables PKCE for added security - scopes: - - roles # Appended to defaults: ["openid", "profile", "email", "roles"] -``` - -## Provider-Specific Configuration Examples - -### Google OIDC Configuration - -```yaml -apiVersion: traefik.io/v1alpha1 -kind: Middleware -metadata: - name: oidc-google - namespace: traefik -spec: - plugin: - traefikoidc: - providerURL: https://accounts.google.com - clientID: your-google-client-id.apps.googleusercontent.com - clientSecret: your-google-client-secret - sessionEncryptionKey: your-secure-encryption-key-min-32-chars - callbackURL: /oauth2/callback - logoutURL: /oauth2/logout - scopes: - - roles # Appended to defaults: ["openid", "profile", "email", "roles"] - # Note: DO NOT manually add offline_access scope for Google - # The middleware automatically handles Google-specific requirements - refreshGracePeriodSeconds: 300 # Optional: Start refresh 5 min before expiry - allowedUserDomains: - - your-gsuite-domain.com # Optional: Restrict to workspace users -``` - -### Azure AD Configuration - -```yaml -apiVersion: traefik.io/v1alpha1 -kind: Middleware -metadata: - name: oidc-azure - namespace: traefik -spec: - plugin: - traefikoidc: - providerURL: https://login.microsoftonline.com/your-tenant-id/v2.0 - clientID: your-azure-ad-client-id - clientSecret: your-azure-ad-client-secret - sessionEncryptionKey: your-secure-encryption-key-min-32-chars - callbackURL: /oauth2/callback - logoutURL: /oauth2/logout - scopes: - - roles # For group/role claims, configure in Azure AD Token Configuration - allowedUserDomains: - - yourcompany.com - allowedRolesAndGroups: - - "group-object-id-1" # Azure AD group Object IDs - - "AppRoleName" # Application role names -``` - -### Azure AD Configuration (Users Without Email) - -For Azure AD users without email addresses (service accounts, organizational accounts without mail attributes): - -```yaml -apiVersion: traefik.io/v1alpha1 -kind: Middleware -metadata: - name: oidc-azure-no-email - namespace: traefik -spec: - plugin: - traefikoidc: - providerURL: https://login.microsoftonline.com/your-tenant-id/v2.0 - clientID: your-azure-ad-client-id - clientSecret: your-azure-ad-client-secret - sessionEncryptionKey: your-secure-encryption-key-min-32-chars - callbackURL: /oauth2/callback - logoutURL: /oauth2/logout - - # Use 'sub' instead of 'email' for user identification - userIdentifierClaim: sub # Can also use: "oid", "upn", "preferred_username" - - overrideScopes: true # Optional: Don't request email scope if not needed - scopes: - - openid - - profile - - groups - - # When using non-email identifiers, allowedUsers matches against the claim value - allowedUsers: - - "abc12345-6789-0abc-def0-123456789abc" # Azure AD user object ID - - "def67890-1234-5678-90ab-cdef12345678" - - # NOTE: allowedUserDomains is ignored when userIdentifierClaim is not "email" -``` - -> **Note**: When `userIdentifierClaim` is set to a non-email claim (like `sub`, `oid`, or `upn`), the `allowedUserDomains` configuration is ignored since domain-based validation only applies to email addresses. Use `allowedUsers` with the actual claim values instead. - -### Auth0 Configuration - -```yaml -apiVersion: traefik.io/v1alpha1 -kind: Middleware -metadata: - name: oidc-auth0 - namespace: traefik -spec: - plugin: - traefikoidc: - providerURL: https://your-auth0-domain.auth0.com - clientID: your-auth0-client-id - clientSecret: your-auth0-client-secret - sessionEncryptionKey: your-secure-encryption-key-min-32-chars - callbackURL: /oauth2/callback - logoutURL: /oauth2/logout - - # Audience configuration for custom APIs - audience: "https://my-api.example.com" # Your API identifier from Auth0 - strictAudienceValidation: true # Enforce proper audience validation - - scopes: - - read:custom_data # Custom scopes as needed - - # Custom claim names for Auth0 namespaced claims - roleClaimName: "https://your-app.com/roles" # Auth0 requires namespaced custom claims - groupClaimName: "https://your-app.com/groups" # Must match claims added in Auth0 Actions - - allowedRolesAndGroups: - - admin # Will match "admin" in https://your-app.com/roles claim - - editor - postLogoutRedirectURI: /logged-out-page # Must be in Auth0 Allowed Logout URLs -``` - -**Note**: For detailed Auth0 audience configuration including opaque tokens and all security scenarios, see [AUTH0_AUDIENCE_GUIDE.md](docs/AUTH0_AUDIENCE_GUIDE.md). - -### Okta Configuration - -```yaml -apiVersion: traefik.io/v1alpha1 -kind: Middleware -metadata: - name: oidc-okta - namespace: traefik -spec: - plugin: - traefikoidc: - providerURL: https://your-tenant.okta.com/oauth2/default - clientID: your-okta-client-id - clientSecret: your-okta-client-secret - sessionEncryptionKey: your-secure-encryption-key-min-32-chars - callbackURL: /oauth2/callback - logoutURL: /oauth2/logout - scopes: - - groups # Include groups in token claims - allowedRolesAndGroups: - - admin - - developer - - "Everyone" # Default Okta group -``` - -### Keycloak Configuration - -```yaml -apiVersion: traefik.io/v1alpha1 -kind: Middleware -metadata: - name: oidc-keycloak - namespace: traefik -spec: - plugin: - traefikoidc: - providerURL: https://your-keycloak-domain/auth/realms/your-realm - clientID: your-keycloak-client-id - clientSecret: your-keycloak-client-secret - sessionEncryptionKey: your-secure-encryption-key-min-32-chars - callbackURL: /oauth2/callback - logoutURL: /oauth2/logout - scopes: - - roles - - groups - allowedRolesAndGroups: - - admin - - editor - # Ensure Keycloak client mappers add necessary claims to ID Token - # For internal Keycloak deployments with private IPs (e.g., Docker network): - # allowPrivateIPAddresses: true -``` - -> **Internal Network Deployment**: If your Keycloak runs on an internal network with private IP addresses (e.g., `192.168.x.x`, `10.x.x.x`, `172.16-31.x.x`) and you don't have DNS resolution available, set `allowPrivateIPAddresses: true` to allow the plugin to connect to your Keycloak instance. See [Issue #97](https://github.com/lukaszraczylo/traefikoidc/issues/97) for details. - -### AWS Cognito Configuration - -```yaml -apiVersion: traefik.io/v1alpha1 -kind: Middleware -metadata: - name: oidc-cognito - namespace: traefik -spec: - plugin: - traefikoidc: - providerURL: https://cognito-idp.us-east-1.amazonaws.com/us-east-1_YourUserPool - clientID: your-cognito-client-id - clientSecret: your-cognito-client-secret - sessionEncryptionKey: your-secure-encryption-key-min-32-chars - callbackURL: /oauth2/callback - logoutURL: /oauth2/logout - scopes: - - aws.cognito.signin.user.admin # Cognito-specific scope - allowedRolesAndGroups: - - admin - - user -``` - -### GitLab Configuration - -```yaml -apiVersion: traefik.io/v1alpha1 -kind: Middleware -metadata: - name: oidc-gitlab - namespace: traefik -spec: - plugin: - traefikoidc: - providerURL: https://gitlab.com - clientID: your-gitlab-client-id - clientSecret: your-gitlab-client-secret - sessionEncryptionKey: your-secure-encryption-key-min-32-chars - callbackURL: /oauth2/callback - logoutURL: /oauth2/logout - scopes: - - read_user - - read_api - allowedUserDomains: - - yourcompany.com -``` - -### GitHub OAuth Configuration ⚠️ - -**Warning**: GitHub uses OAuth 2.0, not OpenID Connect. Use only for API access, not user authentication. - -```yaml -apiVersion: traefik.io/v1alpha1 -kind: Middleware -metadata: - name: oauth-github - namespace: traefik -spec: - plugin: - traefikoidc: - providerURL: https://github.com/login/oauth - clientID: your-github-client-id - clientSecret: your-github-client-secret - sessionEncryptionKey: your-secure-encryption-key-min-32-chars - callbackURL: /oauth2/callback - logoutURL: /oauth2/logout - scopes: - - user:email - - read:user - # Note: No ID tokens available, only access tokens for GitHub API - # No refresh tokens - users must re-authenticate when tokens expire -``` - -The middleware automatically detects each provider and applies the necessary adjustments to ensure proper authentication and token refresh. - -### Keeping Secrets Secret in Kubernetes - -For Kubernetes environments, you can reference secrets instead of hardcoding sensitive values: - -```yaml -apiVersion: traefik.io/v1alpha1 -kind: Middleware -metadata: - name: oidc-with-secrets - namespace: traefik -spec: - plugin: - traefikoidc: - providerURL: urn:k8s:secret:traefik-middleware-oidc:ISSUER - clientID: urn:k8s:secret:traefik-middleware-oidc:CLIENT_ID - clientSecret: urn:k8s:secret:traefik-middleware-oidc:SECRET - sessionEncryptionKey: potato-secret-is-at-least-32-bytes-long - callbackURL: /oauth2/callback - logoutURL: /oauth2/logout - scopes: - - roles # Appended to defaults: ["openid", "profile", "email", "roles"] -``` - -Don't forget to create the secret: - -```bash -kubectl create secret generic traefik-middleware-oidc \ - --from-literal=ISSUER=https://accounts.google.com \ - --from-literal=CLIENT_ID=1234567890.apps.googleusercontent.com \ - --from-literal=SECRET=your-client-secret \ - -n traefik -``` - -## Complete Docker Compose Example - -Here's a complete example of using the middleware with Docker Compose: - -```yaml -version: "3.7" - -services: - traefik: - image: traefik:v3.2.1 - command: - - "--experimental.plugins.traefikoidc.modulename=github.com/lukaszraczylo/traefikoidc" - - "--experimental.plugins.traefikoidc.version=v0.7.10" - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - ./traefik-config/traefik.yml:/etc/traefik/traefik.yml - - ./traefik-config/dynamic-configuration.yml:/etc/traefik/dynamic-configuration.yml - labels: - - "traefik.http.routers.dash.rule=Host(`dash.localhost`)" - - "traefik.http.routers.dash.service=api@internal" - ports: - - "80:80" - - hello: - image: containous/whoami - labels: - - traefik.enable=true - - traefik.http.routers.hello.entrypoints=http - - traefik.http.routers.hello.rule=Host(`hello.localhost`) - - traefik.http.services.hello.loadbalancer.server.port=80 - - traefik.http.routers.hello.middlewares=my-plugin@file - - whoami: - image: jwilder/whoami - labels: - - traefik.enable=true - - traefik.http.routers.whoami.entrypoints=http - - traefik.http.routers.whoami.rule=Host(`whoami.localhost`) - - traefik.http.services.whoami.loadbalancer.server.port=8000 - - traefik.http.routers.whoami.middlewares=my-plugin@file -``` - -`traefik-config/traefik.yml`: -```yaml -log: - level: INFO - -experimental: - localPlugins: - traefikoidc: - moduleName: github.com/lukaszraczylo/traefikoidc - -# API and dashboard configuration -api: - dashboard: true - insecure: true - -entryPoints: - http: - address: ":80" - forwardedHeaders: - insecure: true - -providers: - docker: - endpoint: "unix:///var/run/docker.sock" - exposedByDefault: false - file: - filename: /etc/traefik/dynamic-configuration.yml -``` - -`traefik-config/dynamic-configuration.yml`: -```yaml -http: - middlewares: - my-plugin: - plugin: - traefikoidc: - providerURL: https://accounts.google.com - clientID: 1234567890.apps.googleusercontent.com - clientSecret: your-client-secret - callbackURL: /oauth2/callback - logoutURL: /oauth2/logout - postLogoutRedirectURI: /logged-out-page - sessionEncryptionKey: potato-secret-is-at-least-32-bytes-long - scopes: - - roles # Appended to defaults: ["openid", "profile", "email", "roles"] - allowedUserDomains: - - company.com - allowedUsers: - - special-user@gmail.com - - contractor@external.org - allowedRolesAndGroups: - - admin - - developer - forceHTTPS: false - logLevel: debug - rateLimit: 100 - excludedURLs: - - /login - - /public - - /health - - /metrics - headers: - # Using YAML literal style to prevent Traefik from pre-evaluating templates - - name: "X-User-Email" - value: | - {{.Claims.email}} - - name: "X-User-ID" - value: | - {{.Claims.sub}} - - name: "Authorization" - value: | - Bearer {{.AccessToken}} - - name: "X-User-Roles" - value: | - {{range $i, $e := .Claims.roles}}{{if $i}},{{end}}{{$e}}{{end}} -``` - -### Templated Headers - -The middleware supports setting custom HTTP headers with values templated from OIDC claims and tokens. This allows you to pass authentication information to downstream services in a flexible, customized format. - -Templates can access the following variables: -- `{{.Claims.field}}` - Access individual claims from the ID token (e.g., `{{.Claims.email}}`, `{{.Claims.sub}}`) -- `{{.AccessToken}}` - The raw access token string -- `{{.IdToken}}` - The raw ID token string (same as AccessToken in most configurations) -- `{{.RefreshToken}}` - The raw refresh token string - -**⚠️ Important: Template Escaping** - -If you encounter the error `can't evaluate field AccessToken in type bool` when starting Traefik, this indicates that Traefik is attempting to evaluate the template expressions before passing them to the plugin. This is a known issue when using template syntax in Traefik plugin configurations. - -**Solution:** You must escape the template expressions using double curly braces: +Forward identity to backends via Go templates over ID-token claims and tokens: ```yaml headers: - - name: "Authorization" - value: "Bearer {{{{.AccessToken}}}}" -``` - -This is the only reliable method that works consistently. Here's why: - -- **Double curly braces (`{{{{.AccessToken}}}}`)** ✅ - - The YAML parser converts `{{{{` → `{{` and `}}}}` → `}}` - - Result: `Bearer {{.AccessToken}}` reaches the Go template engine correctly - -- **Other methods (YAML literal style, single quotes) do NOT work** ❌ - - These methods don't prevent Traefik's YAML parser from interpreting the curly braces - - The template syntax gets processed incorrectly before reaching the plugin - -**Working example configuration:** -```yaml -headers: - - name: "X-User-Email" + - name: X-User-Email value: "{{{{.Claims.email}}}}" - - name: "X-User-ID" - value: "{{{{.Claims.sub}}}}" - - name: "Authorization" + - name: Authorization value: "Bearer {{{{.AccessToken}}}}" - - name: "X-User-Name" - value: "{{{{.Claims.given_name}}}} {{{{.Claims.family_name}}}}" -``` - -**Advanced template examples:** - -Conditional logic: -```yaml -headers: - - name: "X-Is-Admin" - value: "{{{{if eq .Claims.role \"admin\"}}}}true{{{{else}}}}false{{{{end}}}}" -``` - -Array handling: -```yaml -headers: - - name: "X-User-Roles" + - name: X-User-Roles value: "{{{{range $i, $e := .Claims.roles}}}}{{{{if $i}}}},{{{{end}}}}{{{{$e}}}}{{{{end}}}}" ``` -**Notes:** -- Variable names are case-sensitive (use `.Claims`, not `.claims`) -- Missing claims will result in `` in the header value -- The middleware validates templates during startup and logs errors for invalid templates -- Always use double curly braces (`{{{{` and `}}}}`) to escape template expressions in YAML configuration files - -### Default Headers Set for Downstream Services - - -When a user is authenticated, the middleware sets the following headers for downstream services: - -- `X-Forwarded-User`: The user's email address (always set) -- `X-User-Groups`: Comma-separated list of user groups (if available) -- `X-User-Roles`: Comma-separated list of user roles (if available) -- `X-Auth-Request-Redirect`: The original request URI -- `X-Auth-Request-User`: The user's email address -- `X-Auth-Request-Token`: The user's ID token (can be large) - -#### Minimal Headers Mode - -If your downstream services return **"431 Request Header Fields Too Large"** errors, you can enable minimal headers mode to reduce header overhead: - -```yaml -http: - middlewares: - my-auth: - plugin: - traefikoidc: - minimalHeaders: true - # ... other config -``` - -When `minimalHeaders: true` is set: -- **Only forwards**: `X-Forwarded-User` -- **Skips**: `X-Auth-Request-Token` (the full ID token - often the largest header), `X-Auth-Request-User`, `X-Auth-Request-Redirect` -- **Still forwards**: `X-User-Groups` and `X-User-Roles` (if configured) -- **Still processes**: Custom templated headers - -This is particularly useful when: -- Your ID tokens are large (many claims, long group lists) -- Downstream services have limited header buffer sizes (default 8KB in many servers) -- You don't need the full token forwarded to backend services - -See [GitHub Issue #64](https://github.com/lukaszraczylo/traefikoidc/issues/64) for details. - -#### Strip Auth Cookies Mode - -If your backend services return **"431 Request Header Fields Too Large"** errors due to large OIDC session cookies (which can reach ~28KB with token chunking), you can strip them before forwarding: - -```yaml -http: - middlewares: - my-auth: - plugin: - traefikoidc: - stripAuthCookies: true - # ... other config -``` - -When `stripAuthCookies: true` is set: -- **Strips**: All OIDC session cookies (`_oidc_raczylo_*`) from the request before forwarding to the backend -- **Preserves**: All non-OIDC cookies (application sessions, preferences, etc.) -- **No browser impact**: Cookies remain in the browser and are still sent to Traefik for session management - -This can be combined with `minimalHeaders: true` for maximum header size reduction. - -See [GitHub Issue #122](https://github.com/lukaszraczylo/traefikoidc/issues/122) for details. - -### Security Headers - -The middleware also sets the following security headers: - -- `X-Frame-Options: DENY` -- `X-Content-Type-Options: nosniff` -- `X-XSS-Protection: 1; mode=block` -- `Referrer-Policy: strict-origin-when-cross-origin` - -## Provider Configuration Recommendations - -**Important: ID Token Validation** - -This Traefik OIDC plugin performs authentication and extracts user claims (like email, roles, groups) exclusively from the **ID Token** provided by your OIDC provider. It does not primarily use the Access Token for these critical functions. Therefore, it is crucial to ensure that all necessary claims are included in the ID Token itself. A common issue is that some OIDC providers might, by default, place certain claims only in the Access Token or UserInfo endpoint. - -This section provides guidance on configuring popular OIDC providers to work optimally with this plugin. - -### Google Workspace / Google Cloud Identity - -Google's OIDC implementation is well-supported with automatic configuration. - -* **Automatic Configuration**: The middleware automatically detects Google and applies required settings: - * Uses `access_type=offline` and `prompt=consent` for refresh tokens - * Filters out unsupported `offline_access` scope - * Handles Google-specific token refresh -* **Setup Requirements**: - * Create OAuth 2.0 credentials in Google Cloud Console - * Configure OAuth consent screen (must be "Published" for production) - * Add authorized redirect URIs -* **ID Token Claims**: Google includes standard claims like `email`, `sub`, `name`, `given_name`, `family_name`, `picture` -* **Hosted Domain**: For Google Workspace, the `hd` claim contains the organization domain -* **Best Practices**: Use `providerURL: https://accounts.google.com` - -### Azure AD (Microsoft Entra ID) - -Azure AD provides comprehensive enterprise OIDC support. - -* **Tenant Configuration**: Use tenant-specific endpoint: `https://login.microsoftonline.com/{tenant-id}/v2.0` -* **Group Claims**: Configure in App Registration → Token Configuration → Add groups claim -* **ID Token Claims**: Includes `email`, `name`, `preferred_username`, `oid` by default -* **Group Handling**: Be aware of group "overage" - too many groups results in a groups claim link instead of embedded groups -* **Optional Claims**: Add custom claims via Token Configuration section -* **Multi-tenant**: Supports both single-tenant and multi-tenant applications - -### Auth0 - -Auth0 provides flexible OIDC with custom claims support. - -* **Custom Claims**: Use Auth0 Actions (recommended) or Rules to add claims to ID Token: - ```javascript - // Auth0 Action example - exports.onExecutePostLogin = async (event, api) => { - const namespace = 'https://your-app.com/'; - if (event.authorization) { - api.idToken.setCustomClaim(namespace + 'roles', event.authorization.roles); - api.idToken.setCustomClaim('email', event.user.email); - } - }; - ``` -* **Logout Configuration**: Ensure `postLogoutRedirectURI` is in "Allowed Logout URLs" -* **Application Type**: Set to "Regular Web Application" for server-side flows -* **Refresh Tokens**: Automatically handled with `offline_access` scope - -### Okta - -Okta provides enterprise-grade OIDC with extensive customization. - -* **Application Setup**: Create OIDC Web Application in Okta Admin Console -* **Authorization Server**: Use default (`/oauth2/default`) or custom authorization server -* **Group Claims**: Configure Groups claim in authorization server to include user groups -* **Scopes**: Default scopes sufficient; add `groups` scope for group information -* **Sign-On Policy**: Configure authentication policies and MFA requirements -* **Custom Claims**: Add custom attributes via user profiles and authorization server claims - -### Keycloak - -Keycloak is highly configurable, requiring proper client mapper setup. - -* **Client Mappers**: Essential for including claims in ID Token: - * **Email**: User Property mapper for `email` with "Add to ID token" enabled - * **Roles**: User Client Role or User Realm Role mappers with "Add to ID token" enabled - * **Groups**: Group Membership mapper with "Add to ID token" enabled -* **Token Claim Names**: Use mapper "Token Claim Name" in `allowedRolesAndGroups` configuration -* **Realm Configuration**: Ensure proper realm settings and client configuration -* **Issuer URL Format**: `https://your-keycloak/auth/realms/your-realm` -* **Troubleshooting**: Verify mappers in Clients → Your Client → Mappers tab - -### AWS Cognito - -AWS Cognito provides managed OIDC with regional deployment. - -* **User Pool Setup**: Create User Pool with proper app client configuration -* **App Client**: Enable "Authorization code grant" and configure callback URLs -* **Regional Endpoints**: Auto-detected from issuer URL format -* **Custom Attributes**: Configure custom attributes and map to claims -* **Groups**: Use Cognito Groups for role-based access control -* **Federation**: Supports federated identity providers (SAML, social providers) - -### GitLab - -GitLab supports OIDC for both GitLab.com and self-hosted instances. - -* **Application Registration**: Create in GitLab Admin Area → Applications -* **Scopes**: Use `openid`, `profile`, `email` for basic claims -* **Self-hosted**: Use your GitLab instance URL as `providerURL` -* **GitLab.com**: Use `https://gitlab.com` as `providerURL` -* **Group Claims**: May require custom configuration for group information -* **API Access**: Include `read_api` scope for GitLab API access via access token - -### GitHub (OAuth 2.0 Only) ⚠️ - -**Important**: GitHub uses OAuth 2.0, not OpenID Connect. - -* **OAuth App Setup**: Register OAuth App in GitHub Settings → Developer settings -* **Limitations**: - * No ID tokens (access tokens only) - * No refresh tokens (tokens expire, requiring re-authentication) - * No standard OIDC claims -* **Use Cases**: API access only, not suitable for user authentication with claims -* **Scopes**: Use `user:email`, `read:user` for basic profile access -* **Detection**: Auto-detected from `github.com` in issuer URL - -### Auth0 - -Auth0 is generally OIDC compliant and works well. - -* **ID Token Claims**: - * To add custom claims or standard claims not included by default (like roles or permissions) to the ID Token, you'll need to use Auth0 Rules or Actions. - * **Using Actions (Recommended)**: Create a custom Action that runs after login to add claims to the ID Token. Example: - ```javascript - // Auth0 Action to add email and roles to ID Token - exports.onExecutePostLogin = async (event, api) => { - const namespace = 'https://your-app.com/'; // Or your custom namespace - if (event.authorization) { - api.idToken.setCustomClaim(namespace + 'roles', event.authorization.roles); - api.idToken.setCustomClaim('email', event.user.email); // Standard claim, ensure it's there - // Add other claims as needed - } - }; - ``` - * Ensure the claims you add (e.g., `https://your-app.com/roles`) are then used in the plugin's `allowedRolesAndGroups` or `headers` configuration. -* **Scopes**: Request appropriate scopes. You might need custom scopes if your Actions/Rules depend on them to add specific claims. -* **Endpoints**: Your `providerURL` will be `https://your-auth0-domain.auth0.com`. -* **Logout**: Ensure `postLogoutRedirectURI` is registered in your Auth0 application settings under "Allowed Logout URLs". - -### Generic OIDC Providers - -For other OIDC providers (e.g., Okta, Zitadel, self-hosted solutions): - -* **ID Token is Key**: The primary requirement is that all claims needed for authentication decisions (email, roles, groups, custom attributes for headers) **must** be included in the ID Token. -* **Check Provider Documentation**: Consult your OIDC provider's documentation on how to: - * Configure client applications. - * Map user attributes, roles, or group memberships to claims in the ID Token. - * Define custom scopes if they are necessary to include certain claims. -* **Standard Endpoints**: Ensure your provider exposes a standard OIDC discovery document (`.well-known/openid-configuration`) at the `providerURL`. The plugin uses this to find authorization, token, JWKS, and end_session endpoints. -* **Scopes**: Always include `openid` in your scopes. `profile` and `email` are generally recommended. Add other scopes as required by your provider to release specific claims to the ID Token. -* **Troubleshooting**: If the plugin isn't working as expected (e.g., access denied, claims missing), the first step is to decode the ID Token received from your provider (e.g., using jwt.io) to verify its contents. This will show you exactly what claims the plugin is seeing. - -For common issues and general troubleshooting, please refer to the [Troubleshooting](#troubleshooting) section. - -## Troubleshooting - -### Logging - -Set the `logLevel` to `debug` to get more detailed logs: - -```yaml -logLevel: debug -``` - -### Common Issues - -1. **Token verification failed**: Check that your `providerURL` is correct and accessible. -2. **Session encryption key too short**: Ensure your `sessionEncryptionKey` is at least 32 bytes long. -3. **No matching public key found**: The JWKS endpoint might be unavailable or the token's key ID (kid) doesn't match any key in the JWKS. -4. **Access denied: Your email domain is not allowed**: The user's email domain is not in the `allowedUserDomains` list. -5. **Access denied: You do not have any of the allowed roles or groups**: The user doesn't have any of the roles or groups specified in `allowedRolesAndGroups`. -6. **"can't evaluate field AccessToken in type bool" error**: This error occurs when Traefik attempts to evaluate template expressions in the headers configuration before passing them to the plugin. To fix this: - - Use double curly braces to escape template expressions: `value: "Bearer {{{{.AccessToken}}}}"` - - This is the only reliable method that works with Traefik's YAML parsing - - See the [Templated Headers](#templated-headers) section for complete examples - -#### Provider-Specific Issues - -7. **Google sessions expire after ~1 hour**: If using Google as the OIDC provider and sessions expire prematurely: - - Do NOT manually add the `offline_access` scope. Google rejects this scope as invalid. - - The middleware automatically applies Google parameters (`access_type=offline` and `prompt=consent`). - - Ensure your Google Cloud OAuth consent screen is "Published" for production. - - "Testing" mode limits refresh token validity. - -8. **Keycloak: Claims Missing from ID Token**: - - Configure client mappers to add email, roles, groups to ID Token - - Check "Add to ID token" is enabled for all required mappers - - Verify "Token Claim Name" matches your configuration - -9. **Azure AD: Group overage issues**: - - Users with many groups may receive a groups link instead of embedded groups - - Consider using app roles instead of groups for many-group scenarios - - Configure group claims in App Registration → Token Configuration - -10. **Auth0: Custom claims not appearing**: - - Use Auth0 Actions (not Rules) to add custom claims to ID Token - - Ensure namespaced claims follow format: `https://your-app.com/claim` - - Add claims to ID token specifically, not just access token - -11. **Okta: Authorization server issues**: - - Verify using correct authorization server endpoint (`/oauth2/default` or custom) - - Ensure Groups claim is configured in authorization server - - Check application assignment and user group membership - -12. **AWS Cognito: Regional endpoint errors**: - - Use correct regional endpoint format: `cognito-idp.{region}.amazonaws.com` - - Verify User Pool ID is correct in issuer URL - - Check app client has authorization code grant enabled - -13. **GitLab: Self-hosted instance issues**: - - Ensure issuer URL points to your GitLab instance root - - Verify application is created in Admin Area → Applications - - Check redirect URI configuration matches exactly - -14. **GitHub: Limited functionality warnings**: - - Remember GitHub is OAuth 2.0 only, not OIDC - - No ID tokens available (access tokens only) - - No refresh tokens (re-authentication required on expiry) - - Use only for GitHub API access, not user authentication - -15. **Environment variable names containing "API" cause plugin failure** ([Issue #98](https://github.com/lukaszraczylo/traefikoidc/issues/98)): - - When using environment variable syntax like `${OIDC_ENCRYPTION_SECRET_API}` in Traefik configuration, the plugin fails with "invalid handler type: \" error - - This is a **Traefik-side issue**, not a plugin bug. Traefik uses reserved environment variables starting with `TRAEFIK_API_*` for its internal API configuration, and the "API" substring in user-defined variable names may interfere with Traefik's environment variable processing - - **Workaround**: Avoid using "API" as a substring in environment variable names. Use alternatives like: - - `${OIDC_ENCRYPTION_SECRET_SVC}` instead of `${OIDC_ENCRYPTION_SECRET_API}` - - `${OIDC_ENCRYPTION_SECRET_SERVICE}` - - `${OIDC_ENCRYPTION_SECRET_BACKEND}` - - Any name that doesn't contain the literal substring "API" - -### Provider Warnings and Recommendations - -The middleware includes built-in warnings for provider-specific limitations. Check your logs for important notices about: - -- **GitHub OAuth 2.0 limitations** (no OIDC support) -- **Auth0 offline_access scope requirements** -- **Keycloak URL pattern requirements** -- **AWS Cognito regional endpoint requirements** -- **Provider-specific setup recommendations** - -For detailed provider-specific guidance, see the [Provider-Specific Configuration Examples](#provider-specific-configuration-examples) section. - -## Recent Improvements - -### Security Features (v0.4.0+) - -- **Security Headers**: Complete security headers system with CSP, HSTS, CORS, and XSS protection -- **Multiple Security Profiles**: Choose from default, strict, development, API, or custom security configurations -- **Enhanced Token Validation**: Improved JWT validation with comprehensive security checks -- **Advanced Rate Limiting**: Configurable rate limiting to prevent abuse - -### User Experience (v0.4.0+) - -- **Automatic Provider Detection**: Seamless configuration for major OIDC providers -- **Improved Error Handling**: Better error messages and graceful degradation -- **Enhanced Session Management**: More reliable session handling with automatic cleanup -- **Flexible Configuration**: Expanded configuration options for different deployment scenarios - -### Reliability (v0.4.0+) - -- **Automatic Token Refresh**: Proactive token refresh to prevent authentication interruptions -- **Memory Management**: Improved memory efficiency and automatic resource cleanup -- **Better Provider Support**: Enhanced compatibility with provider-specific features -- **Comprehensive Testing**: Extensive test coverage ensures reliability in production - -## Architecture Overview - -### Design Principles - -The middleware is designed with the following principles: - -- **Reliability**: Automatic error recovery and graceful degradation -- **Security**: Comprehensive security measures and validation -- **Performance**: Efficient resource usage and caching -- **Flexibility**: Extensive configuration options for different use cases -- **Compatibility**: Support for all major OIDC providers with automatic detection - -### Key Features - -- **Automatic Session Management**: Handles session lifecycle, cleanup, and security -- **Provider Integration**: Seamless integration with OIDC providers including auto-discovery -- **Security Integration**: Built-in security headers and protection mechanisms -- **Resource Management**: Efficient memory usage and automatic cleanup -- **Error Handling**: Comprehensive error recovery and user-friendly error messages - -## Contributing - -Contributions are welcome! Please feel free to submit a Pull Request. - -### Development Guidelines - -1. **Memory Management**: Ensure all goroutines can be cancelled and resources are bounded -2. **Testing**: Add tests for new features, including memory leak tests where appropriate -3. **Race Conditions**: Run tests with `-race` flag to detect race conditions -4. **Documentation**: Update README and .traefik.yml for any new configuration options +Available bindings: `.Claims.`, `.AccessToken`, `.IdToken`, +`.RefreshToken`. Names are case-sensitive (`.Claims`, not `.claims`). + +> **Escape with quadruple braces.** If you see +> `can't evaluate field AccessToken in type bool`, Traefik's YAML parser ate +> your `{{ }}`. The fix that actually works is `{{{{ }}}}` — the YAML pass +> turns it into `{{ }}` for the Go template engine. Other escaping tricks +> (literal blocks, single quotes) do not work reliably. + +## Default downstream headers + +When a request is authenticated, the middleware sets: + +| Header | Notes | +|--------|-------| +| `X-Forwarded-User` | User's email (always). | +| `X-User-Groups` | Comma-separated. | +| `X-User-Roles` | Comma-separated. | +| `X-Auth-Request-User` | User's email. | +| `X-Auth-Request-Redirect` | Original request URI. | +| `X-Auth-Request-Token` | Full ID token — the largest header; suppressed by `minimalHeaders`. | + +Plus security headers (CSP, HSTS, X-Frame-Options, X-Content-Type-Options, +X-XSS-Protection, Referrer-Policy) controlled by the `securityHeaders` +section — see [docs/CONFIGURATION.md](docs/CONFIGURATION.md#security-headers). + +## Common errors + +| Symptom | Cause | +|---------|-------| +| `Token verification failed` | Wrong/unreachable `providerURL`, or clock skew. | +| `Session encryption key too short` | `sessionEncryptionKey` is < 32 bytes. | +| `No matching public key found` | JWKS endpoint down, or `kid` mismatch. | +| `Access denied: Your email domain is not allowed` | User's domain not in `allowedUserDomains`. | +| `Access denied: You do not have any of the allowed roles or groups` | Claims missing or not in `allowedRolesAndGroups`. | +| `can't evaluate field AccessToken in type bool` | Template not escaped — use `{{{{ }}}}`. | +| `tls: failed to verify certificate: x509: certificate signed by unknown authority` | Internal CA — set `caCertPath` / `caCertPEM`. | +| `invalid handler type: ` | Env var name contains `API` — rename it. | +| `false positive replay detected` | Multi-replica without Redis — see [Multi-replica deployments](#multi-replica-deployments). | +| Google sessions expire after ~1h | Consent screen still in "Testing" mode. **Do not** add `offline_access` — Google rejects it; the middleware sets `access_type=offline` automatically. | + +Provider-specific issues (Keycloak mappers, Azure AD group overage, Auth0 +namespaced claims, Cognito regions, GitLab self-hosted) live in +[docs/PROVIDERS.md](docs/PROVIDERS.md). + +Set `logLevel: debug` to surface detail. + +## License + +See [LICENSE](LICENSE). diff --git a/auth_flow.go b/auth_flow.go index 8936366..9cf672d 100644 --- a/auth_flow.go +++ b/auth_flow.go @@ -4,8 +4,6 @@ import ( "fmt" "net/http" "strings" - - "github.com/google/uuid" ) // validateRedirectCount checks if redirect limit is exceeded and handles the error @@ -77,7 +75,12 @@ func (t *TraefikOidc) defaultInitiateAuthentication(rw http.ResponseWriter, req return } - csrfToken := uuid.NewString() + csrfToken, err := newUUIDv4() + if err != nil { + t.logger.Errorf("Failed to generate CSRF token: %v", err) + http.Error(rw, "Failed to generate CSRF token", http.StatusInternalServerError) + return + } nonce, err := generateNonce() if err != nil { t.logger.Errorf("Failed to generate nonce: %v", err) @@ -334,6 +337,29 @@ func (t *TraefikOidc) isAjaxRequest(req *http.Request) bool { strings.Contains(accept, "application/json") } +// isNonNavigationRequest reports whether the request is a browser +// sub-resource (script, image, stylesheet, fetch, serviceWorker) rather than +// a top-level HTML navigation. Non-navigation requests MUST NOT trigger an +// OIDC redirect flow: several sub-resource loads happening in parallel would +// each call defaultInitiateAuthentication, each overwriting the session's +// CSRF/nonce, breaking the eventual callback (issue #129). +// +// Detection prefers Sec-Fetch-Mode, which all modern browsers send +// (Chrome/Edge/Firefox/Safari). For older or non-browser clients we fall +// back to Accept: if Accept is present and does not list text/html, treat +// it as a sub-resource. An empty/missing Accept is assumed to be navigation +// (safer to redirect than 401 on an ambiguous request). +func (t *TraefikOidc) isNonNavigationRequest(req *http.Request) bool { + if mode := req.Header.Get("Sec-Fetch-Mode"); mode != "" { + return mode != "navigate" + } + accept := req.Header.Get("Accept") + if accept == "" || accept == "*/*" { + return false + } + return !strings.Contains(accept, "text/html") +} + // isRefreshTokenExpired checks if refresh token is likely expired (older than 6 hours) func (t *TraefikOidc) isRefreshTokenExpired(session *SessionData) bool { // This is a heuristic check - actual implementation would depend on diff --git a/auth_flow_behaviour_test.go b/auth_flow_behaviour_test.go index b39af6a..9e694b4 100644 --- a/auth_flow_behaviour_test.go +++ b/auth_flow_behaviour_test.go @@ -305,6 +305,90 @@ func (s *AuthFlowBehaviourSuite) TestIsAjaxRequest() { } } +// TestIsNonNavigationRequest verifies browser sub-resource detection used to +// suppress OIDC redirects on parallel static-asset loads (issue #129). +func (s *AuthFlowBehaviourSuite) TestIsNonNavigationRequest() { + testCases := []struct { + headers map[string]string + name string + expectNonNavigation bool + }{ + { + name: "Sec-Fetch-Mode navigate", + headers: map[string]string{"Sec-Fetch-Mode": "navigate"}, + expectNonNavigation: false, + }, + { + name: "Sec-Fetch-Mode no-cors", + headers: map[string]string{"Sec-Fetch-Mode": "no-cors"}, + expectNonNavigation: true, + }, + { + name: "Sec-Fetch-Mode cors", + headers: map[string]string{"Sec-Fetch-Mode": "cors"}, + expectNonNavigation: true, + }, + { + name: "Sec-Fetch-Mode same-origin (fetch in page)", + headers: map[string]string{"Sec-Fetch-Mode": "same-origin"}, + expectNonNavigation: true, + }, + { + name: "Accept text/html (fallback)", + headers: map[string]string{"Accept": "text/html,application/xhtml+xml"}, + expectNonNavigation: false, + }, + { + name: "Accept image/png (fallback)", + headers: map[string]string{"Accept": "image/png,image/*;q=0.8"}, + expectNonNavigation: true, + }, + { + name: "Accept application/javascript (fallback)", + headers: map[string]string{"Accept": "application/javascript"}, + expectNonNavigation: true, + }, + { + name: "Accept */* treated as navigation", + headers: map[string]string{"Accept": "*/*"}, + expectNonNavigation: false, + }, + { + name: "No Accept header assumed navigation", + headers: map[string]string{}, + expectNonNavigation: false, + }, + { + name: "Sec-Fetch-Mode beats Accept (navigate wins)", + headers: map[string]string{ + "Sec-Fetch-Mode": "navigate", + "Accept": "application/javascript", + }, + expectNonNavigation: false, + }, + { + name: "Sec-Fetch-Mode beats Accept (no-cors wins)", + headers: map[string]string{ + "Sec-Fetch-Mode": "no-cors", + "Accept": "text/html", + }, + expectNonNavigation: true, + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + req := httptest.NewRequest(http.MethodGet, "/_static/asset.js", nil) + for key, value := range tc.headers { + req.Header.Set(key, value) + } + + result := s.tOidc.isNonNavigationRequest(req) + s.Equal(tc.expectNonNavigation, result) + }) + } +} + // TestHandleCallback_MissingState tests callback with missing state parameter func (s *AuthFlowBehaviourSuite) TestHandleCallback_MissingState() { sessionManager, err := NewSessionManager( diff --git a/background_tasks_ultra_test.go b/background_tasks_ultra_test.go index c9b32bc..51ce837 100644 --- a/background_tasks_ultra_test.go +++ b/background_tasks_ultra_test.go @@ -29,8 +29,9 @@ func TestMemoryMonitorComprehensive(t *testing.T) { pressure := monitor.GetMemoryPressure() assert.Equal(t, MemoryPressureNone, pressure) - // Collect stats to populate lastStats - monitor.GetCurrentStats() + // Explicitly sample to populate lastStats; GetCurrentStats is now a + // cached read and no longer forces a runtime.ReadMemStats. + monitor.Refresh() // Now should return a valid pressure level pressure = monitor.GetMemoryPressure() @@ -46,11 +47,13 @@ func TestMemoryMonitorComprehensive(t *testing.T) { thresholds := DefaultMemoryAlertThresholds() monitor := NewMemoryMonitor(newNoOpLogger(), thresholds) - // Start monitoring should not panic + // Start monitoring should not panic. Interval is clamped to the + // minimum (30s); we rely on Refresh() when we need a synchronous + // sample instead of waiting for a tick. assert.NotPanics(t, func() { ctx := context.Background() - monitor.StartMonitoring(ctx, 100*time.Millisecond) - time.Sleep(GetTestDuration(50 * time.Millisecond)) + monitor.StartMonitoring(ctx, 0) + monitor.Refresh() }) // Clean up @@ -117,6 +120,9 @@ func TestMemoryMonitorComprehensive(t *testing.T) { thresholds := DefaultMemoryAlertThresholds() monitor := NewMemoryMonitor(newNoOpLogger(), thresholds) + // Refresh forces a synchronous sample; GetCurrentStats is a cached + // read, so we sample first to guarantee fresh data. + monitor.Refresh() stats := monitor.GetCurrentStats() assert.NotNil(t, stats) assert.Greater(t, stats.HeapAllocBytes, uint64(0)) @@ -450,12 +456,12 @@ func TestMemoryMonitorIntegration(t *testing.T) { monitor := NewMemoryMonitor(newNoOpLogger(), thresholds) defer monitor.StopMonitoring() - // Start monitoring + // Start monitoring. The interval is clamped to the minimum (30s) so + // the ticker won't fire during the test; drive the sample manually via + // Refresh() instead. ctx := context.Background() - monitor.StartMonitoring(ctx, 50*time.Millisecond) - - // Wait for at least one check - time.Sleep(GetTestDuration(150 * time.Millisecond)) + monitor.StartMonitoring(ctx, 0) + monitor.Refresh() // Get pressure (should be a valid pressure level) pressure := monitor.GetMemoryPressure() @@ -488,6 +494,7 @@ func TestMemoryStatsCollection(t *testing.T) { thresholds := DefaultMemoryAlertThresholds() monitor := NewMemoryMonitor(newNoOpLogger(), thresholds) + monitor.Refresh() stats := monitor.GetCurrentStats() assert.NotNil(t, stats) @@ -501,6 +508,7 @@ func TestMemoryStatsCollection(t *testing.T) { thresholds := DefaultMemoryAlertThresholds() monitor := NewMemoryMonitor(newNoOpLogger(), thresholds) + monitor.Refresh() stats := monitor.GetCurrentStats() // Should calculate and include pressure level @@ -521,13 +529,14 @@ func TestMemoryStatsCollection(t *testing.T) { // Allocate some memory _ = make([]byte, 1024*1024) // 1MB - // Get stats before GC - beforeStats := monitor.GetCurrentStats() + // Get stats before GC (explicit Refresh so we have a fresh pre-GC + // snapshot to compare against, not the constructor baseline). + beforeStats := monitor.Refresh() - // Trigger GC + // Trigger GC (internally Refresh()es before and after) monitor.TriggerGC() - // Get stats after GC + // Get stats after GC from cache (TriggerGC already refreshed it) afterStats := monitor.GetCurrentStats() // After GC should have different stats diff --git a/ca_cert_test.go b/ca_cert_test.go new file mode 100644 index 0000000..cc6c457 --- /dev/null +++ b/ca_cert_test.go @@ -0,0 +1,137 @@ +package traefikoidc + +import ( + "encoding/pem" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" +) + +// testCertPEM returns a valid PEM-encoded certificate harvested from an +// httptest.NewTLSServer. Using httptest keeps the test free of any +// handwritten static cert that could expire. +func testCertPEM(t *testing.T) string { + t.Helper() + srv := httptest.NewTLSServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {})) + t.Cleanup(srv.Close) + + cert := srv.Certificate() + if cert == nil { + t.Fatal("httptest.NewTLSServer did not expose a certificate") + } + return string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw})) +} + +func TestLoadCACertPool_Empty(t *testing.T) { + cfg := &Config{} + pool, err := cfg.loadCACertPool() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if pool != nil { + t.Errorf("expected nil pool when no CA source configured, got %v", pool) + } +} + +func TestLoadCACertPool_InlinePEM(t *testing.T) { + cfg := &Config{CACertPEM: testCertPEM(t)} + pool, err := cfg.loadCACertPool() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if pool == nil { + t.Fatal("expected non-nil pool for valid CACertPEM") + } +} + +func TestLoadCACertPool_InlinePEM_Garbage(t *testing.T) { + cfg := &Config{CACertPEM: "not a pem"} + pool, err := cfg.loadCACertPool() + if err == nil { + t.Fatal("expected error for garbage CACertPEM, got nil") + } + if pool != nil { + t.Errorf("expected nil pool on error, got %v", pool) + } + if !strings.Contains(err.Error(), "caCertPEM") { + t.Errorf("error should name the failing field, got: %v", err) + } +} + +func TestLoadCACertPool_FilePath(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "ca.pem") + if err := os.WriteFile(path, []byte(testCertPEM(t)), 0o600); err != nil { + t.Fatalf("writing temp PEM: %v", err) + } + + cfg := &Config{CACertPath: path} + pool, err := cfg.loadCACertPool() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if pool == nil { + t.Fatal("expected non-nil pool for valid CACertPath") + } +} + +func TestLoadCACertPool_FilePath_Missing(t *testing.T) { + cfg := &Config{CACertPath: "/does/not/exist/ca.pem"} + pool, err := cfg.loadCACertPool() + if err == nil { + t.Fatal("expected error for missing CACertPath, got nil") + } + if pool != nil { + t.Errorf("expected nil pool on error, got %v", pool) + } +} + +func TestLoadCACertPool_Combined(t *testing.T) { + // Both inline and file sources populated — certificates from both should + // be accepted into the same pool. + dir := t.TempDir() + path := filepath.Join(dir, "ca.pem") + if err := os.WriteFile(path, []byte(testCertPEM(t)), 0o600); err != nil { + t.Fatalf("writing temp PEM: %v", err) + } + + cfg := &Config{CACertPath: path, CACertPEM: testCertPEM(t)} + pool, err := cfg.loadCACertPool() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if pool == nil { + t.Fatal("expected non-nil pool when both sources set") + } +} + +func TestSharedTransportPool_ConfigKeyDistinguishesCAAndSkipVerify(t *testing.T) { + p := GetGlobalTransportPool() + cfgSystem := DefaultHTTPClientConfig() + + cfgSkip := DefaultHTTPClientConfig() + cfgSkip.InsecureSkipVerify = true + + cfgCustomCA := DefaultHTTPClientConfig() + pool, err := (&Config{CACertPEM: testCertPEM(t)}).loadCACertPool() + if err != nil { + t.Fatalf("loadCACertPool: %v", err) + } + cfgCustomCA.RootCAs = pool + + keys := map[string]string{ + "system": p.configKey(cfgSystem), + "skip": p.configKey(cfgSkip), + "customCA": p.configKey(cfgCustomCA), + } + seen := make(map[string]string, len(keys)) + for name, key := range keys { + if dup, ok := seen[key]; ok { + t.Errorf("configKey collision: %s and %s share key %q", name, dup, key) + } + seen[key] = name + } +} diff --git a/docs/AUTH0_AUDIENCE_GUIDE.md b/docs/AUTH0_AUDIENCE_GUIDE.md index 16ecb0a..f233bbe 100644 --- a/docs/AUTH0_AUDIENCE_GUIDE.md +++ b/docs/AUTH0_AUDIENCE_GUIDE.md @@ -25,7 +25,10 @@ The **audience** (`aud`) claim in a JWT identifies the intended recipient of the ### Why Does This Matter? -Proper audience validation prevents **token confusion attacks** where a token intended for one API is used to access another API. +Audience validation rejects access tokens whose `aud` claim does not match the +expected audience, blocking the trivial form of token confusion where a token +issued for API A is presented to API B. (Defence in depth — pair with +short-lived tokens, rotation, and per-API client credentials.) --- @@ -137,8 +140,8 @@ http: **Recommended:** `true` for production **What it does:** -- When `true`: Rejects sessions if access token audience doesn't match (prevents Scenario 2) -- When `false`: Logs warnings but allows fallback to ID token (backward compatible) +- When `true`: On audience mismatch, the middleware does **not** silently fall back to ID-token validation. It tries to refresh the access token first; if no refresh token is present (or refresh fails), the user is re-authenticated. +- When `false`: Logs warnings and falls back to ID-token validation (backward compatible). **Example:** ```yaml @@ -349,7 +352,7 @@ When opaque tokens are detected: **Cache behavior:** - Cache key: Token hash -- TTL: 5 minutes or token expiry (whichever is shorter) +- TTL: 5 minutes; if the token's `exp` is sooner, the cache entry expires at `exp` instead. Tokens without `exp` use the flat 5-minute TTL. - Reduces introspection requests for frequently used tokens --- diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 10fe5d0..4298a24 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -52,7 +52,7 @@ spec: | `logoutURL` | string | `callbackURL + "/logout"` | Path for logout requests | | `postLogoutRedirectURI` | string | `/` | Redirect URL after logout | | `logLevel` | string | `info` | Logging verbosity (`debug`, `info`, `error`) | -| `forceHTTPS` | bool | `false` | Force HTTPS for redirect URIs | +| `forceHTTPS` | bool | `true` | Force HTTPS for redirect URIs (set `false` only for plaintext HTTP local dev) | | `rateLimit` | int | `100` | Maximum requests per second | | `excludedURLs` | []string | none | Paths that bypass authentication | | `revocationURL` | string | auto-discovered | Token revocation endpoint | @@ -62,13 +62,13 @@ spec: ### TLS Termination at Load Balancer -If running Traefik behind a load balancer (AWS ALB, Google Cloud LB, Azure App Gateway) that terminates TLS: +`forceHTTPS` defaults to `true`, so redirect URIs always use `https://`. This is +the correct default behind any TLS-terminating load balancer (AWS ALB, Google +Cloud LB, Azure App Gateway) — `X-Forwarded-Proto` cannot be trusted (ALB may +overwrite it). -```yaml -forceHTTPS: true # Required for correct redirect URIs -``` - -Without this setting, redirect URIs will use `http://` instead of `https://`, causing OAuth callback failures. +Set `forceHTTPS: false` only when you serve OIDC over plaintext HTTP (local +dev). Otherwise leave it at default. --- diff --git a/docs/DCR.md b/docs/DCR.md new file mode 100644 index 0000000..a3f8b13 --- /dev/null +++ b/docs/DCR.md @@ -0,0 +1,95 @@ +# Dynamic Client Registration (RFC 7591) + +The middleware can register itself with an OIDC provider at startup instead of +using a pre-provisioned `clientID` / `clientSecret`. Useful for multi-tenant +deployments, self-service integrations, and ephemeral environments. + +## How it works + +1. Middleware reads `registration_endpoint` from `.well-known/openid-configuration`. +2. If `clientID` is empty, it `POST`s `clientMetadata` to the registration endpoint. +3. Returned `client_id` / `client_secret` are cached, optionally persisted. +4. Subsequent requests use the registered credentials. + +For multi-replica deployments, set `storageBackend: redis` so all replicas +share one client and avoid registration races. + +## Configuration + +```yaml +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: oidc-dcr + namespace: traefik +spec: + plugin: + traefikoidc: + providerURL: https://your-oidc-provider.com + sessionEncryptionKey: your-secure-encryption-key-min-32-chars + callbackURL: /oauth2/callback + dynamicClientRegistration: + enabled: true + persistCredentials: true + storageBackend: redis # file | redis | auto + initialAccessToken: "" # optional, for protected endpoints + registrationEndpoint: "" # optional, override discovery + credentialsFile: /tmp/oidc-client-credentials.json + redisKeyPrefix: "dcr:creds:" + clientMetadata: + redirect_uris: + - https://app.example.com/oauth2/callback + client_name: My Application + application_type: web + grant_types: [authorization_code, refresh_token] + response_types: [code] + token_endpoint_auth_method: client_secret_basic + contacts: [admin@example.com] +``` + +## Parameters + +| Parameter | Default | Description | +|-----------|---------|-------------| +| `enabled` | `false` | Enable DCR. | +| `persistCredentials` | `false` | Save returned credentials for reuse across restarts. | +| `storageBackend` | `auto` | `file`, `redis`, or `auto` (Redis if available, else file). | +| `credentialsFile` | `/tmp/oidc-client-credentials.json` | Path for file-backed storage. Mode `0600`. | +| `redisKeyPrefix` | (none — set explicitly) | Key prefix for Redis-backed storage. The code does not inject a default; if unset, keys have no prefix. `dcr:creds:` is a sensible convention. | +| `registrationEndpoint` | discovered | Override the discovered endpoint. | +| `initialAccessToken` | none | Bearer token for protected registration endpoints. | +| `clientMetadata.redirect_uris` | required | Callback URIs for the OAuth flow. | +| `clientMetadata.client_name` | none | Human-readable client name. | +| `clientMetadata.application_type` | `web` | `web` or `native`. | +| `clientMetadata.grant_types` | `[authorization_code, refresh_token]` | OAuth grant types. | +| `clientMetadata.response_types` | `[code]` | OAuth response types. | +| `clientMetadata.token_endpoint_auth_method` | `client_secret_basic` | `client_secret_basic`, `client_secret_post`, or `none`. | +| `clientMetadata.scope` | none | Space-separated scopes. | +| `clientMetadata.contacts` | none | Admin email addresses. | +| `clientMetadata.logo_uri` | none | Logo URL for consent screens. | +| `clientMetadata.client_uri` | none | Client homepage URL. | +| `clientMetadata.policy_uri` | none | Privacy policy URL. | +| `clientMetadata.tos_uri` | none | Terms of service URL. | + +## Provider support + +The middleware does not gate DCR by provider — if the provider exposes a +`registration_endpoint` in its discovery document (or you set +`registrationEndpoint` explicitly), DCR will attempt registration. The table +below is informational guidance based on each provider's published support. + +| Provider | DCR | Notes | +|----------|-----|-------| +| Keycloak | Yes | Enable in realm settings. | +| Auth0 | Yes | Requires Management API token. | +| Okta | Yes | Enable Dynamic Client Registration in admin console. | +| Azure AD | Limited | Use App Registration API instead. | +| Google | No | Manual registration required. | +| AWS Cognito | No | Manual registration required. | + +## Security notes + +- Registration endpoints must be HTTPS (loopback excepted for local dev). +- Use `initialAccessToken` in production to gate registration. +- File-backed credentials use `0600`; protect the mount path. +- The plugin marks credentials invalid when within ~5 min of `client_secret_expires_at` but does **not** automatically re-register. If your provider sets a non-zero expiry, schedule manual rotation (delete the credentials file or Redis entry, restart) before that time. diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 92064ae..04ebeb4 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -16,9 +16,8 @@ Guide for local development, testing, and contributing to the Traefik OIDC middl ## Prerequisites -- **Go 1.23+** for plugin compilation -- **Docker & Docker Compose** for local testing -- **OIDC Provider** credentials (Google, Azure, etc.) +- **Go 1.24+** (matches `go.mod`; CI runs Go 1.24.11) +- **OIDC Provider** credentials (Google, Azure, etc.) for any end-to-end test against a real provider ### Required Development Tools @@ -40,110 +39,32 @@ go install golang.org/x/vuln/cmd/govulncheck@latest ## Local Development Setup -### Docker Compose Environment - -The repository includes a Docker Compose setup for testing the plugin locally. - -#### 1. Host Configuration - -Add to `/etc/hosts`: +### Build and unit tests ```bash -127.0.0.1 hello.localhost -127.0.0.1 traefik.localhost +go mod tidy +go build ./... +go test ./... -short # fast loop, < 30 s +go test -race -timeout=15m ./... ``` -#### 2. Plugin Configuration +### Sample plugin configurations -The plugin is loaded using Traefik's **local plugins mode**: +Working middleware/Traefik configs live in [`examples/`](../examples/): -- Plugin source: Parent directory (`../`) -- Mount path: `/plugins-local/src/github.com/lukaszraczylo/traefikoidc` -- Configuration: `experimental.localPlugins` in `traefik.yml` +- `complete-traefik-config.yaml` — full middleware example +- `redis-config.yaml` — Redis cache configuration -#### 3. OIDC Provider Setup +To run the plugin against a real Traefik instance, drop the project on disk +and load it via `experimental.localPlugins` in your Traefik static config — +see the [README install section](../README.md#install). -Edit `docker/dynamic.yml` with your provider details: +### Integration tests -**Google:** -```yaml -http: - middlewares: - oidc-auth: - plugin: - traefikoidc: - providerURL: "https://accounts.google.com" - clientID: "your-client-id.apps.googleusercontent.com" - clientSecret: "your-google-client-secret" - sessionEncryptionKey: "your-32-character-encryption-key" - callbackURL: "/oauth2/callback" - logoutURL: "/oauth2/logout" - scopes: - - "openid" - - "email" - - "profile" -``` - -**Azure AD:** -```yaml -http: - middlewares: - oidc-auth: - plugin: - traefikoidc: - providerURL: "https://login.microsoftonline.com/your-tenant-id/v2.0" - clientID: "your-azure-client-id" - clientSecret: "your-azure-client-secret" - sessionEncryptionKey: "your-32-character-encryption-key" - callbackURL: "/oauth2/callback" - scopes: - - "openid" - - "email" - - "profile" -``` - -#### 4. Start Environment +Integration tests live in `integration/`. Run them explicitly: ```bash -cd docker -docker-compose up -d -``` - -#### 5. Test Plugin - -- **Protected App**: http://hello.localhost (redirects to OIDC) -- **Traefik Dashboard**: http://traefik.localhost:8080 - -### Development Workflow - -1. **Edit plugin code** in the project root -2. **Build and test** (optional syntax check): - ```bash - go mod tidy - go build . - go test ./... - ``` -3. **Restart Traefik** to reload plugin: - ```bash - docker-compose restart traefik - ``` -4. **Test changes** at http://hello.localhost - -### Debugging - -**View plugin logs:** -```bash -docker-compose logs -f traefik | grep traefikoidc -``` - -**Check plugin loading:** -```bash -docker-compose logs traefik | grep -i plugin -``` - -**Verify plugin directory:** -```bash -docker-compose exec traefik ls -la /plugins-local/src/github.com/lukaszraczylo/traefikoidc/ +go test ./integration/... -run Integration -v ``` --- @@ -299,7 +220,7 @@ The repository uses GitHub Actions for comprehensive validation with 20+ paralle #### Testing (9 suites) - Race Detector -- Coverage (75% threshold) +- Coverage (70% threshold, enforced in `pr.yaml`) - Memory Leaks - Integration Tests - Regression Tests @@ -323,13 +244,13 @@ Tests run in parallel for: #### Performance & Build (3 checks) - Benchmarks - Multi-platform Build (linux/darwin x amd64/arm64) -- Go Version Compatibility (Go 1.23 & 1.24) +- Go Version Compatibility (currently Go 1.24.11 in CI) ### Quality Gates All PRs must pass: - All parallel checks -- 75% test coverage minimum +- 70% test coverage minimum - Zero security vulnerabilities - No race conditions - No memory leaks diff --git a/docs/PROVIDERS.md b/docs/PROVIDERS.md index 8db9d82..0e4602b 100644 --- a/docs/PROVIDERS.md +++ b/docs/PROVIDERS.md @@ -23,10 +23,10 @@ Configuration reference for each supported OIDC provider. | Provider | OIDC Support | Refresh Tokens | Auto-Detection | ID Tokens | |----------|-------------|----------------|----------------|-----------| | Google | Full | Yes | `accounts.google.com` | Yes | -| Azure AD | Full | Yes | `login.microsoftonline.com` | Yes | +| Azure AD | Full | Yes | `login.microsoftonline.com`, `sts.windows.net` | Yes | | Auth0 | Full | Yes | `*.auth0.com` | Yes | -| Okta | Full | Yes | `*.okta.com` | Yes | -| Keycloak | Full | Yes | `/auth/realms/` path | Yes | +| Okta | Full | Yes | `*.okta.com`, `*.oktapreview.com`, `*.okta-emea.com` | Yes | +| Keycloak | Full | Yes | host containing `keycloak`, or `/realms/` in path (matches both `/auth/realms/` legacy and `/realms/` modern) | Yes | | AWS Cognito | Full | Yes | `cognito-idp.*.amazonaws.com` | Yes | | GitLab | Full | Yes | `gitlab.com` | Yes | | GitHub | OAuth 2.0 Only | No | `github.com` | No | diff --git a/docs/REDIS.md b/docs/REDIS.md index 434547b..243c38c 100644 --- a/docs/REDIS.md +++ b/docs/REDIS.md @@ -109,11 +109,11 @@ redis: | `writeTimeout` | int | `3` | Write timeout (seconds) | | `enableTLS` | bool | `false` | Enable TLS for connections | | `tlsSkipVerify` | bool | `false` | Skip TLS certificate verification | -| `enableCircuitBreaker` | bool | `true` | Enable circuit breaker | -| `circuitBreakerThreshold` | int | `5` | Failures before circuit opens | -| `circuitBreakerTimeout` | int | `60` | Circuit reset timeout (seconds) | -| `enableHealthCheck` | bool | `true` | Enable periodic health checks | -| `healthCheckInterval` | int | `30` | Health check interval (seconds) | +| `enableCircuitBreaker` | bool | `false` | Wrap the Redis backend with a circuit breaker. **Recommended `true` in production.** | +| `circuitBreakerThreshold` | int | `5` | Consecutive failures before the circuit opens (only when `enableCircuitBreaker: true`). | +| `circuitBreakerTimeout` | int | `60` | Seconds the circuit stays open before allowing a probe (only when `enableCircuitBreaker: true`). | +| `enableHealthCheck` | bool | `false` | Wrap the Redis backend with periodic health checks. **Recommended `true` in production.** | +| `healthCheckInterval` | int | `30` | Health check interval in seconds (only when `enableHealthCheck: true`). | | `hybridL1Size` | int | `500` | Max items in L1 cache (hybrid mode) | | `hybridL1MemoryMB` | int64 | `10` | Max memory for L1 cache in MB | @@ -134,13 +134,21 @@ REDIS_READ_TIMEOUT=3 REDIS_WRITE_TIMEOUT=3 REDIS_ENABLE_TLS=false REDIS_TLS_SKIP_VERIFY=false +REDIS_HYBRID_L1_SIZE=500 +REDIS_HYBRID_L1_MEMORY_MB=10 ``` +> Resilience fields (`enableCircuitBreaker`, `enableHealthCheck`, +> `circuitBreakerThreshold`, `circuitBreakerTimeout`, `healthCheckInterval`) +> have no environment variable fallback — set them in plugin configuration. + +Invalid `cacheMode` values are rejected at plugin startup. + --- ## Cache Modes -### Memory Mode (Default without Redis) +### Memory Mode (used when Redis is disabled) ```yaml redis: diff --git a/docs/TESTING.md b/docs/TESTING.md index dd59e3d..f04c371 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -6,8 +6,8 @@ Comprehensive testing infrastructure for traefikoidc. | Metric | Value | |--------|-------| -| Test files | 99 | -| Lines of test code | ~65,500 | +| Test files | 110 | +| Lines of test code | ~72,000 | | Code coverage | 71.0% | | Race conditions | None (all pass with `-race`) | diff --git a/go.mod b/go.mod index e718543..3c2d9c1 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,6 @@ go 1.24.0 require ( github.com/alicebob/miniredis/v2 v2.35.0 - github.com/google/uuid v1.6.0 github.com/gorilla/sessions v1.3.0 github.com/redis/go-redis/v9 v9.17.2 github.com/stretchr/testify v1.10.0 diff --git a/go.sum b/go.sum index a246f92..c4c1ed6 100644 --- a/go.sum +++ b/go.sum @@ -12,8 +12,6 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/gorilla/sessions v1.3.0 h1:XYlkq7KcpOB2ZhHBPv5WpjMIxrQosiZanfoy1HLZFzg= diff --git a/helpers.go b/helpers.go index 7a8b407..300d5e5 100644 --- a/helpers.go +++ b/helpers.go @@ -17,6 +17,21 @@ import ( "github.com/lukaszraczylo/traefikoidc/internal/utils" ) +// newUUIDv4 returns an RFC 4122 v4 UUID string (e.g. +// "f47ac10b-58cc-4372-a567-0e02b2c3d479") backed by crypto/rand. Used for CSRF +// tokens and other opaque random identifiers — replaces github.com/google/uuid +// to keep the plugin stdlib-only on the production path. +func newUUIDv4() (string, error) { + var b [16]byte + if _, err := rand.Read(b[:]); err != nil { + return "", fmt.Errorf("could not generate UUID: %w", err) + } + b[6] = (b[6] & 0x0f) | 0x40 // version 4 + b[8] = (b[8] & 0x3f) | 0x80 // RFC 4122 variant + return fmt.Sprintf("%08x-%04x-%04x-%04x-%012x", + b[0:4], b[4:6], b[6:8], b[8:10], b[10:16]), nil +} + // generateNonce creates a cryptographically secure random nonce for OIDC flows. // The nonce is used to prevent replay attacks and associate client sessions with ID tokens. // Returns: diff --git a/helpers_test.go b/helpers_test.go new file mode 100644 index 0000000..66a250f --- /dev/null +++ b/helpers_test.go @@ -0,0 +1,29 @@ +package traefikoidc + +import ( + "regexp" + "testing" +) + +// TestNewUUIDv4 verifies the in-house UUID v4 generator produces RFC 4122 +// compliant identifiers. Locks in the replacement for github.com/google/uuid +// — a regression here would weaken the CSRF token used in the OIDC flow. +func TestNewUUIDv4(t *testing.T) { + rfc4122v4 := regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$`) + + const samples = 1000 + seen := make(map[string]struct{}, samples) + for i := 0; i < samples; i++ { + got, err := newUUIDv4() + if err != nil { + t.Fatalf("newUUIDv4 failed: %v", err) + } + if !rfc4122v4.MatchString(got) { + t.Fatalf("UUID %q does not match RFC 4122 v4 format", got) + } + if _, dup := seen[got]; dup { + t.Fatalf("duplicate UUID emitted within %d samples: %q", samples, got) + } + seen[got] = struct{}{} + } +} diff --git a/http_client_factory.go b/http_client_factory.go index 9dcc758..ea67792 100644 --- a/http_client_factory.go +++ b/http_client_factory.go @@ -3,6 +3,7 @@ package traefikoidc import ( "context" "crypto/tls" + "crypto/x509" "fmt" "net" "net/http" @@ -25,10 +26,16 @@ type HTTPClientConfig struct { Timeout time.Duration MaxConnsPerHost int WriteBufferSize int - UseCookieJar bool - ForceHTTP2 bool - DisableKeepAlives bool - DisableCompression bool + // RootCAs is an optional certificate pool used for TLS verification. + // A nil pool means "use the system trust store" (default behavior). + RootCAs *x509.CertPool + // InsecureSkipVerify disables TLS certificate verification. + // ONLY set this for local development against self-signed certificates. + InsecureSkipVerify bool + UseCookieJar bool + ForceHTTP2 bool + DisableKeepAlives bool + DisableCompression bool } // DefaultHTTPClientConfig returns the default configuration for general use @@ -203,7 +210,8 @@ func (f *HTTPClientFactory) CreateHTTPClient(config HTTPClientConfig) *http.Clie tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, }, PreferServerCipherSuites: true, - InsecureSkipVerify: false, // Always verify certificates + RootCAs: config.RootCAs, + InsecureSkipVerify: config.InsecureSkipVerify, //nolint:gosec // opt-in, loud warning emitted at plugin startup }, ForceAttemptHTTP2: config.ForceHTTP2, TLSHandshakeTimeout: config.TLSHandshakeTimeout, diff --git a/http_client_pool.go b/http_client_pool.go index 6352e3c..020981d 100644 --- a/http_client_pool.go +++ b/http_client_pool.go @@ -3,6 +3,7 @@ package traefikoidc import ( "context" "crypto/tls" + "fmt" "net" "net/http" "sync" @@ -103,7 +104,8 @@ func (p *SharedTransportPool) GetOrCreateTransport(config HTTPClientConfig) *htt tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, }, PreferServerCipherSuites: true, - InsecureSkipVerify: false, + RootCAs: config.RootCAs, + InsecureSkipVerify: config.InsecureSkipVerify, //nolint:gosec // opt-in, loud warning emitted at plugin startup }, ForceAttemptHTTP2: config.ForceHTTP2, TLSHandshakeTimeout: config.TLSHandshakeTimeout, @@ -205,8 +207,21 @@ func (p *SharedTransportPool) performCleanup() { // configKey generates a unique key for a config func (p *SharedTransportPool) configKey(config HTTPClientConfig) string { - // Simple key based on main parameters - return string(rune(config.MaxConnsPerHost)) + string(rune(config.MaxIdleConnsPerHost)) + // Pool transports by the parameters that change TLS or connection + // behavior. RootCAs and InsecureSkipVerify MUST be part of the key: + // otherwise a middleware configured with a custom CA would share a + // transport with one using the system store, silently bypassing its + // CA configuration. + skip := "0" + if config.InsecureSkipVerify { + skip = "1" + } + return fmt.Sprintf("%d|%d|%p|%s", + config.MaxConnsPerHost, + config.MaxIdleConnsPerHost, + config.RootCAs, + skip, + ) } // Cleanup closes all transports and stops the cleanup goroutine diff --git a/internal/cache/backends/redis_pool_test.go b/internal/cache/backends/redis_pool_test.go index 4ac2344..bf8431c 100644 --- a/internal/cache/backends/redis_pool_test.go +++ b/internal/cache/backends/redis_pool_test.go @@ -3,6 +3,7 @@ package backends import ( "context" "errors" + "strings" "sync" "testing" "time" @@ -617,4 +618,33 @@ func TestRedisConn_TooManyArguments(t *testing.T) { assert.NotContains(t, err.Error(), "too many arguments") } }) + +} + +// TestRedisConn_RejectOversizedArgumentBytes is a regression test for CodeQL +// alert #10 (go/allocation-size-overflow). A single argument larger than +// maxTotalArgBytes (64 MiB) must be rejected by the per-argument overflow +// guard in Do() before any allocation is attempted. +func TestRedisConn_RejectOversizedArgumentBytes(t *testing.T) { + mr := NewMiniredisServer(t) + + pool, err := NewConnectionPool(&PoolConfig{ + Address: mr.GetAddr(), + MaxConnections: 1, + ConnectTimeout: 5 * time.Second, + ReadTimeout: 3 * time.Second, + WriteTimeout: 3 * time.Second, + }) + require.NoError(t, err) + defer pool.Close() + + conn, err := pool.Get(context.Background()) + require.NoError(t, err) + defer pool.Put(conn) + + largeArg := strings.Repeat("x", (64<<20)+1) + + _, err = conn.Do("SET", "k", largeArg) + require.Error(t, err) + assert.Contains(t, err.Error(), "arguments too large") } diff --git a/internal/providers/registry.go b/internal/providers/registry.go index 5dd8dd5..fed57db 100644 --- a/internal/providers/registry.go +++ b/internal/providers/registry.go @@ -147,7 +147,8 @@ func (r *ProviderRegistry) detectProviderUnsafe(issuerURL string) OIDCProvider { return p } case ProviderTypeKeycloak: - if strings.Contains(host, "keycloak") || strings.Contains(normalizedURL.Path, "/auth/realms/") { + // Match both Keycloak <17 (`/auth/realms/`) and 17+ (`/realms/`). + if strings.Contains(host, "keycloak") || strings.Contains(normalizedURL.Path, "/realms/") { return p } case ProviderTypeAWSCognito: diff --git a/internal/providers/registry_test.go b/internal/providers/registry_test.go index 06fcb0d..6a354a2 100644 --- a/internal/providers/registry_test.go +++ b/internal/providers/registry_test.go @@ -225,10 +225,15 @@ func TestProviderRegistry_DetectProvider(t *testing.T) { expected: oktaProvider, }, { - name: "Keycloak provider detection", + name: "Keycloak provider detection (legacy /auth/realms/)", issuerURL: "https://auth.example.com/auth/realms/master", expected: keycloakProvider, }, + { + name: "Keycloak provider detection (modern /realms/, KC 17+)", + issuerURL: "https://auth.example.com/realms/master", + expected: keycloakProvider, + }, { name: "AWS Cognito provider detection", issuerURL: "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_example", diff --git a/main.go b/main.go index 654da31..9086980 100644 --- a/main.go +++ b/main.go @@ -113,12 +113,26 @@ func NewWithContext(ctx context.Context, config *Config, next http.Handler, name } } // Setup HTTP client + caPool, err := config.loadCACertPool() + if err != nil { + return nil, fmt.Errorf("failed to load CA certificates: %w", err) + } + if config.InsecureSkipVerify { + logger.Errorf("SECURITY WARNING: InsecureSkipVerify is enabled for the OIDC provider. TLS certificate verification is DISABLED. Do not use in production.") + } var httpClient *http.Client if config.HTTPClient != nil { httpClient = config.HTTPClient } else { - httpClient = CreateDefaultHTTPClient() + defaultCfg := DefaultHTTPClientConfig() + defaultCfg.RootCAs = caPool + defaultCfg.InsecureSkipVerify = config.InsecureSkipVerify + httpClient = CreatePooledHTTPClient(defaultCfg) } + tokenCfg := TokenHTTPClientConfig() + tokenCfg.RootCAs = caPool + tokenCfg.InsecureSkipVerify = config.InsecureSkipVerify + tokenHTTPClient := CreatePooledHTTPClient(tokenCfg) goroutineWG := &sync.WaitGroup{} cacheManager := GetGlobalCacheManagerWithConfig(goroutineWG, config) @@ -199,7 +213,7 @@ func NewWithContext(ctx context.Context, config *Config, next http.Handler, name limiter: rate.NewLimiter(rate.Every(time.Second), config.RateLimit), tokenCache: cacheManager.GetSharedTokenCache(), httpClient: httpClient, - tokenHTTPClient: CreateTokenHTTPClient(), + tokenHTTPClient: tokenHTTPClient, excludedURLs: createStringMap(config.ExcludedURLs), allowedUserDomains: createStringMap(config.AllowedUserDomains), allowedUsers: createCaseInsensitiveStringMap(config.AllowedUsers), @@ -293,13 +307,12 @@ func NewWithContext(ctx context.Context, config *Config, next http.Handler, name startReplayCacheCleanup(pluginCtx, logger) - // Start memory monitoring for leak detection and performance insights + // Start memory monitoring for leak detection and performance insights. + // The interval is clamped to MinMemoryMonitorInterval (30s) inside + // StartMonitoring; tests that need deterministic sampling should call + // MemoryMonitor.Refresh() directly instead of waiting on a fast ticker. memoryMonitor := GetGlobalMemoryMonitor() - monitorInterval := 60 * time.Second - if isTestMode() { - monitorInterval = 100 * time.Millisecond // Fast interval for tests - } - memoryMonitor.StartMonitoring(pluginCtx, monitorInterval) + memoryMonitor.StartMonitoring(pluginCtx, DefaultMemoryMonitorInterval) logger.Debug("Started global memory monitoring") logger.Debugf("TraefikOidc.New: Final t.scopes initialized to: %v", t.scopes) diff --git a/memory_leak_fixes.go b/memory_leak_fixes.go index 49d8d18..be54d2e 100644 --- a/memory_leak_fixes.go +++ b/memory_leak_fixes.go @@ -9,13 +9,18 @@ import ( // LazyBackgroundTask wraps BackgroundTask to provide delayed initialization. // This prevents memory leaks from unnecessary background tasks by starting // them only when actually needed, reducing resource usage in idle scenarios. +// +// Lifecycle is one-shot: once Stop has been called the task cannot be +// restarted. The underlying BackgroundTask uses sync.Once for Start and +// refuses to re-run after Stop, so restart is not supported by design. type LazyBackgroundTask struct { // BackgroundTask is the underlying task implementation *BackgroundTask - // started tracks whether the task has been activated + // mu guards the started flag against concurrent StartIfNeeded / Stop calls. + mu sync.Mutex + // started tracks whether the task has been activated. + // Only mutated while holding mu. started bool - // startOnce ensures single initialization - startOnce sync.Once } // NewLazyBackgroundTask creates a background task that doesn't start immediately. @@ -29,24 +34,28 @@ func NewLazyBackgroundTask(name string, interval time.Duration, taskFunc func(), } // StartIfNeeded starts the background task only if it hasn't been started yet. -// Uses sync.Once to ensure thread-safe single initialization. +// Safe to call concurrently. After Stop has been called this is a no-op; +// the task is not restartable. func (lt *LazyBackgroundTask) StartIfNeeded() { - lt.startOnce.Do(func() { - if !lt.started { - lt.BackgroundTask.Start() - lt.started = true - } - }) + lt.mu.Lock() + defer lt.mu.Unlock() + if lt.started { + return + } + lt.BackgroundTask.Start() + lt.started = true } // Stop stops the background task if it was started. -// Resets the start state to allow potential future re-initialization. +// Once stopped, the task cannot be restarted (see type doc). func (lt *LazyBackgroundTask) Stop() { - if lt.started { - lt.BackgroundTask.Stop() - lt.started = false - lt.startOnce = sync.Once{} + lt.mu.Lock() + defer lt.mu.Unlock() + if !lt.started { + return } + lt.BackgroundTask.Stop() + lt.started = false } // NewLazyCacheWithLogger creates a cache that doesn't start cleanup until first use. diff --git a/memory_monitor.go b/memory_monitor.go index 036e5f4..5ec4276 100644 --- a/memory_monitor.go +++ b/memory_monitor.go @@ -58,13 +58,21 @@ func (mpl MemoryPressureLevel) String() string { } } -// MemoryMonitor provides comprehensive memory monitoring and alerting +// MemoryMonitor provides comprehensive memory monitoring and alerting. +// +// Memory sampling is expensive: runtime.ReadMemStats is a stop-the-world +// operation. To keep latency predictable the monitor caches the most recent +// sample and only refreshes it when the background ticker fires, when TriggerGC +// is invoked, or when a caller explicitly calls Refresh(). GetCurrentStats is a +// cheap read of that cached sample. type MemoryMonitor struct { lastGCTime time.Time startTime time.Time lastStats *MemoryStats + cachedMemStats runtime.MemStats logger *Logger alertThresholds MemoryAlertThresholds + config MemoryMonitorConfig baselineGoroutines int baselineHeap uint64 heapGrowthRate float64 @@ -84,6 +92,30 @@ type MemoryAlertThresholds struct { GCFrequency float64 // Alert when GC frequency exceeds this per minute } +// MemoryMonitorConfig configures the memory monitor's scheduling behavior. +// Thresholds are kept separate in MemoryAlertThresholds. +type MemoryMonitorConfig struct { + // Interval between background samples. Must be >= MinMemoryMonitorInterval + // (30s). Values below the minimum are clamped when monitoring starts. + Interval time.Duration +} + +// Default and minimum interval values. The minimum exists because +// runtime.ReadMemStats is stop-the-world and hammering it on a hot loop causes +// noticeable latency spikes, especially under yaegi. +const ( + DefaultMemoryMonitorInterval = 60 * time.Second + MinMemoryMonitorInterval = 30 * time.Second +) + +// DefaultMemoryMonitorConfig returns a config with sensible production +// defaults. +func DefaultMemoryMonitorConfig() MemoryMonitorConfig { + return MemoryMonitorConfig{ + Interval: DefaultMemoryMonitorInterval, + } +} + // DefaultMemoryAlertThresholds returns sensible default alert thresholds func DefaultMemoryAlertThresholds() MemoryAlertThresholds { return MemoryAlertThresholds{ @@ -95,35 +127,82 @@ func DefaultMemoryAlertThresholds() MemoryAlertThresholds { } } -// NewMemoryMonitor creates a new memory monitor +// NewMemoryMonitor creates a new memory monitor using default scheduling +// configuration. See NewMemoryMonitorWithConfig for full control. func NewMemoryMonitor(logger *Logger, thresholds MemoryAlertThresholds) *MemoryMonitor { + return NewMemoryMonitorWithConfig(logger, thresholds, DefaultMemoryMonitorConfig()) +} + +// NewMemoryMonitorWithConfig creates a new memory monitor with an explicit +// scheduling config. +// +// NOTE: the constructor performs a single runtime.ReadMemStats call to capture +// baseline heap / goroutine / GC counters used for leak and growth detection. +// This is a one-time stop-the-world cost at startup; all subsequent samples +// only happen on the monitoring ticker or on explicit Refresh() calls. +func NewMemoryMonitorWithConfig(logger *Logger, thresholds MemoryAlertThresholds, config MemoryMonitorConfig) *MemoryMonitor { if logger == nil { logger = GetSingletonNoOpLogger() } + if config.Interval <= 0 { + config.Interval = DefaultMemoryMonitorInterval + } + + // One-time initial sample to seed baselines used for growth / leak + // detection. All subsequent sampling is gated by the monitoring ticker or + // explicit Refresh() calls. var memStats runtime.MemStats runtime.ReadMemStats(&memStats) - return &MemoryMonitor{ + mm := &MemoryMonitor{ logger: logger, startTime: time.Now(), alertThresholds: thresholds, + config: config, baselineHeap: memStats.HeapAlloc, baselineGoroutines: runtime.NumGoroutine(), // #nosec G115 -- LastGC nanoseconds fits in int64 for centuries lastGCTime: time.Unix(0, int64(memStats.LastGC)), lastGCCount: memStats.NumGC, } + mm.cachedMemStats = memStats + return mm } -// GetCurrentStats collects current memory statistics +// GetCurrentStats returns the most recently sampled memory statistics. +// +// This is a cheap cached read: it does NOT call runtime.ReadMemStats. Samples +// are refreshed only by the monitoring ticker or by an explicit call to +// Refresh(). If no sample has been produced yet, stats derived from the +// constructor-time raw sample are returned (with no additional STW cost). func (mm *MemoryMonitor) GetCurrentStats() *MemoryStats { + mm.mu.RLock() + stats := mm.lastStats + mm.mu.RUnlock() + if stats != nil { + return stats + } + return mm.buildStatsFromCache() +} + +// Refresh synchronously samples current memory statistics via +// runtime.ReadMemStats and updates the cached value. This is the only path +// (other than the monitoring ticker and TriggerGC) that pays the stop-the-world +// cost. Use it in tests or in callers that explicitly need a fresh sample. +func (mm *MemoryMonitor) Refresh() *MemoryStats { + return mm.sample() +} + +// sample performs a stop-the-world ReadMemStats, updates the cached raw stats, +// computes a derived MemoryStats snapshot, and stores it as lastStats. +func (mm *MemoryMonitor) sample() *MemoryStats { var memStats runtime.MemStats runtime.ReadMemStats(&memStats) now := time.Now() - // Calculate GC frequency + // Calculate GC frequency relative to the previous snapshot. gcFrequency := 0.0 mm.mu.RLock() lastStats := mm.lastStats @@ -168,6 +247,7 @@ func (mm *MemoryMonitor) GetCurrentStats() *MemoryStats { mm.updateHeapGrowthTracking(stats) mm.mu.Lock() + mm.cachedMemStats = memStats mm.lastStats = stats mm.lastGCCount = memStats.NumGC mm.mu.Unlock() @@ -175,6 +255,35 @@ func (mm *MemoryMonitor) GetCurrentStats() *MemoryStats { return stats } +// buildStatsFromCache constructs a MemoryStats snapshot from the cached raw +// runtime.MemStats without issuing a new ReadMemStats call. Used as a fallback +// when GetCurrentStats is called before the first sample() has completed. +func (mm *MemoryMonitor) buildStatsFromCache() *MemoryStats { + mm.mu.RLock() + memStats := mm.cachedMemStats + mm.mu.RUnlock() + + stats := &MemoryStats{ + HeapAllocBytes: memStats.HeapAlloc, + HeapSysBytes: memStats.HeapSys, + HeapIdleBytes: memStats.HeapIdle, + HeapInuseBytes: memStats.HeapInuse, + HeapReleasedBytes: memStats.HeapReleased, + HeapObjects: memStats.HeapObjects, + StackInuseBytes: memStats.StackInuse, + StackSysBytes: memStats.StackSys, + GCSysBytes: memStats.GCSys, + NumGoroutines: runtime.NumGoroutine(), + // #nosec G115 -- LastGC nanoseconds fits in int64 for centuries + LastGCTime: time.Unix(0, int64(memStats.LastGC)), + GCFrequency: 0.0, + Timestamp: time.Now(), + } + mm.collectApplicationStats(stats) + stats.MemoryPressure = mm.calculateMemoryPressure(stats) + return stats +} + // collectApplicationStats gathers application-specific memory stats func (mm *MemoryMonitor) collectApplicationStats(stats *MemoryStats) { // Get session count from ChunkManager if available @@ -302,7 +411,16 @@ var ( globalMonitoringMutex sync.Mutex ) -// StartMonitoring starts continuous memory monitoring as a global singleton +// StartMonitoring starts continuous memory monitoring as a global singleton. +// +// The effective interval is resolved as follows: +// 1. If the caller passes a positive interval, that is used. +// 2. Otherwise the configured MemoryMonitorConfig.Interval is used. +// 3. Otherwise the built-in default (60s) is used. +// +// The result is then clamped to a minimum of MinMemoryMonitorInterval (30s) to +// avoid stop-the-world ReadMemStats storms. Callers that need rapid updates in +// tests should call Refresh() directly instead of spinning the ticker fast. func (mm *MemoryMonitor) StartMonitoring(ctx context.Context, interval time.Duration) { globalMonitoringMutex.Lock() defer globalMonitoringMutex.Unlock() @@ -316,7 +434,17 @@ func (mm *MemoryMonitor) StartMonitoring(ctx context.Context, interval time.Dura } if interval <= 0 { - interval = 30 * time.Second + interval = mm.config.Interval + } + if interval <= 0 { + interval = DefaultMemoryMonitorInterval + } + if interval < MinMemoryMonitorInterval { + if !isTestMode() { + mm.logger.Debug("Memory monitor interval %v is below minimum %v; clamping", + interval, MinMemoryMonitorInterval) + } + interval = MinMemoryMonitorInterval } registry := GetGlobalTaskRegistry() @@ -325,7 +453,7 @@ func (mm *MemoryMonitor) StartMonitoring(ctx context.Context, interval time.Dura "memory-monitor", interval, func() { - stats := mm.GetCurrentStats() + stats := mm.sample() mm.LogMemoryStats(stats) mm.checkAlerts(stats) }, @@ -369,14 +497,16 @@ func (mm *MemoryMonitor) checkAlerts(stats *MemoryStats) { } } -// TriggerGC forces garbage collection and logs the impact +// TriggerGC forces garbage collection and logs the impact. Both the before and +// after measurements are fresh samples (explicit Refresh() calls) because the +// comparison is meaningless against a stale cached snapshot. func (mm *MemoryMonitor) TriggerGC() { - before := mm.GetCurrentStats() + before := mm.Refresh() runtime.GC() runtime.GC() // Run twice to ensure full collection - after := mm.GetCurrentStats() + after := mm.Refresh() // #nosec G115 -- heap allocation bytes fit in int64 for practical purposes freedBytes := int64(before.HeapAllocBytes) - int64(after.HeapAllocBytes) diff --git a/middleware.go b/middleware.go index b7e3251..7ee012d 100644 --- a/middleware.go +++ b/middleware.go @@ -13,6 +13,63 @@ import ( "github.com/lukaszraczylo/traefikoidc/internal/utils" ) +// bypassReason describes why a request is being forwarded without OIDC auth. +// It is only used for logging and to decide whether extra SSE-specific work +// (propagating the user header from an existing session) should run. +const ( + bypassReasonExcluded = "excluded-url" + bypassReasonSSE = "sse" +) + +// shouldBypassAuth decides whether a request must skip OIDC authentication +// entirely. It returns (true, reason) when either the request path matches a +// configured excluded URL or the Accept header asks for a text/event-stream +// response (SSE). The reason lets ServeHTTP apply any side-effects that are +// unique to the bypass kind (e.g. propagating user headers for SSE). +// +// This must be called BEFORE waiting on t.initComplete so excluded and SSE +// traffic is never blocked by a slow/broken provider. +func (t *TraefikOidc) shouldBypassAuth(req *http.Request) (bool, string) { + if t.determineExcludedURL(req.URL.Path) { + return true, bypassReasonExcluded + } + if strings.Contains(req.Header.Get("Accept"), "text/event-stream") { + return true, bypassReasonSSE + } + return false, "" +} + +// applySSEUserHeaders attempts to copy the authenticated user's identity from +// an existing session onto the outgoing SSE request so downstream services +// can still see who the user is. Failures are logged (not silenced) because +// they indicate either a corrupt cookie or a misconfigured session manager +// and are useful for debugging, but they never block the bypass itself. +func (t *TraefikOidc) applySSEUserHeaders(req *http.Request) { + if t.sessionManager == nil { + return + } + + session, err := t.sessionManager.GetSession(req) + if err != nil { + // Intentionally not fatal: SSE requests bypass auth, we just lose the + // forwarded-user header for this request. + t.logger.Debugf("SSE bypass: unable to load session for user header propagation: %v", err) + return + } + defer session.returnToPoolSafely() + + email := session.GetEmail() + if email == "" { + return + } + + req.Header.Set("X-Forwarded-User", email) + if !t.minimalHeaders { + req.Header.Set("X-Auth-Request-User", email) + } + t.logger.Debugf("SSE bypass: forwarded user %s from session", email) +} + // ServeHTTP implements the main middleware logic for processing HTTP requests. // It handles the complete OIDC authentication flow including: // - Excluded URL bypass @@ -67,17 +124,15 @@ func (t *TraefikOidc) ServeHTTP(rw http.ResponseWriter, req *http.Request) { t.firstRequestMutex.Unlock() } - // Check excluded URLs before waiting for initialization - if t.determineExcludedURL(req.URL.Path) { - t.logger.Debugf("Request path %s excluded by configuration, bypassing OIDC", req.URL.Path) - t.next.ServeHTTP(rw, req) - return - } - - // Check for SSE requests before waiting for initialization - acceptHeader := req.Header.Get("Accept") - if strings.Contains(acceptHeader, "text/event-stream") { - t.logger.Debugf("Request accepts text/event-stream (%s), bypassing OIDC", acceptHeader) + // Evaluate auth-bypass once, before waiting for initialization. Excluded URLs + // and SSE requests must not block on provider init. For SSE we additionally + // attempt to forward the user identity from an existing session (best + // effort) so downstream handlers still see X-Forwarded-User. + if bypass, reason := t.shouldBypassAuth(req); bypass { + t.logger.Debugf("Bypassing OIDC for %s (%s)", req.URL.Path, reason) + if reason == bypassReasonSSE { + t.applySSEUserHeaders(req) + } t.next.ServeHTTP(rw, req) return } @@ -85,6 +140,11 @@ func (t *TraefikOidc) ServeHTTP(rw http.ResponseWriter, req *http.Request) { // Log waiting for initialization to help diagnose hanging requests t.logger.Debug("Waiting for OIDC provider initialization...") + // time.NewTimer + Stop avoids leaking a goroutine+channel for 30s on every + // request when initComplete fires quickly (would happen with time.After). + initTimer := time.NewTimer(30 * time.Second) + defer initTimer.Stop() + select { case <-t.initComplete: // Read issuerURL with RLock @@ -115,34 +175,13 @@ func (t *TraefikOidc) ServeHTTP(rw http.ResponseWriter, req *http.Request) { t.logger.Debug("Request canceled while waiting for OIDC initialization") t.sendErrorResponse(rw, req, "Request canceled", http.StatusRequestTimeout) return - case <-time.After(30 * time.Second): + case <-initTimer.C: t.logger.Error("Timeout waiting for OIDC initialization") t.sendErrorResponse(rw, req, "Timeout waiting for OIDC provider initialization - please try again later", http.StatusServiceUnavailable) return } - if t.determineExcludedURL(req.URL.Path) { - t.logger.Debugf("Request path %s excluded by configuration, bypassing OIDC", req.URL.Path) - t.next.ServeHTTP(rw, req) - return - } - acceptHeader = req.Header.Get("Accept") - if strings.Contains(acceptHeader, "text/event-stream") { - t.logger.Debugf("Request accepts text/event-stream (%s), bypassing OIDC", acceptHeader) - // Set forwarded user headers from existing session before bypassing - if session, err := t.sessionManager.GetSession(req); err == nil { - defer session.returnToPoolSafely() - if email := session.GetEmail(); email != "" { - req.Header.Set("X-Forwarded-User", email) - if !t.minimalHeaders { - req.Header.Set("X-Auth-Request-User", email) - } - t.logger.Debugf("SSE bypass: forwarded user %s from session", email) - } - } - t.next.ServeHTTP(rw, req) - return - } + // Bypass checks already ran before the init wait; no need to repeat them. t.sessionManager.CleanupOldCookies(rw, req) session, err := t.sessionManager.GetSession(req) @@ -160,6 +199,14 @@ func (t *TraefikOidc) ServeHTTP(rw http.ResponseWriter, req *http.Request) { t.sendErrorResponse(rw, req, "Critical session error", http.StatusInternalServerError) return } + // Sub-resource requests (script/image/fetch/serviceWorker) must not + // trigger an OIDC redirect from this path either: they would overwrite + // any in-flight CSRF/nonce in the session. Let the next HTML navigation + // initiate the flow. See issue #129. + if t.isAjaxRequest(req) || t.isNonNavigationRequest(req) { + t.sendErrorResponse(rw, req, "Authentication required", http.StatusUnauthorized) + return + } scheme := utils.DetermineScheme(req, t.forceHTTPS) host := utils.DetermineHost(req) redirectURL := buildFullURL(scheme, host, t.redirURLPath) @@ -213,8 +260,12 @@ func (t *TraefikOidc) ServeHTTP(rw http.ResponseWriter, req *http.Request) { refreshTokenPresent := session.GetRefreshToken() != "" - // Check if this is an AJAX request that should receive 401 instead of redirect - isAjaxRequest := t.isAjaxRequest(req) + // Decide whether to answer with 401 instead of a redirect. AJAX requests + // cannot follow a 302 into an IdP, and sub-resource loads (script/image/ + // fetch/serviceWorker) must not trigger a fresh OIDC flow because parallel + // loads would each overwrite the session CSRF/nonce (issue #129). Only + // top-level HTML navigations should redirect. + isAjaxRequest := t.isAjaxRequest(req) || t.isNonNavigationRequest(req) // Check if refresh token is likely expired (older than 6 hours) refreshTokenExpired := refreshTokenPresent && t.isRefreshTokenExpired(session) @@ -397,7 +448,10 @@ func (t *TraefikOidc) processAuthorizedRequest(rw http.ResponseWriter, req *http } if len(t.headerTemplates) > 0 { - claims, err := t.extractClaimsFunc(session.GetIDToken()) + // Reuse claims parsed earlier in this request if the ID token has not + // changed. Saves an unnecessary JWT parse on every authenticated + // request that uses headerTemplates. + claims, err := session.GetIDTokenClaims(t.extractClaimsFunc) if err != nil { t.logger.Errorf("Failed to extract claims from ID Token for template headers: %v", err) } else { diff --git a/refresh_coordinator.go b/refresh_coordinator.go index 9d7ddd1..b741e26 100644 --- a/refresh_coordinator.go +++ b/refresh_coordinator.go @@ -18,7 +18,6 @@ type RefreshCoordinator struct { inFlightRefreshes map[string]*refreshOperation cleanupTimers map[string]*time.Timer sessionRefreshAttempts map[string]*refreshAttemptTracker - delayedCleanupQueue chan delayedCleanupItem circuitBreaker *RefreshCircuitBreaker metrics *RefreshMetrics logger *Logger @@ -107,12 +106,6 @@ type RefreshMetrics struct { currentInFlightRefreshes int32 } -// delayedCleanupItem represents an item scheduled for delayed cleanup -type delayedCleanupItem struct { - cleanupAt time.Time - tokenHash string -} - // RefreshCircuitBreaker implements a circuit breaker specifically for refresh operations type RefreshCircuitBreaker struct { lastFailureTime time.Time @@ -143,7 +136,6 @@ func NewRefreshCoordinator(config RefreshCoordinatorConfig, logger *Logger) *Ref metrics: &RefreshMetrics{}, logger: logger, stopChan: make(chan struct{}), - delayedCleanupQueue: make(chan delayedCleanupItem, 1000), // Buffered channel for cleanup items cleanupTimers: make(map[string]*time.Timer), circuitBreaker: &RefreshCircuitBreaker{ config: RefreshCircuitBreakerConfig{ @@ -158,10 +150,6 @@ func NewRefreshCoordinator(config RefreshCoordinatorConfig, logger *Logger) *Ref rc.wg.Add(1) go rc.cleanupRoutine() - // Start delayed cleanup processor (single goroutine processes all cleanup timers) - rc.wg.Add(1) - go rc.processDelayedCleanups() - return rc } @@ -377,35 +365,19 @@ func (rc *RefreshCoordinator) scheduleDelayedCleanup(tokenHash string) { rc.cleanupTimerMu.Unlock() } -// performCleanup removes the operation from the in-flight map +// performCleanup removes the operation from the in-flight map. +// Idempotent: only decrements the in-flight counter if an entry was actually +// removed. This guards against any future path accidentally calling cleanup +// twice for the same tokenHash (which would corrupt the refresh budget). func (rc *RefreshCoordinator) performCleanup(tokenHash string) { rc.refreshMutex.Lock() - delete(rc.inFlightRefreshes, tokenHash) + _, existed := rc.inFlightRefreshes[tokenHash] + if existed { + delete(rc.inFlightRefreshes, tokenHash) + } rc.refreshMutex.Unlock() - atomic.AddInt32(&rc.metrics.currentInFlightRefreshes, -1) -} - -// processDelayedCleanups processes delayed cleanup requests from the queue -// This is a single goroutine that handles all delayed cleanups -func (rc *RefreshCoordinator) processDelayedCleanups() { - defer rc.wg.Done() - - for { - select { - case item := <-rc.delayedCleanupQueue: - // Wait until cleanup time - waitDuration := time.Until(item.cleanupAt) - if waitDuration > 0 { - select { - case <-time.After(waitDuration): - case <-rc.stopChan: - return - } - } - rc.performCleanup(item.tokenHash) - case <-rc.stopChan: - return - } + if existed { + atomic.AddInt32(&rc.metrics.currentInFlightRefreshes, -1) } } @@ -498,11 +470,16 @@ func (rc *RefreshCoordinator) hashRefreshToken(token string) string { return hex.EncodeToString(hash[:]) } -// isUnderMemoryPressure checks if the system is under memory pressure +// isUnderMemoryPressure checks if the system is under memory pressure by +// consulting the global memory monitor. Returns true when pressure reaches +// High or Critical, at which point we refuse new refresh operations to +// avoid aggravating an already-stressed heap. func (rc *RefreshCoordinator) isUnderMemoryPressure() bool { - // This is a simplified check - in production you'd want to use runtime.MemStats - // or system-specific memory monitoring - return false // Placeholder - implement actual memory check + monitor := GetGlobalMemoryMonitor() + if monitor == nil { + return false + } + return monitor.GetMemoryPressure() >= MemoryPressureHigh } // cleanupRoutine periodically cleans up stale tracking entries diff --git a/session.go b/session.go index e806c50..8954783 100644 --- a/session.go +++ b/session.go @@ -1216,6 +1216,18 @@ type SessionData struct { dirty bool inUse bool + + // cachedClaimsToken is the ID token string whose claims were last parsed and + // cached. A lazy, per-request cache to avoid re-parsing the JWT on every + // authenticated request (e.g. for headerTemplates). Protected by sessionMutex. + cachedClaimsToken string + + // cachedClaims holds the parsed claims for cachedClaimsToken. + cachedClaims map[string]interface{} + + // cachedClaimsErr holds the parse error (if any) for cachedClaimsToken so + // failures are not retried within the same request. + cachedClaimsErr error } // IsDirty returns true if the session data has been modified since it was last loaded or saved. @@ -1737,6 +1749,12 @@ func (sd *SessionData) Reset() { sd.request = nil sd.useCombinedStorage = true // Reset to use combined storage by default + // Drop any cached claims so pooled SessionData does not leak claim data + // between requests/users. + sd.cachedClaimsToken = "" + sd.cachedClaims = nil + sd.cachedClaimsErr = nil + // Reset the refresh mutex to ensure clean state // Note: We don't need to lock it since sessionMutex is already held // and this session is not in use by any request @@ -2514,6 +2532,41 @@ func (sd *SessionData) GetIDToken() string { return sd.getIDTokenUnsafe() } +// GetIDTokenClaims returns claims parsed from the current ID token, caching +// the result on the SessionData so repeated callers within the same request +// do not re-parse the JWT. The cache is keyed on the ID token string and is +// cleared when the SessionData is reset (see Reset) or when the ID token +// changes (e.g. after a refresh). +// +// The parser parameter is typically the TraefikOidc.extractClaimsFunc, which +// lets tests inject mocks just like the direct call it replaces. +// +// Returns an empty claims map and a nil error when the session has no ID +// token, matching the existing "no-op" behavior of the caller sites. +func (sd *SessionData) GetIDTokenClaims(parser func(string) (map[string]interface{}, error)) (map[string]interface{}, error) { + sd.sessionMutex.Lock() + defer sd.sessionMutex.Unlock() + + token := sd.getIDTokenUnsafe() + if token == "" { + // Invalidate any stale cache without running the parser. + sd.cachedClaimsToken = "" + sd.cachedClaims = nil + sd.cachedClaimsErr = nil + return nil, nil + } + + if sd.cachedClaimsToken == token && (sd.cachedClaims != nil || sd.cachedClaimsErr != nil) { + return sd.cachedClaims, sd.cachedClaimsErr + } + + claims, err := parser(token) + sd.cachedClaimsToken = token + sd.cachedClaims = claims + sd.cachedClaimsErr = err + return claims, err +} + // getIDTokenUnsafe retrieves the ID token without acquiring locks. // Enhanced ID token retrieval with comprehensive integrity checks and chunking support. // Used when the session mutex is already held to prevent deadlocks. diff --git a/settings.go b/settings.go index af20edb..3aec242 100644 --- a/settings.go +++ b/settings.go @@ -1,6 +1,7 @@ package traefikoidc import ( + "crypto/x509" "fmt" "io" "log" @@ -70,6 +71,46 @@ type Config struct { EnableFrontchannelLogout bool `json:"enableFrontchannelLogout,omitempty"` BackchannelLogoutURL string `json:"backchannelLogoutURL,omitempty"` FrontchannelLogoutURL string `json:"frontchannelLogoutURL,omitempty"` + // CACertPath is an optional filesystem path to a PEM-encoded CA bundle used + // to verify the OIDC provider's TLS certificate. Use this when the provider + // is signed by an internal/private CA that is not in the system trust store. + CACertPath string `json:"caCertPath,omitempty"` + // CACertPEM is an optional inline PEM-encoded CA bundle, equivalent to + // CACertPath but supplied directly in the middleware configuration. Both + // may be set; certificates from both sources are combined. + CACertPEM string `json:"caCertPEM,omitempty"` + // InsecureSkipVerify disables TLS certificate verification for the OIDC + // provider. Intended ONLY for local development against self-signed + // providers. Enabling this in production is a security hole — prefer + // CACertPath/CACertPEM. Emits a loud warning at startup. + InsecureSkipVerify bool `json:"insecureSkipVerify,omitempty"` +} + +// loadCACertPool assembles an x509.CertPool from CACertPath and CACertPEM. +// Returns (nil, nil) when neither is configured — callers should fall back to +// the system trust store. Returns a descriptive error if a PEM source is +// configured but contains no parseable certificates, so misconfigurations +// surface at startup rather than as unexplained TLS failures at runtime. +func (c *Config) loadCACertPool() (*x509.CertPool, error) { + if c.CACertPath == "" && c.CACertPEM == "" { + return nil, nil + } + pool := x509.NewCertPool() + if c.CACertPath != "" { + data, err := os.ReadFile(c.CACertPath) + if err != nil { + return nil, fmt.Errorf("read caCertPath %q: %w", c.CACertPath, err) + } + if !pool.AppendCertsFromPEM(data) { + return nil, fmt.Errorf("caCertPath %q: no valid PEM certificates found", c.CACertPath) + } + } + if c.CACertPEM != "" { + if !pool.AppendCertsFromPEM([]byte(c.CACertPEM)) { + return nil, fmt.Errorf("caCertPEM: no valid PEM certificates found") + } + } + return pool, nil } // RedisConfig configures Redis cache backend settings for distributed caching. @@ -734,6 +775,16 @@ func (l *Logger) Errorf(format string, args ...interface{}) { l.logError.Printf(format, args...) } +// IsDebug reports whether debug-level logging is enabled. +// Callers should use this to avoid expensive format-string expansion +// (e.g. on hot paths under yaegi) when debug output would be discarded. +func (l *Logger) IsDebug() bool { + if l == nil || l.logDebug == nil { + return false + } + return l.logDebug.Writer() != io.Discard +} + // newNoOpLogger creates a logger that discards all output. // // Deprecated: Use GetSingletonNoOpLogger() instead for better memory efficiency. diff --git a/singleton_resources.go b/singleton_resources.go index 5af8021..e7e69f4 100644 --- a/singleton_resources.go +++ b/singleton_resources.go @@ -548,17 +548,24 @@ func (gc *GenericCache) Delete(key string) { delete(gc.data, key) } -// cleanupRoutine periodically cleans up the cache +// cleanupRoutine periodically wipes the cache. +// +// NOTE: GenericCache does not track per-entry timestamps, so this is a +// "clear-all on tick" strategy — every `gc.ttl` interval the entire map +// is replaced, regardless of when each entry was written. This is the +// intentional (simplified) behavior of GenericCache, which exists mainly +// as a generic fallback for tests and non-typed caches. Callers that +// require true per-entry TTL must use UniversalCache / UnifiedCache which +// track expiry per entry. func (gc *GenericCache) cleanupRoutine() { - ticker := time.NewTicker(gc.ttl) - defer ticker.Stop() + wipeTicker := time.NewTicker(gc.ttl) + defer wipeTicker.Stop() for { select { - case <-ticker.C: + case <-wipeTicker.C: gc.mu.Lock() - // Simple cleanup - clear all data after TTL - // In production, you'd track individual entry TTLs + // Clear-all on tick, not per-entry TTL (see function doc). gc.data = make(map[string]interface{}) gc.mu.Unlock() case <-gc.stopChan: diff --git a/universal_cache.go b/universal_cache.go index 913b35b..6a75a05 100644 --- a/universal_cache.go +++ b/universal_cache.go @@ -306,8 +306,10 @@ func (c *UniversalCache) Set(key string, value interface{}, ttl time.Duration) e c.currentMemory += size } - c.logger.Debugf("UniversalCache[%s]: Set key=%s, ttl=%v, size=%d bytes", - c.config.Type, key, ttl, size) + if c.logger.IsDebug() { + c.logger.Debugf("UniversalCache[%s]: Set key=%s, ttl=%v, size=%d bytes", + c.config.Type, key, ttl, size) + } return nil } @@ -331,10 +333,11 @@ func (c *UniversalCache) Get(key string) (interface{}, bool) { // Fall through to local cache } else { atomic.AddInt64(&c.hits, 1) - // Update local cache with backend value - go func() { - _ = c.updateLocalCache(key, value, c.config.DefaultTTL) - }() + // Update local cache with backend value synchronously. + // Under yaegi, goroutine spawn is 5-10x costlier than compiled Go, + // and this path fires per-request on cold local cache. + // updateLocalCache is cheap (map write under mutex). + _ = c.updateLocalCache(key, value, c.config.DefaultTTL) return value, true } } @@ -540,7 +543,9 @@ func (c *UniversalCache) evictOldest() { if item, exists := c.items[key]; exists { c.removeItem(key, item) atomic.AddInt64(&c.evictions, 1) - c.logger.Debugf("UniversalCache[%s]: Evicted key=%s", c.config.Type, key) + if c.logger.IsDebug() { + c.logger.Debugf("UniversalCache[%s]: Evicted key=%s", c.config.Type, key) + } } } } diff --git a/vendor/github.com/alicebob/miniredis/v2/gopher-json/json.go b/vendor/github.com/alicebob/miniredis/v2/gopher-json/json.go index 21fb2ff..1156133 100644 --- a/vendor/github.com/alicebob/miniredis/v2/gopher-json/json.go +++ b/vendor/github.com/alicebob/miniredis/v2/gopher-json/json.go @@ -10,7 +10,7 @@ import ( // Preload adds json to the given Lua state's package.preload table. After it // has been preloaded, it can be loaded using require: // -// local json = require("json") +// local json = require("json") func Preload(L *lua.LState) { L.PreloadModule("json", Loader) } diff --git a/vendor/github.com/davecgh/go-spew/spew/bypass.go b/vendor/github.com/davecgh/go-spew/spew/bypass.go index 70ddeaa..7929947 100644 --- a/vendor/github.com/davecgh/go-spew/spew/bypass.go +++ b/vendor/github.com/davecgh/go-spew/spew/bypass.go @@ -18,7 +18,6 @@ // tag is deprecated and thus should not be used. // Go versions prior to 1.4 are disabled because they use a different layout // for interfaces which make the implementation of unsafeReflectValue more complex. -//go:build !js && !appengine && !safe && !disableunsafe && go1.4 // +build !js,!appengine,!safe,!disableunsafe,go1.4 package spew diff --git a/vendor/github.com/davecgh/go-spew/spew/bypasssafe.go b/vendor/github.com/davecgh/go-spew/spew/bypasssafe.go index 5e2d890..205c28d 100644 --- a/vendor/github.com/davecgh/go-spew/spew/bypasssafe.go +++ b/vendor/github.com/davecgh/go-spew/spew/bypasssafe.go @@ -16,7 +16,6 @@ // when the code is running on Google App Engine, compiled by GopherJS, or // "-tags safe" is added to the go build command line. The "disableunsafe" // tag is deprecated and thus should not be used. -//go:build js || appengine || safe || disableunsafe || !go1.4 // +build js appengine safe disableunsafe !go1.4 package spew diff --git a/vendor/github.com/davecgh/go-spew/spew/config.go b/vendor/github.com/davecgh/go-spew/spew/config.go index 161895f..2e3d22f 100644 --- a/vendor/github.com/davecgh/go-spew/spew/config.go +++ b/vendor/github.com/davecgh/go-spew/spew/config.go @@ -254,15 +254,15 @@ pointer addresses used to indirect to the final value. It provides the following features over the built-in printing facilities provided by the fmt package: - - Pointers are dereferenced and followed - - Circular data structures are detected and handled properly - - Custom Stringer/error interfaces are optionally invoked, including - on unexported types - - Custom types which only implement the Stringer/error interfaces via - a pointer receiver are optionally invoked when passing non-pointer - variables - - Byte arrays and slices are dumped like the hexdump -C command which - includes offsets, byte values in hex, and ASCII output + * Pointers are dereferenced and followed + * Circular data structures are detected and handled properly + * Custom Stringer/error interfaces are optionally invoked, including + on unexported types + * Custom types which only implement the Stringer/error interfaces via + a pointer receiver are optionally invoked when passing non-pointer + variables + * Byte arrays and slices are dumped like the hexdump -C command which + includes offsets, byte values in hex, and ASCII output The configuration options are controlled by modifying the public members of c. See ConfigState for options documentation. @@ -295,12 +295,12 @@ func (c *ConfigState) convertArgs(args []interface{}) (formatters []interface{}) // NewDefaultConfig returns a ConfigState with the following default settings. // -// Indent: " " -// MaxDepth: 0 -// DisableMethods: false -// DisablePointerMethods: false -// ContinueOnMethod: false -// SortKeys: false +// Indent: " " +// MaxDepth: 0 +// DisableMethods: false +// DisablePointerMethods: false +// ContinueOnMethod: false +// SortKeys: false func NewDefaultConfig() *ConfigState { return &ConfigState{Indent: " "} } diff --git a/vendor/github.com/davecgh/go-spew/spew/doc.go b/vendor/github.com/davecgh/go-spew/spew/doc.go index 722e9aa..aacaac6 100644 --- a/vendor/github.com/davecgh/go-spew/spew/doc.go +++ b/vendor/github.com/davecgh/go-spew/spew/doc.go @@ -21,36 +21,35 @@ debugging. A quick overview of the additional features spew provides over the built-in printing facilities for Go data types are as follows: - - Pointers are dereferenced and followed - - Circular data structures are detected and handled properly - - Custom Stringer/error interfaces are optionally invoked, including - on unexported types - - Custom types which only implement the Stringer/error interfaces via - a pointer receiver are optionally invoked when passing non-pointer - variables - - Byte arrays and slices are dumped like the hexdump -C command which - includes offsets, byte values in hex, and ASCII output (only when using - Dump style) + * Pointers are dereferenced and followed + * Circular data structures are detected and handled properly + * Custom Stringer/error interfaces are optionally invoked, including + on unexported types + * Custom types which only implement the Stringer/error interfaces via + a pointer receiver are optionally invoked when passing non-pointer + variables + * Byte arrays and slices are dumped like the hexdump -C command which + includes offsets, byte values in hex, and ASCII output (only when using + Dump style) There are two different approaches spew allows for dumping Go data structures: - - Dump style which prints with newlines, customizable indentation, - and additional debug information such as types and all pointer addresses - used to indirect to the final value - - A custom Formatter interface that integrates cleanly with the standard fmt - package and replaces %v, %+v, %#v, and %#+v to provide inline printing - similar to the default %v while providing the additional functionality - outlined above and passing unsupported format verbs such as %x and %q - along to fmt + * Dump style which prints with newlines, customizable indentation, + and additional debug information such as types and all pointer addresses + used to indirect to the final value + * A custom Formatter interface that integrates cleanly with the standard fmt + package and replaces %v, %+v, %#v, and %#+v to provide inline printing + similar to the default %v while providing the additional functionality + outlined above and passing unsupported format verbs such as %x and %q + along to fmt -# Quick Start +Quick Start This section demonstrates how to quickly get started with spew. See the sections below for further details on formatting and configuration options. To dump a variable with full newlines, indentation, type, and pointer information use Dump, Fdump, or Sdump: - spew.Dump(myVar1, myVar2, ...) spew.Fdump(someWriter, myVar1, myVar2, ...) str := spew.Sdump(myVar1, myVar2, ...) @@ -59,13 +58,12 @@ Alternatively, if you would prefer to use format strings with a compacted inline printing style, use the convenience wrappers Printf, Fprintf, etc with %v (most compact), %+v (adds pointer addresses), %#v (adds types), or %#+v (adds types and pointer addresses): - spew.Printf("myVar1: %v -- myVar2: %+v", myVar1, myVar2) spew.Printf("myVar3: %#v -- myVar4: %#+v", myVar3, myVar4) spew.Fprintf(someWriter, "myVar1: %v -- myVar2: %+v", myVar1, myVar2) spew.Fprintf(someWriter, "myVar3: %#v -- myVar4: %#+v", myVar3, myVar4) -# Configuration Options +Configuration Options Configuration of spew is handled by fields in the ConfigState type. For convenience, all of the top-level functions use a global state available @@ -76,52 +74,51 @@ equivalent to the top-level functions. This allows concurrent configuration options. See the ConfigState documentation for more details. The following configuration options are available: + * Indent + String to use for each indentation level for Dump functions. + It is a single space by default. A popular alternative is "\t". - - Indent - String to use for each indentation level for Dump functions. - It is a single space by default. A popular alternative is "\t". + * MaxDepth + Maximum number of levels to descend into nested data structures. + There is no limit by default. - - MaxDepth - Maximum number of levels to descend into nested data structures. - There is no limit by default. + * DisableMethods + Disables invocation of error and Stringer interface methods. + Method invocation is enabled by default. - - DisableMethods - Disables invocation of error and Stringer interface methods. - Method invocation is enabled by default. + * DisablePointerMethods + Disables invocation of error and Stringer interface methods on types + which only accept pointer receivers from non-pointer variables. + Pointer method invocation is enabled by default. - - DisablePointerMethods - Disables invocation of error and Stringer interface methods on types - which only accept pointer receivers from non-pointer variables. - Pointer method invocation is enabled by default. + * DisablePointerAddresses + DisablePointerAddresses specifies whether to disable the printing of + pointer addresses. This is useful when diffing data structures in tests. - - DisablePointerAddresses - DisablePointerAddresses specifies whether to disable the printing of - pointer addresses. This is useful when diffing data structures in tests. + * DisableCapacities + DisableCapacities specifies whether to disable the printing of + capacities for arrays, slices, maps and channels. This is useful when + diffing data structures in tests. - - DisableCapacities - DisableCapacities specifies whether to disable the printing of - capacities for arrays, slices, maps and channels. This is useful when - diffing data structures in tests. + * ContinueOnMethod + Enables recursion into types after invoking error and Stringer interface + methods. Recursion after method invocation is disabled by default. - - ContinueOnMethod - Enables recursion into types after invoking error and Stringer interface - methods. Recursion after method invocation is disabled by default. + * SortKeys + Specifies map keys should be sorted before being printed. Use + this to have a more deterministic, diffable output. Note that + only native types (bool, int, uint, floats, uintptr and string) + and types which implement error or Stringer interfaces are + supported with other types sorted according to the + reflect.Value.String() output which guarantees display + stability. Natural map order is used by default. - - SortKeys - Specifies map keys should be sorted before being printed. Use - this to have a more deterministic, diffable output. Note that - only native types (bool, int, uint, floats, uintptr and string) - and types which implement error or Stringer interfaces are - supported with other types sorted according to the - reflect.Value.String() output which guarantees display - stability. Natural map order is used by default. + * SpewKeys + Specifies that, as a last resort attempt, map keys should be + spewed to strings and sorted by those strings. This is only + considered if SortKeys is true. - - SpewKeys - Specifies that, as a last resort attempt, map keys should be - spewed to strings and sorted by those strings. This is only - considered if SortKeys is true. - -# Dump Usage +Dump Usage Simply call spew.Dump with a list of variables you want to dump: @@ -136,7 +133,7 @@ A third option is to call spew.Sdump to get the formatted output as a string: str := spew.Sdump(myVar1, myVar2, ...) -# Sample Dump Output +Sample Dump Output See the Dump example for details on the setup of the types and variables being shown here. @@ -153,14 +150,13 @@ shown here. Byte (and uint8) arrays and slices are displayed uniquely like the hexdump -C command as shown. - ([]uint8) (len=32 cap=32) { 00000000 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 |............... | 00000010 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30 |!"#$%&'()*+,-./0| 00000020 31 32 |12| } -# Custom Formatter +Custom Formatter Spew provides a custom formatter that implements the fmt.Formatter interface so that it integrates cleanly with standard fmt package printing functions. The @@ -174,7 +170,7 @@ standard fmt package for formatting. In addition, the custom formatter ignores the width and precision arguments (however they will still work on the format specifiers not handled by the custom formatter). -# Custom Formatter Usage +Custom Formatter Usage The simplest way to make use of the spew custom formatter is to call one of the convenience functions such as spew.Printf, spew.Println, or spew.Printf. The @@ -188,17 +184,15 @@ functions have syntax you are most likely already familiar with: See the Index for the full list convenience functions. -# Sample Formatter Output +Sample Formatter Output Double pointer to a uint8: - %v: <**>5 %+v: <**>(0xf8400420d0->0xf8400420c8)5 %#v: (**uint8)5 %#+v: (**uint8)(0xf8400420d0->0xf8400420c8)5 Pointer to circular struct with a uint8 field and a pointer to itself: - %v: <*>{1 <*>} %+v: <*>(0xf84003e260){ui8:1 c:<*>(0xf84003e260)} %#v: (*main.circular){ui8:(uint8)1 c:(*main.circular)} @@ -207,7 +201,7 @@ Pointer to circular struct with a uint8 field and a pointer to itself: See the Printf example for details on the setup of variables being shown here. -# Errors +Errors Since it is possible for custom Stringer/error interfaces to panic, spew detects them and handles them internally by printing the panic information diff --git a/vendor/github.com/davecgh/go-spew/spew/dump.go b/vendor/github.com/davecgh/go-spew/spew/dump.go index 8323041..f78d89f 100644 --- a/vendor/github.com/davecgh/go-spew/spew/dump.go +++ b/vendor/github.com/davecgh/go-spew/spew/dump.go @@ -488,15 +488,15 @@ pointer addresses used to indirect to the final value. It provides the following features over the built-in printing facilities provided by the fmt package: - - Pointers are dereferenced and followed - - Circular data structures are detected and handled properly - - Custom Stringer/error interfaces are optionally invoked, including - on unexported types - - Custom types which only implement the Stringer/error interfaces via - a pointer receiver are optionally invoked when passing non-pointer - variables - - Byte arrays and slices are dumped like the hexdump -C command which - includes offsets, byte values in hex, and ASCII output + * Pointers are dereferenced and followed + * Circular data structures are detected and handled properly + * Custom Stringer/error interfaces are optionally invoked, including + on unexported types + * Custom types which only implement the Stringer/error interfaces via + a pointer receiver are optionally invoked when passing non-pointer + variables + * Byte arrays and slices are dumped like the hexdump -C command which + includes offsets, byte values in hex, and ASCII output The configuration options are controlled by an exported package global, spew.Config. See ConfigState for options documentation. diff --git a/vendor/github.com/google/uuid/CHANGELOG.md b/vendor/github.com/google/uuid/CHANGELOG.md deleted file mode 100644 index 7ec5ac7..0000000 --- a/vendor/github.com/google/uuid/CHANGELOG.md +++ /dev/null @@ -1,41 +0,0 @@ -# Changelog - -## [1.6.0](https://github.com/google/uuid/compare/v1.5.0...v1.6.0) (2024-01-16) - - -### Features - -* add Max UUID constant ([#149](https://github.com/google/uuid/issues/149)) ([c58770e](https://github.com/google/uuid/commit/c58770eb495f55fe2ced6284f93c5158a62e53e3)) - - -### Bug Fixes - -* fix typo in version 7 uuid documentation ([#153](https://github.com/google/uuid/issues/153)) ([016b199](https://github.com/google/uuid/commit/016b199544692f745ffc8867b914129ecb47ef06)) -* Monotonicity in UUIDv7 ([#150](https://github.com/google/uuid/issues/150)) ([a2b2b32](https://github.com/google/uuid/commit/a2b2b32373ff0b1a312b7fdf6d38a977099698a6)) - -## [1.5.0](https://github.com/google/uuid/compare/v1.4.0...v1.5.0) (2023-12-12) - - -### Features - -* Validate UUID without creating new UUID ([#141](https://github.com/google/uuid/issues/141)) ([9ee7366](https://github.com/google/uuid/commit/9ee7366e66c9ad96bab89139418a713dc584ae29)) - -## [1.4.0](https://github.com/google/uuid/compare/v1.3.1...v1.4.0) (2023-10-26) - - -### Features - -* UUIDs slice type with Strings() convenience method ([#133](https://github.com/google/uuid/issues/133)) ([cd5fbbd](https://github.com/google/uuid/commit/cd5fbbdd02f3e3467ac18940e07e062be1f864b4)) - -### Fixes - -* Clarify that Parse's job is to parse but not necessarily validate strings. (Documents current behavior) - -## [1.3.1](https://github.com/google/uuid/compare/v1.3.0...v1.3.1) (2023-08-18) - - -### Bug Fixes - -* Use .EqualFold() to parse urn prefixed UUIDs ([#118](https://github.com/google/uuid/issues/118)) ([574e687](https://github.com/google/uuid/commit/574e6874943741fb99d41764c705173ada5293f0)) - -## Changelog diff --git a/vendor/github.com/google/uuid/CONTRIBUTING.md b/vendor/github.com/google/uuid/CONTRIBUTING.md deleted file mode 100644 index a502fdc..0000000 --- a/vendor/github.com/google/uuid/CONTRIBUTING.md +++ /dev/null @@ -1,26 +0,0 @@ -# How to contribute - -We definitely welcome patches and contribution to this project! - -### Tips - -Commits must be formatted according to the [Conventional Commits Specification](https://www.conventionalcommits.org). - -Always try to include a test case! If it is not possible or not necessary, -please explain why in the pull request description. - -### Releasing - -Commits that would precipitate a SemVer change, as described in the Conventional -Commits Specification, will trigger [`release-please`](https://github.com/google-github-actions/release-please-action) -to create a release candidate pull request. Once submitted, `release-please` -will create a release. - -For tips on how to work with `release-please`, see its documentation. - -### Legal requirements - -In order to protect both you and ourselves, you will need to sign the -[Contributor License Agreement](https://cla.developers.google.com/clas). - -You may have already signed it for other Google projects. diff --git a/vendor/github.com/google/uuid/CONTRIBUTORS b/vendor/github.com/google/uuid/CONTRIBUTORS deleted file mode 100644 index b4bb97f..0000000 --- a/vendor/github.com/google/uuid/CONTRIBUTORS +++ /dev/null @@ -1,9 +0,0 @@ -Paul Borman -bmatsuo -shawnps -theory -jboverfelt -dsymonds -cd1 -wallclockbuilder -dansouza diff --git a/vendor/github.com/google/uuid/LICENSE b/vendor/github.com/google/uuid/LICENSE deleted file mode 100644 index 5dc6826..0000000 --- a/vendor/github.com/google/uuid/LICENSE +++ /dev/null @@ -1,27 +0,0 @@ -Copyright (c) 2009,2014 Google Inc. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - * Neither the name of Google Inc. nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/google/uuid/README.md b/vendor/github.com/google/uuid/README.md deleted file mode 100644 index 3e9a618..0000000 --- a/vendor/github.com/google/uuid/README.md +++ /dev/null @@ -1,21 +0,0 @@ -# uuid -The uuid package generates and inspects UUIDs based on -[RFC 4122](https://datatracker.ietf.org/doc/html/rfc4122) -and DCE 1.1: Authentication and Security Services. - -This package is based on the github.com/pborman/uuid package (previously named -code.google.com/p/go-uuid). It differs from these earlier packages in that -a UUID is a 16 byte array rather than a byte slice. One loss due to this -change is the ability to represent an invalid UUID (vs a NIL UUID). - -###### Install -```sh -go get github.com/google/uuid -``` - -###### Documentation -[![Go Reference](https://pkg.go.dev/badge/github.com/google/uuid.svg)](https://pkg.go.dev/github.com/google/uuid) - -Full `go doc` style documentation for the package can be viewed online without -installing this package by using the GoDoc site here: -http://pkg.go.dev/github.com/google/uuid diff --git a/vendor/github.com/google/uuid/dce.go b/vendor/github.com/google/uuid/dce.go deleted file mode 100644 index fa820b9..0000000 --- a/vendor/github.com/google/uuid/dce.go +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright 2016 Google Inc. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package uuid - -import ( - "encoding/binary" - "fmt" - "os" -) - -// A Domain represents a Version 2 domain -type Domain byte - -// Domain constants for DCE Security (Version 2) UUIDs. -const ( - Person = Domain(0) - Group = Domain(1) - Org = Domain(2) -) - -// NewDCESecurity returns a DCE Security (Version 2) UUID. -// -// The domain should be one of Person, Group or Org. -// On a POSIX system the id should be the users UID for the Person -// domain and the users GID for the Group. The meaning of id for -// the domain Org or on non-POSIX systems is site defined. -// -// For a given domain/id pair the same token may be returned for up to -// 7 minutes and 10 seconds. -func NewDCESecurity(domain Domain, id uint32) (UUID, error) { - uuid, err := NewUUID() - if err == nil { - uuid[6] = (uuid[6] & 0x0f) | 0x20 // Version 2 - uuid[9] = byte(domain) - binary.BigEndian.PutUint32(uuid[0:], id) - } - return uuid, err -} - -// NewDCEPerson returns a DCE Security (Version 2) UUID in the person -// domain with the id returned by os.Getuid. -// -// NewDCESecurity(Person, uint32(os.Getuid())) -func NewDCEPerson() (UUID, error) { - return NewDCESecurity(Person, uint32(os.Getuid())) -} - -// NewDCEGroup returns a DCE Security (Version 2) UUID in the group -// domain with the id returned by os.Getgid. -// -// NewDCESecurity(Group, uint32(os.Getgid())) -func NewDCEGroup() (UUID, error) { - return NewDCESecurity(Group, uint32(os.Getgid())) -} - -// Domain returns the domain for a Version 2 UUID. Domains are only defined -// for Version 2 UUIDs. -func (uuid UUID) Domain() Domain { - return Domain(uuid[9]) -} - -// ID returns the id for a Version 2 UUID. IDs are only defined for Version 2 -// UUIDs. -func (uuid UUID) ID() uint32 { - return binary.BigEndian.Uint32(uuid[0:4]) -} - -func (d Domain) String() string { - switch d { - case Person: - return "Person" - case Group: - return "Group" - case Org: - return "Org" - } - return fmt.Sprintf("Domain%d", int(d)) -} diff --git a/vendor/github.com/google/uuid/doc.go b/vendor/github.com/google/uuid/doc.go deleted file mode 100644 index 5b8a4b9..0000000 --- a/vendor/github.com/google/uuid/doc.go +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2016 Google Inc. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Package uuid generates and inspects UUIDs. -// -// UUIDs are based on RFC 4122 and DCE 1.1: Authentication and Security -// Services. -// -// A UUID is a 16 byte (128 bit) array. UUIDs may be used as keys to -// maps or compared directly. -package uuid diff --git a/vendor/github.com/google/uuid/hash.go b/vendor/github.com/google/uuid/hash.go deleted file mode 100644 index dc60082..0000000 --- a/vendor/github.com/google/uuid/hash.go +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright 2016 Google Inc. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package uuid - -import ( - "crypto/md5" - "crypto/sha1" - "hash" -) - -// Well known namespace IDs and UUIDs -var ( - NameSpaceDNS = Must(Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c8")) - NameSpaceURL = Must(Parse("6ba7b811-9dad-11d1-80b4-00c04fd430c8")) - NameSpaceOID = Must(Parse("6ba7b812-9dad-11d1-80b4-00c04fd430c8")) - NameSpaceX500 = Must(Parse("6ba7b814-9dad-11d1-80b4-00c04fd430c8")) - Nil UUID // empty UUID, all zeros - - // The Max UUID is special form of UUID that is specified to have all 128 bits set to 1. - Max = UUID{ - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - } -) - -// NewHash returns a new UUID derived from the hash of space concatenated with -// data generated by h. The hash should be at least 16 byte in length. The -// first 16 bytes of the hash are used to form the UUID. The version of the -// UUID will be the lower 4 bits of version. NewHash is used to implement -// NewMD5 and NewSHA1. -func NewHash(h hash.Hash, space UUID, data []byte, version int) UUID { - h.Reset() - h.Write(space[:]) //nolint:errcheck - h.Write(data) //nolint:errcheck - s := h.Sum(nil) - var uuid UUID - copy(uuid[:], s) - uuid[6] = (uuid[6] & 0x0f) | uint8((version&0xf)<<4) - uuid[8] = (uuid[8] & 0x3f) | 0x80 // RFC 4122 variant - return uuid -} - -// NewMD5 returns a new MD5 (Version 3) UUID based on the -// supplied name space and data. It is the same as calling: -// -// NewHash(md5.New(), space, data, 3) -func NewMD5(space UUID, data []byte) UUID { - return NewHash(md5.New(), space, data, 3) -} - -// NewSHA1 returns a new SHA1 (Version 5) UUID based on the -// supplied name space and data. It is the same as calling: -// -// NewHash(sha1.New(), space, data, 5) -func NewSHA1(space UUID, data []byte) UUID { - return NewHash(sha1.New(), space, data, 5) -} diff --git a/vendor/github.com/google/uuid/marshal.go b/vendor/github.com/google/uuid/marshal.go deleted file mode 100644 index 14bd340..0000000 --- a/vendor/github.com/google/uuid/marshal.go +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2016 Google Inc. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package uuid - -import "fmt" - -// MarshalText implements encoding.TextMarshaler. -func (uuid UUID) MarshalText() ([]byte, error) { - var js [36]byte - encodeHex(js[:], uuid) - return js[:], nil -} - -// UnmarshalText implements encoding.TextUnmarshaler. -func (uuid *UUID) UnmarshalText(data []byte) error { - id, err := ParseBytes(data) - if err != nil { - return err - } - *uuid = id - return nil -} - -// MarshalBinary implements encoding.BinaryMarshaler. -func (uuid UUID) MarshalBinary() ([]byte, error) { - return uuid[:], nil -} - -// UnmarshalBinary implements encoding.BinaryUnmarshaler. -func (uuid *UUID) UnmarshalBinary(data []byte) error { - if len(data) != 16 { - return fmt.Errorf("invalid UUID (got %d bytes)", len(data)) - } - copy(uuid[:], data) - return nil -} diff --git a/vendor/github.com/google/uuid/node.go b/vendor/github.com/google/uuid/node.go deleted file mode 100644 index d651a2b..0000000 --- a/vendor/github.com/google/uuid/node.go +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright 2016 Google Inc. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package uuid - -import ( - "sync" -) - -var ( - nodeMu sync.Mutex - ifname string // name of interface being used - nodeID [6]byte // hardware for version 1 UUIDs - zeroID [6]byte // nodeID with only 0's -) - -// NodeInterface returns the name of the interface from which the NodeID was -// derived. The interface "user" is returned if the NodeID was set by -// SetNodeID. -func NodeInterface() string { - defer nodeMu.Unlock() - nodeMu.Lock() - return ifname -} - -// SetNodeInterface selects the hardware address to be used for Version 1 UUIDs. -// If name is "" then the first usable interface found will be used or a random -// Node ID will be generated. If a named interface cannot be found then false -// is returned. -// -// SetNodeInterface never fails when name is "". -func SetNodeInterface(name string) bool { - defer nodeMu.Unlock() - nodeMu.Lock() - return setNodeInterface(name) -} - -func setNodeInterface(name string) bool { - iname, addr := getHardwareInterface(name) // null implementation for js - if iname != "" && addr != nil { - ifname = iname - copy(nodeID[:], addr) - return true - } - - // We found no interfaces with a valid hardware address. If name - // does not specify a specific interface generate a random Node ID - // (section 4.1.6) - if name == "" { - ifname = "random" - randomBits(nodeID[:]) - return true - } - return false -} - -// NodeID returns a slice of a copy of the current Node ID, setting the Node ID -// if not already set. -func NodeID() []byte { - defer nodeMu.Unlock() - nodeMu.Lock() - if nodeID == zeroID { - setNodeInterface("") - } - nid := nodeID - return nid[:] -} - -// SetNodeID sets the Node ID to be used for Version 1 UUIDs. The first 6 bytes -// of id are used. If id is less than 6 bytes then false is returned and the -// Node ID is not set. -func SetNodeID(id []byte) bool { - if len(id) < 6 { - return false - } - defer nodeMu.Unlock() - nodeMu.Lock() - copy(nodeID[:], id) - ifname = "user" - return true -} - -// NodeID returns the 6 byte node id encoded in uuid. It returns nil if uuid is -// not valid. The NodeID is only well defined for version 1 and 2 UUIDs. -func (uuid UUID) NodeID() []byte { - var node [6]byte - copy(node[:], uuid[10:]) - return node[:] -} diff --git a/vendor/github.com/google/uuid/node_js.go b/vendor/github.com/google/uuid/node_js.go deleted file mode 100644 index b2a0bc8..0000000 --- a/vendor/github.com/google/uuid/node_js.go +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2017 Google Inc. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// +build js - -package uuid - -// getHardwareInterface returns nil values for the JS version of the code. -// This removes the "net" dependency, because it is not used in the browser. -// Using the "net" library inflates the size of the transpiled JS code by 673k bytes. -func getHardwareInterface(name string) (string, []byte) { return "", nil } diff --git a/vendor/github.com/google/uuid/node_net.go b/vendor/github.com/google/uuid/node_net.go deleted file mode 100644 index 0cbbcdd..0000000 --- a/vendor/github.com/google/uuid/node_net.go +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2017 Google Inc. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// +build !js - -package uuid - -import "net" - -var interfaces []net.Interface // cached list of interfaces - -// getHardwareInterface returns the name and hardware address of interface name. -// If name is "" then the name and hardware address of one of the system's -// interfaces is returned. If no interfaces are found (name does not exist or -// there are no interfaces) then "", nil is returned. -// -// Only addresses of at least 6 bytes are returned. -func getHardwareInterface(name string) (string, []byte) { - if interfaces == nil { - var err error - interfaces, err = net.Interfaces() - if err != nil { - return "", nil - } - } - for _, ifs := range interfaces { - if len(ifs.HardwareAddr) >= 6 && (name == "" || name == ifs.Name) { - return ifs.Name, ifs.HardwareAddr - } - } - return "", nil -} diff --git a/vendor/github.com/google/uuid/null.go b/vendor/github.com/google/uuid/null.go deleted file mode 100644 index d7fcbf2..0000000 --- a/vendor/github.com/google/uuid/null.go +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright 2021 Google Inc. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package uuid - -import ( - "bytes" - "database/sql/driver" - "encoding/json" - "fmt" -) - -var jsonNull = []byte("null") - -// NullUUID represents a UUID that may be null. -// NullUUID implements the SQL driver.Scanner interface so -// it can be used as a scan destination: -// -// var u uuid.NullUUID -// err := db.QueryRow("SELECT name FROM foo WHERE id=?", id).Scan(&u) -// ... -// if u.Valid { -// // use u.UUID -// } else { -// // NULL value -// } -// -type NullUUID struct { - UUID UUID - Valid bool // Valid is true if UUID is not NULL -} - -// Scan implements the SQL driver.Scanner interface. -func (nu *NullUUID) Scan(value interface{}) error { - if value == nil { - nu.UUID, nu.Valid = Nil, false - return nil - } - - err := nu.UUID.Scan(value) - if err != nil { - nu.Valid = false - return err - } - - nu.Valid = true - return nil -} - -// Value implements the driver Valuer interface. -func (nu NullUUID) Value() (driver.Value, error) { - if !nu.Valid { - return nil, nil - } - // Delegate to UUID Value function - return nu.UUID.Value() -} - -// MarshalBinary implements encoding.BinaryMarshaler. -func (nu NullUUID) MarshalBinary() ([]byte, error) { - if nu.Valid { - return nu.UUID[:], nil - } - - return []byte(nil), nil -} - -// UnmarshalBinary implements encoding.BinaryUnmarshaler. -func (nu *NullUUID) UnmarshalBinary(data []byte) error { - if len(data) != 16 { - return fmt.Errorf("invalid UUID (got %d bytes)", len(data)) - } - copy(nu.UUID[:], data) - nu.Valid = true - return nil -} - -// MarshalText implements encoding.TextMarshaler. -func (nu NullUUID) MarshalText() ([]byte, error) { - if nu.Valid { - return nu.UUID.MarshalText() - } - - return jsonNull, nil -} - -// UnmarshalText implements encoding.TextUnmarshaler. -func (nu *NullUUID) UnmarshalText(data []byte) error { - id, err := ParseBytes(data) - if err != nil { - nu.Valid = false - return err - } - nu.UUID = id - nu.Valid = true - return nil -} - -// MarshalJSON implements json.Marshaler. -func (nu NullUUID) MarshalJSON() ([]byte, error) { - if nu.Valid { - return json.Marshal(nu.UUID) - } - - return jsonNull, nil -} - -// UnmarshalJSON implements json.Unmarshaler. -func (nu *NullUUID) UnmarshalJSON(data []byte) error { - if bytes.Equal(data, jsonNull) { - *nu = NullUUID{} - return nil // valid null UUID - } - err := json.Unmarshal(data, &nu.UUID) - nu.Valid = err == nil - return err -} diff --git a/vendor/github.com/google/uuid/sql.go b/vendor/github.com/google/uuid/sql.go deleted file mode 100644 index 2e02ec0..0000000 --- a/vendor/github.com/google/uuid/sql.go +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright 2016 Google Inc. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package uuid - -import ( - "database/sql/driver" - "fmt" -) - -// Scan implements sql.Scanner so UUIDs can be read from databases transparently. -// Currently, database types that map to string and []byte are supported. Please -// consult database-specific driver documentation for matching types. -func (uuid *UUID) Scan(src interface{}) error { - switch src := src.(type) { - case nil: - return nil - - case string: - // if an empty UUID comes from a table, we return a null UUID - if src == "" { - return nil - } - - // see Parse for required string format - u, err := Parse(src) - if err != nil { - return fmt.Errorf("Scan: %v", err) - } - - *uuid = u - - case []byte: - // if an empty UUID comes from a table, we return a null UUID - if len(src) == 0 { - return nil - } - - // assumes a simple slice of bytes if 16 bytes - // otherwise attempts to parse - if len(src) != 16 { - return uuid.Scan(string(src)) - } - copy((*uuid)[:], src) - - default: - return fmt.Errorf("Scan: unable to scan type %T into UUID", src) - } - - return nil -} - -// Value implements sql.Valuer so that UUIDs can be written to databases -// transparently. Currently, UUIDs map to strings. Please consult -// database-specific driver documentation for matching types. -func (uuid UUID) Value() (driver.Value, error) { - return uuid.String(), nil -} diff --git a/vendor/github.com/google/uuid/time.go b/vendor/github.com/google/uuid/time.go deleted file mode 100644 index c351129..0000000 --- a/vendor/github.com/google/uuid/time.go +++ /dev/null @@ -1,134 +0,0 @@ -// Copyright 2016 Google Inc. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package uuid - -import ( - "encoding/binary" - "sync" - "time" -) - -// A Time represents a time as the number of 100's of nanoseconds since 15 Oct -// 1582. -type Time int64 - -const ( - lillian = 2299160 // Julian day of 15 Oct 1582 - unix = 2440587 // Julian day of 1 Jan 1970 - epoch = unix - lillian // Days between epochs - g1582 = epoch * 86400 // seconds between epochs - g1582ns100 = g1582 * 10000000 // 100s of a nanoseconds between epochs -) - -var ( - timeMu sync.Mutex - lasttime uint64 // last time we returned - clockSeq uint16 // clock sequence for this run - - timeNow = time.Now // for testing -) - -// UnixTime converts t the number of seconds and nanoseconds using the Unix -// epoch of 1 Jan 1970. -func (t Time) UnixTime() (sec, nsec int64) { - sec = int64(t - g1582ns100) - nsec = (sec % 10000000) * 100 - sec /= 10000000 - return sec, nsec -} - -// GetTime returns the current Time (100s of nanoseconds since 15 Oct 1582) and -// clock sequence as well as adjusting the clock sequence as needed. An error -// is returned if the current time cannot be determined. -func GetTime() (Time, uint16, error) { - defer timeMu.Unlock() - timeMu.Lock() - return getTime() -} - -func getTime() (Time, uint16, error) { - t := timeNow() - - // If we don't have a clock sequence already, set one. - if clockSeq == 0 { - setClockSequence(-1) - } - now := uint64(t.UnixNano()/100) + g1582ns100 - - // If time has gone backwards with this clock sequence then we - // increment the clock sequence - if now <= lasttime { - clockSeq = ((clockSeq + 1) & 0x3fff) | 0x8000 - } - lasttime = now - return Time(now), clockSeq, nil -} - -// ClockSequence returns the current clock sequence, generating one if not -// already set. The clock sequence is only used for Version 1 UUIDs. -// -// The uuid package does not use global static storage for the clock sequence or -// the last time a UUID was generated. Unless SetClockSequence is used, a new -// random clock sequence is generated the first time a clock sequence is -// requested by ClockSequence, GetTime, or NewUUID. (section 4.2.1.1) -func ClockSequence() int { - defer timeMu.Unlock() - timeMu.Lock() - return clockSequence() -} - -func clockSequence() int { - if clockSeq == 0 { - setClockSequence(-1) - } - return int(clockSeq & 0x3fff) -} - -// SetClockSequence sets the clock sequence to the lower 14 bits of seq. Setting to -// -1 causes a new sequence to be generated. -func SetClockSequence(seq int) { - defer timeMu.Unlock() - timeMu.Lock() - setClockSequence(seq) -} - -func setClockSequence(seq int) { - if seq == -1 { - var b [2]byte - randomBits(b[:]) // clock sequence - seq = int(b[0])<<8 | int(b[1]) - } - oldSeq := clockSeq - clockSeq = uint16(seq&0x3fff) | 0x8000 // Set our variant - if oldSeq != clockSeq { - lasttime = 0 - } -} - -// Time returns the time in 100s of nanoseconds since 15 Oct 1582 encoded in -// uuid. The time is only defined for version 1, 2, 6 and 7 UUIDs. -func (uuid UUID) Time() Time { - var t Time - switch uuid.Version() { - case 6: - time := binary.BigEndian.Uint64(uuid[:8]) // Ignore uuid[6] version b0110 - t = Time(time) - case 7: - time := binary.BigEndian.Uint64(uuid[:8]) - t = Time((time>>16)*10000 + g1582ns100) - default: // forward compatible - time := int64(binary.BigEndian.Uint32(uuid[0:4])) - time |= int64(binary.BigEndian.Uint16(uuid[4:6])) << 32 - time |= int64(binary.BigEndian.Uint16(uuid[6:8])&0xfff) << 48 - t = Time(time) - } - return t -} - -// ClockSequence returns the clock sequence encoded in uuid. -// The clock sequence is only well defined for version 1 and 2 UUIDs. -func (uuid UUID) ClockSequence() int { - return int(binary.BigEndian.Uint16(uuid[8:10])) & 0x3fff -} diff --git a/vendor/github.com/google/uuid/util.go b/vendor/github.com/google/uuid/util.go deleted file mode 100644 index 5ea6c73..0000000 --- a/vendor/github.com/google/uuid/util.go +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright 2016 Google Inc. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package uuid - -import ( - "io" -) - -// randomBits completely fills slice b with random data. -func randomBits(b []byte) { - if _, err := io.ReadFull(rander, b); err != nil { - panic(err.Error()) // rand should never fail - } -} - -// xvalues returns the value of a byte as a hexadecimal digit or 255. -var xvalues = [256]byte{ - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 255, 255, 255, 255, 255, 255, - 255, 10, 11, 12, 13, 14, 15, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 10, 11, 12, 13, 14, 15, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, -} - -// xtob converts hex characters x1 and x2 into a byte. -func xtob(x1, x2 byte) (byte, bool) { - b1 := xvalues[x1] - b2 := xvalues[x2] - return (b1 << 4) | b2, b1 != 255 && b2 != 255 -} diff --git a/vendor/github.com/google/uuid/uuid.go b/vendor/github.com/google/uuid/uuid.go deleted file mode 100644 index 5232b48..0000000 --- a/vendor/github.com/google/uuid/uuid.go +++ /dev/null @@ -1,365 +0,0 @@ -// Copyright 2018 Google Inc. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package uuid - -import ( - "bytes" - "crypto/rand" - "encoding/hex" - "errors" - "fmt" - "io" - "strings" - "sync" -) - -// A UUID is a 128 bit (16 byte) Universal Unique IDentifier as defined in RFC -// 4122. -type UUID [16]byte - -// A Version represents a UUID's version. -type Version byte - -// A Variant represents a UUID's variant. -type Variant byte - -// Constants returned by Variant. -const ( - Invalid = Variant(iota) // Invalid UUID - RFC4122 // The variant specified in RFC4122 - Reserved // Reserved, NCS backward compatibility. - Microsoft // Reserved, Microsoft Corporation backward compatibility. - Future // Reserved for future definition. -) - -const randPoolSize = 16 * 16 - -var ( - rander = rand.Reader // random function - poolEnabled = false - poolMu sync.Mutex - poolPos = randPoolSize // protected with poolMu - pool [randPoolSize]byte // protected with poolMu -) - -type invalidLengthError struct{ len int } - -func (err invalidLengthError) Error() string { - return fmt.Sprintf("invalid UUID length: %d", err.len) -} - -// IsInvalidLengthError is matcher function for custom error invalidLengthError -func IsInvalidLengthError(err error) bool { - _, ok := err.(invalidLengthError) - return ok -} - -// Parse decodes s into a UUID or returns an error if it cannot be parsed. Both -// the standard UUID forms defined in RFC 4122 -// (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx and -// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx) are decoded. In addition, -// Parse accepts non-standard strings such as the raw hex encoding -// xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx and 38 byte "Microsoft style" encodings, -// e.g. {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}. Only the middle 36 bytes are -// examined in the latter case. Parse should not be used to validate strings as -// it parses non-standard encodings as indicated above. -func Parse(s string) (UUID, error) { - var uuid UUID - switch len(s) { - // xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx - case 36: - - // urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx - case 36 + 9: - if !strings.EqualFold(s[:9], "urn:uuid:") { - return uuid, fmt.Errorf("invalid urn prefix: %q", s[:9]) - } - s = s[9:] - - // {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} - case 36 + 2: - s = s[1:] - - // xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx - case 32: - var ok bool - for i := range uuid { - uuid[i], ok = xtob(s[i*2], s[i*2+1]) - if !ok { - return uuid, errors.New("invalid UUID format") - } - } - return uuid, nil - default: - return uuid, invalidLengthError{len(s)} - } - // s is now at least 36 bytes long - // it must be of the form xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx - if s[8] != '-' || s[13] != '-' || s[18] != '-' || s[23] != '-' { - return uuid, errors.New("invalid UUID format") - } - for i, x := range [16]int{ - 0, 2, 4, 6, - 9, 11, - 14, 16, - 19, 21, - 24, 26, 28, 30, 32, 34, - } { - v, ok := xtob(s[x], s[x+1]) - if !ok { - return uuid, errors.New("invalid UUID format") - } - uuid[i] = v - } - return uuid, nil -} - -// ParseBytes is like Parse, except it parses a byte slice instead of a string. -func ParseBytes(b []byte) (UUID, error) { - var uuid UUID - switch len(b) { - case 36: // xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx - case 36 + 9: // urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx - if !bytes.EqualFold(b[:9], []byte("urn:uuid:")) { - return uuid, fmt.Errorf("invalid urn prefix: %q", b[:9]) - } - b = b[9:] - case 36 + 2: // {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} - b = b[1:] - case 32: // xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx - var ok bool - for i := 0; i < 32; i += 2 { - uuid[i/2], ok = xtob(b[i], b[i+1]) - if !ok { - return uuid, errors.New("invalid UUID format") - } - } - return uuid, nil - default: - return uuid, invalidLengthError{len(b)} - } - // s is now at least 36 bytes long - // it must be of the form xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx - if b[8] != '-' || b[13] != '-' || b[18] != '-' || b[23] != '-' { - return uuid, errors.New("invalid UUID format") - } - for i, x := range [16]int{ - 0, 2, 4, 6, - 9, 11, - 14, 16, - 19, 21, - 24, 26, 28, 30, 32, 34, - } { - v, ok := xtob(b[x], b[x+1]) - if !ok { - return uuid, errors.New("invalid UUID format") - } - uuid[i] = v - } - return uuid, nil -} - -// MustParse is like Parse but panics if the string cannot be parsed. -// It simplifies safe initialization of global variables holding compiled UUIDs. -func MustParse(s string) UUID { - uuid, err := Parse(s) - if err != nil { - panic(`uuid: Parse(` + s + `): ` + err.Error()) - } - return uuid -} - -// FromBytes creates a new UUID from a byte slice. Returns an error if the slice -// does not have a length of 16. The bytes are copied from the slice. -func FromBytes(b []byte) (uuid UUID, err error) { - err = uuid.UnmarshalBinary(b) - return uuid, err -} - -// Must returns uuid if err is nil and panics otherwise. -func Must(uuid UUID, err error) UUID { - if err != nil { - panic(err) - } - return uuid -} - -// Validate returns an error if s is not a properly formatted UUID in one of the following formats: -// xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx -// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx -// xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -// {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} -// It returns an error if the format is invalid, otherwise nil. -func Validate(s string) error { - switch len(s) { - // Standard UUID format - case 36: - - // UUID with "urn:uuid:" prefix - case 36 + 9: - if !strings.EqualFold(s[:9], "urn:uuid:") { - return fmt.Errorf("invalid urn prefix: %q", s[:9]) - } - s = s[9:] - - // UUID enclosed in braces - case 36 + 2: - if s[0] != '{' || s[len(s)-1] != '}' { - return fmt.Errorf("invalid bracketed UUID format") - } - s = s[1 : len(s)-1] - - // UUID without hyphens - case 32: - for i := 0; i < len(s); i += 2 { - _, ok := xtob(s[i], s[i+1]) - if !ok { - return errors.New("invalid UUID format") - } - } - - default: - return invalidLengthError{len(s)} - } - - // Check for standard UUID format - if len(s) == 36 { - if s[8] != '-' || s[13] != '-' || s[18] != '-' || s[23] != '-' { - return errors.New("invalid UUID format") - } - for _, x := range []int{0, 2, 4, 6, 9, 11, 14, 16, 19, 21, 24, 26, 28, 30, 32, 34} { - if _, ok := xtob(s[x], s[x+1]); !ok { - return errors.New("invalid UUID format") - } - } - } - - return nil -} - -// String returns the string form of uuid, xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx -// , or "" if uuid is invalid. -func (uuid UUID) String() string { - var buf [36]byte - encodeHex(buf[:], uuid) - return string(buf[:]) -} - -// URN returns the RFC 2141 URN form of uuid, -// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx, or "" if uuid is invalid. -func (uuid UUID) URN() string { - var buf [36 + 9]byte - copy(buf[:], "urn:uuid:") - encodeHex(buf[9:], uuid) - return string(buf[:]) -} - -func encodeHex(dst []byte, uuid UUID) { - hex.Encode(dst, uuid[:4]) - dst[8] = '-' - hex.Encode(dst[9:13], uuid[4:6]) - dst[13] = '-' - hex.Encode(dst[14:18], uuid[6:8]) - dst[18] = '-' - hex.Encode(dst[19:23], uuid[8:10]) - dst[23] = '-' - hex.Encode(dst[24:], uuid[10:]) -} - -// Variant returns the variant encoded in uuid. -func (uuid UUID) Variant() Variant { - switch { - case (uuid[8] & 0xc0) == 0x80: - return RFC4122 - case (uuid[8] & 0xe0) == 0xc0: - return Microsoft - case (uuid[8] & 0xe0) == 0xe0: - return Future - default: - return Reserved - } -} - -// Version returns the version of uuid. -func (uuid UUID) Version() Version { - return Version(uuid[6] >> 4) -} - -func (v Version) String() string { - if v > 15 { - return fmt.Sprintf("BAD_VERSION_%d", v) - } - return fmt.Sprintf("VERSION_%d", v) -} - -func (v Variant) String() string { - switch v { - case RFC4122: - return "RFC4122" - case Reserved: - return "Reserved" - case Microsoft: - return "Microsoft" - case Future: - return "Future" - case Invalid: - return "Invalid" - } - return fmt.Sprintf("BadVariant%d", int(v)) -} - -// SetRand sets the random number generator to r, which implements io.Reader. -// If r.Read returns an error when the package requests random data then -// a panic will be issued. -// -// Calling SetRand with nil sets the random number generator to the default -// generator. -func SetRand(r io.Reader) { - if r == nil { - rander = rand.Reader - return - } - rander = r -} - -// EnableRandPool enables internal randomness pool used for Random -// (Version 4) UUID generation. The pool contains random bytes read from -// the random number generator on demand in batches. Enabling the pool -// may improve the UUID generation throughput significantly. -// -// Since the pool is stored on the Go heap, this feature may be a bad fit -// for security sensitive applications. -// -// Both EnableRandPool and DisableRandPool are not thread-safe and should -// only be called when there is no possibility that New or any other -// UUID Version 4 generation function will be called concurrently. -func EnableRandPool() { - poolEnabled = true -} - -// DisableRandPool disables the randomness pool if it was previously -// enabled with EnableRandPool. -// -// Both EnableRandPool and DisableRandPool are not thread-safe and should -// only be called when there is no possibility that New or any other -// UUID Version 4 generation function will be called concurrently. -func DisableRandPool() { - poolEnabled = false - defer poolMu.Unlock() - poolMu.Lock() - poolPos = randPoolSize -} - -// UUIDs is a slice of UUID types. -type UUIDs []UUID - -// Strings returns a string slice containing the string form of each UUID in uuids. -func (uuids UUIDs) Strings() []string { - var uuidStrs = make([]string, len(uuids)) - for i, uuid := range uuids { - uuidStrs[i] = uuid.String() - } - return uuidStrs -} diff --git a/vendor/github.com/google/uuid/version1.go b/vendor/github.com/google/uuid/version1.go deleted file mode 100644 index 4631096..0000000 --- a/vendor/github.com/google/uuid/version1.go +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright 2016 Google Inc. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package uuid - -import ( - "encoding/binary" -) - -// NewUUID returns a Version 1 UUID based on the current NodeID and clock -// sequence, and the current time. If the NodeID has not been set by SetNodeID -// or SetNodeInterface then it will be set automatically. If the NodeID cannot -// be set NewUUID returns nil. If clock sequence has not been set by -// SetClockSequence then it will be set automatically. If GetTime fails to -// return the current NewUUID returns nil and an error. -// -// In most cases, New should be used. -func NewUUID() (UUID, error) { - var uuid UUID - now, seq, err := GetTime() - if err != nil { - return uuid, err - } - - timeLow := uint32(now & 0xffffffff) - timeMid := uint16((now >> 32) & 0xffff) - timeHi := uint16((now >> 48) & 0x0fff) - timeHi |= 0x1000 // Version 1 - - binary.BigEndian.PutUint32(uuid[0:], timeLow) - binary.BigEndian.PutUint16(uuid[4:], timeMid) - binary.BigEndian.PutUint16(uuid[6:], timeHi) - binary.BigEndian.PutUint16(uuid[8:], seq) - - nodeMu.Lock() - if nodeID == zeroID { - setNodeInterface("") - } - copy(uuid[10:], nodeID[:]) - nodeMu.Unlock() - - return uuid, nil -} diff --git a/vendor/github.com/google/uuid/version4.go b/vendor/github.com/google/uuid/version4.go deleted file mode 100644 index 7697802..0000000 --- a/vendor/github.com/google/uuid/version4.go +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright 2016 Google Inc. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package uuid - -import "io" - -// New creates a new random UUID or panics. New is equivalent to -// the expression -// -// uuid.Must(uuid.NewRandom()) -func New() UUID { - return Must(NewRandom()) -} - -// NewString creates a new random UUID and returns it as a string or panics. -// NewString is equivalent to the expression -// -// uuid.New().String() -func NewString() string { - return Must(NewRandom()).String() -} - -// NewRandom returns a Random (Version 4) UUID. -// -// The strength of the UUIDs is based on the strength of the crypto/rand -// package. -// -// Uses the randomness pool if it was enabled with EnableRandPool. -// -// A note about uniqueness derived from the UUID Wikipedia entry: -// -// Randomly generated UUIDs have 122 random bits. One's annual risk of being -// hit by a meteorite is estimated to be one chance in 17 billion, that -// means the probability is about 0.00000000006 (6 × 10−11), -// equivalent to the odds of creating a few tens of trillions of UUIDs in a -// year and having one duplicate. -func NewRandom() (UUID, error) { - if !poolEnabled { - return NewRandomFromReader(rander) - } - return newRandomFromPool() -} - -// NewRandomFromReader returns a UUID based on bytes read from a given io.Reader. -func NewRandomFromReader(r io.Reader) (UUID, error) { - var uuid UUID - _, err := io.ReadFull(r, uuid[:]) - if err != nil { - return Nil, err - } - uuid[6] = (uuid[6] & 0x0f) | 0x40 // Version 4 - uuid[8] = (uuid[8] & 0x3f) | 0x80 // Variant is 10 - return uuid, nil -} - -func newRandomFromPool() (UUID, error) { - var uuid UUID - poolMu.Lock() - if poolPos == randPoolSize { - _, err := io.ReadFull(rander, pool[:]) - if err != nil { - poolMu.Unlock() - return Nil, err - } - poolPos = 0 - } - copy(uuid[:], pool[poolPos:(poolPos+16)]) - poolPos += 16 - poolMu.Unlock() - - uuid[6] = (uuid[6] & 0x0f) | 0x40 // Version 4 - uuid[8] = (uuid[8] & 0x3f) | 0x80 // Variant is 10 - return uuid, nil -} diff --git a/vendor/github.com/google/uuid/version6.go b/vendor/github.com/google/uuid/version6.go deleted file mode 100644 index 339a959..0000000 --- a/vendor/github.com/google/uuid/version6.go +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright 2023 Google Inc. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package uuid - -import "encoding/binary" - -// UUID version 6 is a field-compatible version of UUIDv1, reordered for improved DB locality. -// It is expected that UUIDv6 will primarily be used in contexts where there are existing v1 UUIDs. -// Systems that do not involve legacy UUIDv1 SHOULD consider using UUIDv7 instead. -// -// see https://datatracker.ietf.org/doc/html/draft-peabody-dispatch-new-uuid-format-03#uuidv6 -// -// NewV6 returns a Version 6 UUID based on the current NodeID and clock -// sequence, and the current time. If the NodeID has not been set by SetNodeID -// or SetNodeInterface then it will be set automatically. If the NodeID cannot -// be set NewV6 set NodeID is random bits automatically . If clock sequence has not been set by -// SetClockSequence then it will be set automatically. If GetTime fails to -// return the current NewV6 returns Nil and an error. -func NewV6() (UUID, error) { - var uuid UUID - now, seq, err := GetTime() - if err != nil { - return uuid, err - } - - /* - 0 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | time_high | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | time_mid | time_low_and_version | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - |clk_seq_hi_res | clk_seq_low | node (0-1) | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | node (2-5) | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - */ - - binary.BigEndian.PutUint64(uuid[0:], uint64(now)) - binary.BigEndian.PutUint16(uuid[8:], seq) - - uuid[6] = 0x60 | (uuid[6] & 0x0F) - uuid[8] = 0x80 | (uuid[8] & 0x3F) - - nodeMu.Lock() - if nodeID == zeroID { - setNodeInterface("") - } - copy(uuid[10:], nodeID[:]) - nodeMu.Unlock() - - return uuid, nil -} diff --git a/vendor/github.com/google/uuid/version7.go b/vendor/github.com/google/uuid/version7.go deleted file mode 100644 index 3167b64..0000000 --- a/vendor/github.com/google/uuid/version7.go +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright 2023 Google Inc. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package uuid - -import ( - "io" -) - -// UUID version 7 features a time-ordered value field derived from the widely -// implemented and well known Unix Epoch timestamp source, -// the number of milliseconds seconds since midnight 1 Jan 1970 UTC, leap seconds excluded. -// As well as improved entropy characteristics over versions 1 or 6. -// -// see https://datatracker.ietf.org/doc/html/draft-peabody-dispatch-new-uuid-format-03#name-uuid-version-7 -// -// Implementations SHOULD utilize UUID version 7 over UUID version 1 and 6 if possible. -// -// NewV7 returns a Version 7 UUID based on the current time(Unix Epoch). -// Uses the randomness pool if it was enabled with EnableRandPool. -// On error, NewV7 returns Nil and an error -func NewV7() (UUID, error) { - uuid, err := NewRandom() - if err != nil { - return uuid, err - } - makeV7(uuid[:]) - return uuid, nil -} - -// NewV7FromReader returns a Version 7 UUID based on the current time(Unix Epoch). -// it use NewRandomFromReader fill random bits. -// On error, NewV7FromReader returns Nil and an error. -func NewV7FromReader(r io.Reader) (UUID, error) { - uuid, err := NewRandomFromReader(r) - if err != nil { - return uuid, err - } - - makeV7(uuid[:]) - return uuid, nil -} - -// makeV7 fill 48 bits time (uuid[0] - uuid[5]), set version b0111 (uuid[6]) -// uuid[8] already has the right version number (Variant is 10) -// see function NewV7 and NewV7FromReader -func makeV7(uuid []byte) { - /* - 0 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | unix_ts_ms | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | unix_ts_ms | ver | rand_a (12 bit seq) | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - |var| rand_b | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | rand_b | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - */ - _ = uuid[15] // bounds check - - t, s := getV7Time() - - uuid[0] = byte(t >> 40) - uuid[1] = byte(t >> 32) - uuid[2] = byte(t >> 24) - uuid[3] = byte(t >> 16) - uuid[4] = byte(t >> 8) - uuid[5] = byte(t) - - uuid[6] = 0x70 | (0x0F & byte(s>>8)) - uuid[7] = byte(s) -} - -// lastV7time is the last time we returned stored as: -// -// 52 bits of time in milliseconds since epoch -// 12 bits of (fractional nanoseconds) >> 8 -var lastV7time int64 - -const nanoPerMilli = 1000000 - -// getV7Time returns the time in milliseconds and nanoseconds / 256. -// The returned (milli << 12 + seq) is guarenteed to be greater than -// (milli << 12 + seq) returned by any previous call to getV7Time. -func getV7Time() (milli, seq int64) { - timeMu.Lock() - defer timeMu.Unlock() - - nano := timeNow().UnixNano() - milli = nano / nanoPerMilli - // Sequence number is between 0 and 3906 (nanoPerMilli>>8) - seq = (nano - milli*nanoPerMilli) >> 8 - now := milli<<12 + seq - if now <= lastV7time { - now = lastV7time + 1 - milli = now >> 12 - seq = now & 0xfff - } - lastV7time = now - return milli, seq -} diff --git a/vendor/github.com/pmezard/go-difflib/difflib/difflib.go b/vendor/github.com/pmezard/go-difflib/difflib/difflib.go index 2a73737..003e99f 100644 --- a/vendor/github.com/pmezard/go-difflib/difflib/difflib.go +++ b/vendor/github.com/pmezard/go-difflib/difflib/difflib.go @@ -199,15 +199,12 @@ func (m *SequenceMatcher) isBJunk(s string) bool { // If IsJunk is not defined: // // Return (i,j,k) such that a[i:i+k] is equal to b[j:j+k], where -// -// alo <= i <= i+k <= ahi -// blo <= j <= j+k <= bhi -// +// alo <= i <= i+k <= ahi +// blo <= j <= j+k <= bhi // and for all (i',j',k') meeting those conditions, -// -// k >= k' -// i <= i' -// and if i == i', j <= j' +// k >= k' +// i <= i' +// and if i == i', j <= j' // // In other words, of all maximal matching blocks, return one that // starts earliest in a, and of all those maximal matching blocks that diff --git a/vendor/github.com/redis/go-redis/v9/auth/reauth_credentials_listener.go b/vendor/github.com/redis/go-redis/v9/auth/reauth_credentials_listener.go index 40076a0..f4b3198 100644 --- a/vendor/github.com/redis/go-redis/v9/auth/reauth_credentials_listener.go +++ b/vendor/github.com/redis/go-redis/v9/auth/reauth_credentials_listener.go @@ -44,4 +44,4 @@ func NewReAuthCredentialsListener(reAuth func(credentials Credentials) error, on } // Ensure ReAuthCredentialsListener implements the CredentialsListener interface. -var _ CredentialsListener = (*ReAuthCredentialsListener)(nil) +var _ CredentialsListener = (*ReAuthCredentialsListener)(nil) \ No newline at end of file diff --git a/vendor/github.com/redis/go-redis/v9/internal/pool/conn_state.go b/vendor/github.com/redis/go-redis/v9/internal/pool/conn_state.go index afdc631..2050a74 100644 --- a/vendor/github.com/redis/go-redis/v9/internal/pool/conn_state.go +++ b/vendor/github.com/redis/go-redis/v9/internal/pool/conn_state.go @@ -13,12 +13,11 @@ import ( // States are designed to be lightweight and fast to check. // // State Transitions: -// -// CREATED → INITIALIZING → IDLE ⇄ IN_USE -// ↓ -// UNUSABLE (handoff/reauth) -// ↓ -// IDLE/CLOSED +// CREATED → INITIALIZING → IDLE ⇄ IN_USE +// ↓ +// UNUSABLE (handoff/reauth) +// ↓ +// IDLE/CLOSED type ConnState uint32 const ( @@ -121,7 +120,7 @@ type ConnStateMachine struct { // FIFO queue for waiters - only locked during waiter add/remove/notify mu sync.Mutex - waiters *list.List // List of *waiter + waiters *list.List // List of *waiter waiterCount atomic.Int32 // Fast lock-free check for waiters (avoids mutex in hot path) } @@ -341,3 +340,4 @@ func (sm *ConnStateMachine) notifyWaiters() { } } } + diff --git a/vendor/github.com/redis/go-redis/v9/internal/semaphore.go b/vendor/github.com/redis/go-redis/v9/internal/semaphore.go index a7f4046..a1dfca5 100644 --- a/vendor/github.com/redis/go-redis/v9/internal/semaphore.go +++ b/vendor/github.com/redis/go-redis/v9/internal/semaphore.go @@ -190,4 +190,4 @@ func (s *FIFOSemaphore) Close() { // Len returns the current number of acquired tokens. func (s *FIFOSemaphore) Len() int32 { return s.max - int32(len(s.tokens)) -} +} \ No newline at end of file diff --git a/vendor/gopkg.in/yaml.v3/apic.go b/vendor/gopkg.in/yaml.v3/apic.go index 05fd305..ae7d049 100644 --- a/vendor/gopkg.in/yaml.v3/apic.go +++ b/vendor/gopkg.in/yaml.v3/apic.go @@ -1,17 +1,17 @@ -// +// // Copyright (c) 2011-2019 Canonical Ltd // Copyright (c) 2006-2010 Kirill Simonov -// +// // Permission is hereby granted, free of charge, to any person obtaining a copy of // this software and associated documentation files (the "Software"), to deal in // the Software without restriction, including without limitation the rights to // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies // of the Software, and to permit persons to whom the Software is furnished to do // so, subject to the following conditions: -// +// // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. -// +// // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE diff --git a/vendor/gopkg.in/yaml.v3/emitterc.go b/vendor/gopkg.in/yaml.v3/emitterc.go index dde20e5..0f47c9c 100644 --- a/vendor/gopkg.in/yaml.v3/emitterc.go +++ b/vendor/gopkg.in/yaml.v3/emitterc.go @@ -162,9 +162,10 @@ func yaml_emitter_emit(emitter *yaml_emitter_t, event *yaml_event_t) bool { // Check if we need to accumulate more events before emitting. // // We accumulate extra -// - 1 event for DOCUMENT-START -// - 2 events for SEQUENCE-START -// - 3 events for MAPPING-START +// - 1 event for DOCUMENT-START +// - 2 events for SEQUENCE-START +// - 3 events for MAPPING-START +// func yaml_emitter_need_more_events(emitter *yaml_emitter_t) bool { if emitter.events_head == len(emitter.events) { return true @@ -240,7 +241,7 @@ func yaml_emitter_increase_indent(emitter *yaml_emitter_t, flow, indentless bool emitter.indent += 2 } else { // Everything else aligns to the chosen indentation. - emitter.indent = emitter.best_indent * ((emitter.indent + emitter.best_indent) / emitter.best_indent) + emitter.indent = emitter.best_indent*((emitter.indent+emitter.best_indent)/emitter.best_indent) } } return true diff --git a/vendor/gopkg.in/yaml.v3/parserc.go b/vendor/gopkg.in/yaml.v3/parserc.go index 25fe823..268558a 100644 --- a/vendor/gopkg.in/yaml.v3/parserc.go +++ b/vendor/gopkg.in/yaml.v3/parserc.go @@ -227,8 +227,7 @@ func yaml_parser_state_machine(parser *yaml_parser_t, event *yaml_event_t) bool // Parse the production: // stream ::= STREAM-START implicit_document? explicit_document* STREAM-END -// -// ************ +// ************ func yaml_parser_parse_stream_start(parser *yaml_parser_t, event *yaml_event_t) bool { token := peek_token(parser) if token == nil { @@ -250,12 +249,9 @@ func yaml_parser_parse_stream_start(parser *yaml_parser_t, event *yaml_event_t) // Parse the productions: // implicit_document ::= block_node DOCUMENT-END* -// -// * -// +// * // explicit_document ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END* -// -// ************************* +// ************************* func yaml_parser_parse_document_start(parser *yaml_parser_t, event *yaml_event_t, implicit bool) bool { token := peek_token(parser) @@ -360,8 +356,8 @@ func yaml_parser_parse_document_start(parser *yaml_parser_t, event *yaml_event_t // Parse the productions: // explicit_document ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END* +// *********** // -// *********** func yaml_parser_parse_document_content(parser *yaml_parser_t, event *yaml_event_t) bool { token := peek_token(parser) if token == nil { @@ -383,10 +379,9 @@ func yaml_parser_parse_document_content(parser *yaml_parser_t, event *yaml_event // Parse the productions: // implicit_document ::= block_node DOCUMENT-END* -// -// ************* -// +// ************* // explicit_document ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END* +// func yaml_parser_parse_document_end(parser *yaml_parser_t, event *yaml_event_t) bool { token := peek_token(parser) if token == nil { @@ -433,41 +428,30 @@ func yaml_parser_set_event_comments(parser *yaml_parser_t, event *yaml_event_t) // Parse the productions: // block_node_or_indentless_sequence ::= -// -// ALIAS -// ***** -// | properties (block_content | indentless_block_sequence)? -// ********** * -// | block_content | indentless_block_sequence -// * -// +// ALIAS +// ***** +// | properties (block_content | indentless_block_sequence)? +// ********** * +// | block_content | indentless_block_sequence +// * // block_node ::= ALIAS -// -// ***** -// | properties block_content? -// ********** * -// | block_content -// * -// +// ***** +// | properties block_content? +// ********** * +// | block_content +// * // flow_node ::= ALIAS -// -// ***** -// | properties flow_content? -// ********** * -// | flow_content -// * -// +// ***** +// | properties flow_content? +// ********** * +// | flow_content +// * // properties ::= TAG ANCHOR? | ANCHOR TAG? -// -// ************************* -// +// ************************* // block_content ::= block_collection | flow_collection | SCALAR -// -// ****** -// +// ****** // flow_content ::= flow_collection | SCALAR -// -// ****** +// ****** func yaml_parser_parse_node(parser *yaml_parser_t, event *yaml_event_t, block, indentless_sequence bool) bool { //defer trace("yaml_parser_parse_node", "block:", block, "indentless_sequence:", indentless_sequence)() @@ -698,8 +682,8 @@ func yaml_parser_parse_node(parser *yaml_parser_t, event *yaml_event_t, block, i // Parse the productions: // block_sequence ::= BLOCK-SEQUENCE-START (BLOCK-ENTRY block_node?)* BLOCK-END +// ******************** *********** * ********* // -// ******************** *********** * ********* func yaml_parser_parse_block_sequence_entry(parser *yaml_parser_t, event *yaml_event_t, first bool) bool { if first { token := peek_token(parser) @@ -756,8 +740,7 @@ func yaml_parser_parse_block_sequence_entry(parser *yaml_parser_t, event *yaml_e // Parse the productions: // indentless_sequence ::= (BLOCK-ENTRY block_node?)+ -// -// *********** * +// *********** * func yaml_parser_parse_indentless_sequence_entry(parser *yaml_parser_t, event *yaml_event_t) bool { token := peek_token(parser) if token == nil { @@ -822,14 +805,14 @@ func yaml_parser_split_stem_comment(parser *yaml_parser_t, stem_len int) { // Parse the productions: // block_mapping ::= BLOCK-MAPPING_START +// ******************* +// ((KEY block_node_or_indentless_sequence?)? +// *** * +// (VALUE block_node_or_indentless_sequence?)?)* // -// ******************* -// ((KEY block_node_or_indentless_sequence?)? -// *** * -// (VALUE block_node_or_indentless_sequence?)?)* +// BLOCK-END +// ********* // -// BLOCK-END -// ********* func yaml_parser_parse_block_mapping_key(parser *yaml_parser_t, event *yaml_event_t, first bool) bool { if first { token := peek_token(parser) @@ -898,11 +881,13 @@ func yaml_parser_parse_block_mapping_key(parser *yaml_parser_t, event *yaml_even // Parse the productions: // block_mapping ::= BLOCK-MAPPING_START // -// ((KEY block_node_or_indentless_sequence?)? +// ((KEY block_node_or_indentless_sequence?)? +// +// (VALUE block_node_or_indentless_sequence?)?)* +// ***** * +// BLOCK-END +// // -// (VALUE block_node_or_indentless_sequence?)?)* -// ***** * -// BLOCK-END func yaml_parser_parse_block_mapping_value(parser *yaml_parser_t, event *yaml_event_t) bool { token := peek_token(parser) if token == nil { @@ -930,18 +915,16 @@ func yaml_parser_parse_block_mapping_value(parser *yaml_parser_t, event *yaml_ev // Parse the productions: // flow_sequence ::= FLOW-SEQUENCE-START -// -// ******************* -// (flow_sequence_entry FLOW-ENTRY)* -// * ********** -// flow_sequence_entry? -// * -// FLOW-SEQUENCE-END -// ***************** -// +// ******************* +// (flow_sequence_entry FLOW-ENTRY)* +// * ********** +// flow_sequence_entry? +// * +// FLOW-SEQUENCE-END +// ***************** // flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? +// * // -// * func yaml_parser_parse_flow_sequence_entry(parser *yaml_parser_t, event *yaml_event_t, first bool) bool { if first { token := peek_token(parser) @@ -1004,10 +987,11 @@ func yaml_parser_parse_flow_sequence_entry(parser *yaml_parser_t, event *yaml_ev return true } +// // Parse the productions: // flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? +// *** * // -// *** * func yaml_parser_parse_flow_sequence_entry_mapping_key(parser *yaml_parser_t, event *yaml_event_t) bool { token := peek_token(parser) if token == nil { @@ -1027,8 +1011,8 @@ func yaml_parser_parse_flow_sequence_entry_mapping_key(parser *yaml_parser_t, ev // Parse the productions: // flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? +// ***** * // -// ***** * func yaml_parser_parse_flow_sequence_entry_mapping_value(parser *yaml_parser_t, event *yaml_event_t) bool { token := peek_token(parser) if token == nil { @@ -1051,8 +1035,8 @@ func yaml_parser_parse_flow_sequence_entry_mapping_value(parser *yaml_parser_t, // Parse the productions: // flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? +// * // -// * func yaml_parser_parse_flow_sequence_entry_mapping_end(parser *yaml_parser_t, event *yaml_event_t) bool { token := peek_token(parser) if token == nil { @@ -1069,17 +1053,16 @@ func yaml_parser_parse_flow_sequence_entry_mapping_end(parser *yaml_parser_t, ev // Parse the productions: // flow_mapping ::= FLOW-MAPPING-START -// -// ****************** -// (flow_mapping_entry FLOW-ENTRY)* -// * ********** -// flow_mapping_entry? -// ****************** -// FLOW-MAPPING-END -// **************** -// +// ****************** +// (flow_mapping_entry FLOW-ENTRY)* +// * ********** +// flow_mapping_entry? +// ****************** +// FLOW-MAPPING-END +// **************** // flow_mapping_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? -// - *** * +// * *** * +// func yaml_parser_parse_flow_mapping_key(parser *yaml_parser_t, event *yaml_event_t, first bool) bool { if first { token := peek_token(parser) @@ -1145,7 +1128,8 @@ func yaml_parser_parse_flow_mapping_key(parser *yaml_parser_t, event *yaml_event // Parse the productions: // flow_mapping_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? -// - ***** * +// * ***** * +// func yaml_parser_parse_flow_mapping_value(parser *yaml_parser_t, event *yaml_event_t, empty bool) bool { token := peek_token(parser) if token == nil { diff --git a/vendor/gopkg.in/yaml.v3/readerc.go b/vendor/gopkg.in/yaml.v3/readerc.go index 56af245..b7de0a8 100644 --- a/vendor/gopkg.in/yaml.v3/readerc.go +++ b/vendor/gopkg.in/yaml.v3/readerc.go @@ -1,17 +1,17 @@ -// +// // Copyright (c) 2011-2019 Canonical Ltd // Copyright (c) 2006-2010 Kirill Simonov -// +// // Permission is hereby granted, free of charge, to any person obtaining a copy of // this software and associated documentation files (the "Software"), to deal in // the Software without restriction, including without limitation the rights to // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies // of the Software, and to permit persons to whom the Software is furnished to do // so, subject to the following conditions: -// +// // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. -// +// // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE diff --git a/vendor/gopkg.in/yaml.v3/scannerc.go b/vendor/gopkg.in/yaml.v3/scannerc.go index 30b1f08..ca00701 100644 --- a/vendor/gopkg.in/yaml.v3/scannerc.go +++ b/vendor/gopkg.in/yaml.v3/scannerc.go @@ -1614,11 +1614,11 @@ func yaml_parser_scan_to_next_token(parser *yaml_parser_t) bool { // Scan a YAML-DIRECTIVE or TAG-DIRECTIVE token. // // Scope: +// %YAML 1.1 # a comment \n +// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +// %TAG !yaml! tag:yaml.org,2002: \n +// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ // -// %YAML 1.1 # a comment \n -// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -// %TAG !yaml! tag:yaml.org,2002: \n -// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ func yaml_parser_scan_directive(parser *yaml_parser_t, token *yaml_token_t) bool { // Eat '%'. start_mark := parser.mark @@ -1719,11 +1719,11 @@ func yaml_parser_scan_directive(parser *yaml_parser_t, token *yaml_token_t) bool // Scan the directive name. // // Scope: +// %YAML 1.1 # a comment \n +// ^^^^ +// %TAG !yaml! tag:yaml.org,2002: \n +// ^^^ // -// %YAML 1.1 # a comment \n -// ^^^^ -// %TAG !yaml! tag:yaml.org,2002: \n -// ^^^ func yaml_parser_scan_directive_name(parser *yaml_parser_t, start_mark yaml_mark_t, name *[]byte) bool { // Consume the directive name. if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { @@ -1758,9 +1758,8 @@ func yaml_parser_scan_directive_name(parser *yaml_parser_t, start_mark yaml_mark // Scan the value of VERSION-DIRECTIVE. // // Scope: -// -// %YAML 1.1 # a comment \n -// ^^^^^^ +// %YAML 1.1 # a comment \n +// ^^^^^^ func yaml_parser_scan_version_directive_value(parser *yaml_parser_t, start_mark yaml_mark_t, major, minor *int8) bool { // Eat whitespaces. if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { @@ -1798,11 +1797,10 @@ const max_number_length = 2 // Scan the version number of VERSION-DIRECTIVE. // // Scope: -// -// %YAML 1.1 # a comment \n -// ^ -// %YAML 1.1 # a comment \n -// ^ +// %YAML 1.1 # a comment \n +// ^ +// %YAML 1.1 # a comment \n +// ^ func yaml_parser_scan_version_directive_number(parser *yaml_parser_t, start_mark yaml_mark_t, number *int8) bool { // Repeat while the next character is digit. @@ -1836,9 +1834,9 @@ func yaml_parser_scan_version_directive_number(parser *yaml_parser_t, start_mark // Scan the value of a TAG-DIRECTIVE token. // // Scope: +// %TAG !yaml! tag:yaml.org,2002: \n +// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ // -// %TAG !yaml! tag:yaml.org,2002: \n -// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ func yaml_parser_scan_tag_directive_value(parser *yaml_parser_t, start_mark yaml_mark_t, handle, prefix *[]byte) bool { var handle_value, prefix_value []byte @@ -2849,7 +2847,7 @@ func yaml_parser_scan_line_comment(parser *yaml_parser_t, token_mark yaml_mark_t continue } if parser.buffer[parser.buffer_pos+peek] == '#' { - seen := parser.mark.index + peek + seen := parser.mark.index+peek for { if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { return false @@ -2878,7 +2876,7 @@ func yaml_parser_scan_line_comment(parser *yaml_parser_t, token_mark yaml_mark_t parser.comments = append(parser.comments, yaml_comment_t{ token_mark: token_mark, start_mark: start_mark, - line: text, + line: text, }) } return true @@ -2912,7 +2910,7 @@ func yaml_parser_scan_comments(parser *yaml_parser_t, scan_mark yaml_mark_t) boo // the foot is the line below it. var foot_line = -1 if scan_mark.line > 0 { - foot_line = parser.mark.line - parser.newlines + 1 + foot_line = parser.mark.line-parser.newlines+1 if parser.newlines == 0 && parser.mark.column > 1 { foot_line++ } @@ -2998,7 +2996,7 @@ func yaml_parser_scan_comments(parser *yaml_parser_t, scan_mark yaml_mark_t) boo recent_empty = false // Consume until after the consumed comment line. - seen := parser.mark.index + peek + seen := parser.mark.index+peek for { if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { return false diff --git a/vendor/gopkg.in/yaml.v3/writerc.go b/vendor/gopkg.in/yaml.v3/writerc.go index 266d0b0..b8a116b 100644 --- a/vendor/gopkg.in/yaml.v3/writerc.go +++ b/vendor/gopkg.in/yaml.v3/writerc.go @@ -1,17 +1,17 @@ -// +// // Copyright (c) 2011-2019 Canonical Ltd // Copyright (c) 2006-2010 Kirill Simonov -// +// // Permission is hereby granted, free of charge, to any person obtaining a copy of // this software and associated documentation files (the "Software"), to deal in // the Software without restriction, including without limitation the rights to // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies // of the Software, and to permit persons to whom the Software is furnished to do // so, subject to the following conditions: -// +// // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. -// +// // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE diff --git a/vendor/gopkg.in/yaml.v3/yaml.go b/vendor/gopkg.in/yaml.v3/yaml.go index f0bedf3..8cec6da 100644 --- a/vendor/gopkg.in/yaml.v3/yaml.go +++ b/vendor/gopkg.in/yaml.v3/yaml.go @@ -17,7 +17,8 @@ // // Source code and other details for the project are available at GitHub: // -// https://github.com/go-yaml/yaml +// https://github.com/go-yaml/yaml +// package yaml import ( @@ -74,15 +75,16 @@ type Marshaler interface { // // For example: // -// type T struct { -// F int `yaml:"a,omitempty"` -// B int -// } -// var t T -// yaml.Unmarshal([]byte("a: 1\nb: 2"), &t) +// type T struct { +// F int `yaml:"a,omitempty"` +// B int +// } +// var t T +// yaml.Unmarshal([]byte("a: 1\nb: 2"), &t) // // See the documentation of Marshal for the format of tags and a list of // supported tag options. +// func Unmarshal(in []byte, out interface{}) (err error) { return unmarshal(in, out, false) } @@ -183,35 +185,36 @@ func unmarshal(in []byte, out interface{}, strict bool) (err error) { // // The field tag format accepted is: // -// `(...) yaml:"[][,[,]]" (...)` +// `(...) yaml:"[][,[,]]" (...)` // // The following flags are currently supported: // -// omitempty Only include the field if it's not set to the zero -// value for the type or to empty slices or maps. -// Zero valued structs will be omitted if all their public -// fields are zero, unless they implement an IsZero -// method (see the IsZeroer interface type), in which -// case the field will be excluded if IsZero returns true. +// omitempty Only include the field if it's not set to the zero +// value for the type or to empty slices or maps. +// Zero valued structs will be omitted if all their public +// fields are zero, unless they implement an IsZero +// method (see the IsZeroer interface type), in which +// case the field will be excluded if IsZero returns true. // -// flow Marshal using a flow style (useful for structs, -// sequences and maps). +// flow Marshal using a flow style (useful for structs, +// sequences and maps). // -// inline Inline the field, which must be a struct or a map, -// causing all of its fields or keys to be processed as if -// they were part of the outer struct. For maps, keys must -// not conflict with the yaml keys of other struct fields. +// inline Inline the field, which must be a struct or a map, +// causing all of its fields or keys to be processed as if +// they were part of the outer struct. For maps, keys must +// not conflict with the yaml keys of other struct fields. // // In addition, if the key is "-", the field is ignored. // // For example: // -// type T struct { -// F int `yaml:"a,omitempty"` -// B int -// } -// yaml.Marshal(&T{B: 2}) // Returns "b: 2\n" -// yaml.Marshal(&T{F: 1}} // Returns "a: 1\nb: 0\n" +// type T struct { +// F int `yaml:"a,omitempty"` +// B int +// } +// yaml.Marshal(&T{B: 2}) // Returns "b: 2\n" +// yaml.Marshal(&T{F: 1}} // Returns "a: 1\nb: 0\n" +// func Marshal(in interface{}) (out []byte, err error) { defer handleErr(&err) e := newEncoder() @@ -355,21 +358,22 @@ const ( // // For example: // -// var person struct { -// Name string -// Address yaml.Node -// } -// err := yaml.Unmarshal(data, &person) -// +// var person struct { +// Name string +// Address yaml.Node +// } +// err := yaml.Unmarshal(data, &person) +// // Or by itself: // -// var person Node -// err := yaml.Unmarshal(data, &person) +// var person Node +// err := yaml.Unmarshal(data, &person) +// type Node struct { // Kind defines whether the node is a document, a mapping, a sequence, // a scalar value, or an alias to another node. The specific data type of // scalar nodes may be obtained via the ShortTag and LongTag methods. - Kind Kind + Kind Kind // Style allows customizing the apperance of the node in the tree. Style Style @@ -417,6 +421,7 @@ func (n *Node) IsZero() bool { n.HeadComment == "" && n.LineComment == "" && n.FootComment == "" && n.Line == 0 && n.Column == 0 } + // LongTag returns the long form of the tag that indicates the data type for // the node. If the Tag field isn't explicitly defined, one will be computed // based on the node properties. diff --git a/vendor/gopkg.in/yaml.v3/yamlh.go b/vendor/gopkg.in/yaml.v3/yamlh.go index ddcd551..7c6d007 100644 --- a/vendor/gopkg.in/yaml.v3/yamlh.go +++ b/vendor/gopkg.in/yaml.v3/yamlh.go @@ -438,9 +438,7 @@ type yaml_document_t struct { // The number of written bytes should be set to the size_read variable. // // [in,out] data A pointer to an application data specified by -// -// yaml_parser_set_input(). -// +// yaml_parser_set_input(). // [out] buffer The buffer to write the data from the source. // [in] size The size of the buffer. // [out] size_read The actual number of bytes read from the source. @@ -641,6 +639,7 @@ type yaml_parser_t struct { } type yaml_comment_t struct { + scan_mark yaml_mark_t // Position where scanning for comments started token_mark yaml_mark_t // Position after which tokens will be associated with this comment start_mark yaml_mark_t // Position of '#' comment mark @@ -660,14 +659,13 @@ type yaml_comment_t struct { // @a buffer to the output. // // @param[in,out] data A pointer to an application data specified by -// -// yaml_emitter_set_output(). -// +// yaml_emitter_set_output(). // @param[in] buffer The buffer with bytes to be written. // @param[in] size The size of the buffer. // // @returns On success, the handler should return @c 1. If the handler failed, // the returned value should be @c 0. +// type yaml_write_handler_t func(emitter *yaml_emitter_t, buffer []byte) error type yaml_emitter_state_t int diff --git a/vendor/gopkg.in/yaml.v3/yamlprivateh.go b/vendor/gopkg.in/yaml.v3/yamlprivateh.go index dea1ba9..e88f9c5 100644 --- a/vendor/gopkg.in/yaml.v3/yamlprivateh.go +++ b/vendor/gopkg.in/yaml.v3/yamlprivateh.go @@ -1,17 +1,17 @@ -// +// // Copyright (c) 2011-2019 Canonical Ltd // Copyright (c) 2006-2010 Kirill Simonov -// +// // Permission is hereby granted, free of charge, to any person obtaining a copy of // this software and associated documentation files (the "Software"), to deal in // the Software without restriction, including without limitation the rights to // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies // of the Software, and to permit persons to whom the Software is furnished to do // so, subject to the following conditions: -// +// // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. -// +// // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -137,8 +137,8 @@ func is_crlf(b []byte, i int) bool { func is_breakz(b []byte, i int) bool { //return is_break(b, i) || is_z(b, i) return ( - // is_break: - b[i] == '\r' || // CR (#xD) + // is_break: + b[i] == '\r' || // CR (#xD) b[i] == '\n' || // LF (#xA) b[i] == 0xC2 && b[i+1] == 0x85 || // NEL (#x85) b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA8 || // LS (#x2028) @@ -151,8 +151,8 @@ func is_breakz(b []byte, i int) bool { func is_spacez(b []byte, i int) bool { //return is_space(b, i) || is_breakz(b, i) return ( - // is_space: - b[i] == ' ' || + // is_space: + b[i] == ' ' || // is_breakz: b[i] == '\r' || // CR (#xD) b[i] == '\n' || // LF (#xA) @@ -166,8 +166,8 @@ func is_spacez(b []byte, i int) bool { func is_blankz(b []byte, i int) bool { //return is_blank(b, i) || is_breakz(b, i) return ( - // is_blank: - b[i] == ' ' || b[i] == '\t' || + // is_blank: + b[i] == ' ' || b[i] == '\t' || // is_breakz: b[i] == '\r' || // CR (#xD) b[i] == '\n' || // LF (#xA) diff --git a/vendor/modules.txt b/vendor/modules.txt index af3bce6..0701883 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -18,9 +18,6 @@ github.com/davecgh/go-spew/spew # github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f ## explicit github.com/dgryski/go-rendezvous -# github.com/google/uuid v1.6.0 -## explicit -github.com/google/uuid # github.com/gorilla/securecookie v1.1.2 ## explicit; go 1.20 github.com/gorilla/securecookie