mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-15 04:18:03 +00:00
fixup! Cleanup.
This commit is contained in:
+341
-9
@@ -7,8 +7,9 @@ summary: |
|
|||||||
Universal OpenID Connect (OIDC) authentication middleware for Traefik.
|
Universal OpenID Connect (OIDC) authentication middleware for Traefik.
|
||||||
|
|
||||||
This middleware replaces the need for forward-auth and oauth2-proxy when using Traefik as a reverse proxy.
|
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 like domain restrictions,
|
It provides a complete OIDC authentication solution with features including domain restrictions,
|
||||||
role-based access control, token caching, session management, and memory-efficient operation.
|
role-based access control, session management, comprehensive security headers, automatic token refresh,
|
||||||
|
and support for all major OIDC providers with automatic configuration.
|
||||||
|
|
||||||
🎯 SUPPORTED PROVIDERS (Auto-Detection):
|
🎯 SUPPORTED PROVIDERS (Auto-Detection):
|
||||||
✅ Google - Full OIDC, auto-configured for Workspace
|
✅ Google - Full OIDC, auto-configured for Workspace
|
||||||
@@ -23,19 +24,26 @@ summary: |
|
|||||||
|
|
||||||
🔧 KEY FEATURES:
|
🔧 KEY FEATURES:
|
||||||
- Automatic provider detection and configuration
|
- Automatic provider detection and configuration
|
||||||
- Built-in warnings for provider limitations
|
- Comprehensive security headers (CSP, HSTS, CORS, custom profiles)
|
||||||
- Memory-efficient with bounded resource usage
|
- Domain restrictions and role-based access control
|
||||||
- Comprehensive test coverage and security monitoring
|
- 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
|
||||||
It supports various authentication scenarios including:
|
It supports various authentication scenarios including:
|
||||||
|
|
||||||
- Basic authentication with customizable callback and logout URLs
|
- Basic authentication with customizable callback and logout URLs
|
||||||
- Email domain restrictions to limit access to specific organizations
|
- Email domain restrictions to limit access to specific organizations
|
||||||
- Role and group-based access control
|
- Role and group-based access control based on OIDC claims
|
||||||
- Public URLs that bypass authentication
|
- Public URLs that bypass authentication (excluded paths)
|
||||||
- Rate limiting to prevent brute force attacks
|
|
||||||
- Custom post-logout redirect behavior
|
|
||||||
- Secure session management with encrypted cookies
|
- Secure session management with encrypted cookies
|
||||||
- Automatic token validation and refresh
|
- 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
|
||||||
|
|
||||||
testData:
|
testData:
|
||||||
# Required parameters
|
# Required parameters
|
||||||
@@ -95,6 +103,78 @@ testData:
|
|||||||
overrideScopes: false # When true, replaces default scopes instead of appending (default: false)
|
overrideScopes: false # When true, replaces default scopes instead of appending (default: false)
|
||||||
refreshGracePeriodSeconds: 60 # Seconds before token expiry to attempt proactive refresh (default: 60)
|
refreshGracePeriodSeconds: 60 # Seconds before token expiry to attempt proactive refresh (default: 60)
|
||||||
|
|
||||||
|
# Security Headers Configuration (enabled by default with 'default' profile)
|
||||||
|
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
|
||||||
|
|
||||||
|
# Custom headers
|
||||||
|
customHeaders:
|
||||||
|
X-Custom-Header: "production"
|
||||||
|
X-API-Version: "v1"
|
||||||
|
|
||||||
|
# --- 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
|
||||||
|
# - name: "X-User-Email"
|
||||||
|
# value: "{{.Claims.email}}"
|
||||||
|
# - name: "X-User-ID"
|
||||||
|
# value: "{{.Claims.sub}}"
|
||||||
|
|
||||||
# --- Provider Specific Configuration Examples ---
|
# --- Provider Specific Configuration Examples ---
|
||||||
#
|
#
|
||||||
# This middleware supports 9+ OIDC providers with automatic detection:
|
# This middleware supports 9+ OIDC providers with automatic detection:
|
||||||
@@ -567,3 +647,255 @@ configuration:
|
|||||||
value:
|
value:
|
||||||
type: string
|
type: string
|
||||||
description: Template string for the header value
|
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
|
||||||
|
|||||||
@@ -4,17 +4,18 @@ This middleware replaces the need for forward-auth and oauth2-proxy when using T
|
|||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
The Traefik OIDC middleware provides a complete OIDC authentication solution with features like:
|
The Traefik OIDC middleware provides a complete OIDC authentication solution with these key features:
|
||||||
|
|
||||||
- **Universal provider support**: Works with 9+ OIDC providers including Google, Azure AD, Auth0, Okta, Keycloak, AWS Cognito, GitLab, and more
|
- **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
|
- **Automatic provider detection**: Automatically detects and configures provider-specific settings
|
||||||
- Token validation and verification
|
- **Security headers**: Comprehensive security headers with CORS, CSP, HSTS, and custom profiles
|
||||||
- Session management with automatic cleanup
|
- **Domain restrictions**: Limit access to specific email domains or individual users
|
||||||
- Domain restrictions
|
- **Role-based access control**: Restrict access based on roles and groups from OIDC claims
|
||||||
- Role-based access control
|
- **Session management**: Secure session handling with automatic token refresh
|
||||||
- Token caching and blacklisting
|
- **Rate limiting**: Protection against brute force attacks
|
||||||
- Rate limiting
|
- **Excluded paths**: Configure public URLs that bypass authentication
|
||||||
- Excluded paths (public URLs)
|
- **Custom headers**: Template-based headers using OIDC claims and tokens
|
||||||
- Memory-efficient operation with bounded resource usage
|
- **Comprehensive logging**: Configurable log levels for debugging and monitoring
|
||||||
|
|
||||||
## Supported OIDC Providers
|
## Supported OIDC Providers
|
||||||
|
|
||||||
@@ -36,6 +37,7 @@ The Traefik OIDC middleware provides a complete OIDC authentication solution wit
|
|||||||
|---------|--------|----------|-------|------|----------|---------|--------|--------|---------|
|
|---------|--------|----------|-------|------|----------|---------|--------|--------|---------|
|
||||||
| **ID Tokens** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
|
| **ID Tokens** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
|
||||||
| **Refresh Tokens** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
|
| **Refresh Tokens** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
|
||||||
|
| **Auto-Configuration** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
| **Custom Claims** | Limited | ✅ | ✅ | ✅ | ✅ | ✅ | Limited | ❌ | Varies |
|
| **Custom Claims** | Limited | ✅ | ✅ | ✅ | ✅ | ✅ | Limited | ❌ | Varies |
|
||||||
| **Group/Role Claims** | Limited | ✅ | ✅ | ✅ | ✅ | ✅ | Limited | ❌ | Varies |
|
| **Group/Role Claims** | Limited | ✅ | ✅ | ✅ | ✅ | ✅ | Limited | ❌ | Varies |
|
||||||
| **Domain Restriction** | ✅ (hd claim) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | Varies |
|
| **Domain Restriction** | ✅ (hd claim) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | Varies |
|
||||||
@@ -124,6 +126,7 @@ The middleware supports the following configuration options:
|
|||||||
| `refreshGracePeriodSeconds` | Seconds before token expiry to attempt proactive refresh | `60` | `120` |
|
| `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` |
|
| `cookieDomain` | Explicit domain for session cookies (important for multi-subdomain setups) | auto-detected | `.example.com`, `app.example.com` |
|
||||||
| `headers` | Custom HTTP headers with templates that can access OIDC claims and tokens | none | See "Templated Headers" section |
|
| `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 |
|
||||||
|
|
||||||
## Scope Configuration
|
## Scope Configuration
|
||||||
|
|
||||||
@@ -198,6 +201,195 @@ scopes: []
|
|||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
## 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` |
|
||||||
|
|
||||||
|
### 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:*"]
|
||||||
|
```
|
||||||
|
|
||||||
## Usage Examples
|
## Usage Examples
|
||||||
|
|
||||||
### Basic Configuration
|
### Basic Configuration
|
||||||
@@ -1232,44 +1424,46 @@ For detailed provider-specific guidance, see the [Provider-Specific Configuratio
|
|||||||
|
|
||||||
## Recent Improvements
|
## Recent Improvements
|
||||||
|
|
||||||
### Memory Management (v0.3.0+)
|
### Security Features (v0.4.0+)
|
||||||
|
|
||||||
The middleware has undergone significant improvements to memory management and resource utilization:
|
- **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
|
||||||
|
|
||||||
- **Memory Leak Prevention**: All background goroutines are properly managed with context cancellation
|
### User Experience (v0.4.0+)
|
||||||
- **Bounded Resource Usage**: Session storage, metadata cache, and token cache all have size limits with LRU eviction
|
|
||||||
- **Automatic Cleanup**: Expired sessions and tokens are automatically cleaned up by background tasks
|
|
||||||
- **Graceful Shutdown**: All resources are properly released when the middleware is stopped
|
|
||||||
- **Performance Monitoring**: Built-in monitoring for goroutine leaks and memory growth
|
|
||||||
|
|
||||||
These improvements ensure the middleware operates efficiently even under high load and long-running deployments.
|
- **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
|
||||||
|
|
||||||
### Enhanced Test Coverage
|
### Reliability (v0.4.0+)
|
||||||
|
|
||||||
- Comprehensive test suite with race condition detection
|
- **Automatic Token Refresh**: Proactive token refresh to prevent authentication interruptions
|
||||||
- Memory leak detection tests
|
- **Memory Management**: Improved memory efficiency and automatic resource cleanup
|
||||||
- Goroutine leak prevention tests
|
- **Better Provider Support**: Enhanced compatibility with provider-specific features
|
||||||
- Test coverage increased to 67%+ for main package, 87-99% for subpackages
|
- **Comprehensive Testing**: Extensive test coverage ensures reliability in production
|
||||||
|
|
||||||
## Architecture and Internal Improvements
|
## Architecture Overview
|
||||||
|
|
||||||
### Internal Components
|
### Design Principles
|
||||||
|
|
||||||
The middleware uses several internal components for efficient operation:
|
The middleware is designed with the following principles:
|
||||||
|
|
||||||
1. **SessionManager**: Manages user sessions with automatic cleanup and pool-based allocation
|
- **Reliability**: Automatic error recovery and graceful degradation
|
||||||
2. **ChunkManager**: Handles large session data by splitting it into manageable chunks
|
- **Security**: Comprehensive security measures and validation
|
||||||
3. **MetadataCache**: Caches OIDC provider metadata with LRU eviction and size limits
|
- **Performance**: Efficient resource usage and caching
|
||||||
4. **TaskRegistry**: Manages background tasks with proper lifecycle management
|
- **Flexibility**: Extensive configuration options for different use cases
|
||||||
5. **MemoryMonitor**: Monitors memory usage and detects potential leaks
|
- **Compatibility**: Support for all major OIDC providers with automatic detection
|
||||||
|
|
||||||
### Key Design Decisions
|
### Key Features
|
||||||
|
|
||||||
- **Context-based cancellation**: All background operations use context for clean shutdown
|
- **Automatic Session Management**: Handles session lifecycle, cleanup, and security
|
||||||
- **Bounded queues and caches**: Prevents unbounded memory growth
|
- **Provider Integration**: Seamless integration with OIDC providers including auto-discovery
|
||||||
- **LRU eviction policies**: Ensures most frequently used data stays in cache
|
- **Security Integration**: Built-in security headers and protection mechanisms
|
||||||
- **Atomic operations**: Uses atomic counters for statistics to avoid lock contention
|
- **Resource Management**: Efficient memory usage and automatic cleanup
|
||||||
- **Test-friendly design**: Special handling for test environments to ensure clean test execution
|
- **Error Handling**: Comprehensive error recovery and user-friendly error messages
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
|
|||||||
+355
-21
@@ -2,7 +2,10 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -44,27 +47,28 @@ type Logger interface {
|
|||||||
|
|
||||||
// Config represents the configuration for the OIDC middleware
|
// Config represents the configuration for the OIDC middleware
|
||||||
type Config struct {
|
type Config struct {
|
||||||
ProviderURL string `json:"providerUrl"`
|
ProviderURL string `json:"providerUrl"`
|
||||||
ClientID string `json:"clientId"`
|
ClientID string `json:"clientId"`
|
||||||
ClientSecret string `json:"clientSecret"`
|
ClientSecret string `json:"clientSecret"`
|
||||||
CallbackURL string `json:"callbackUrl"`
|
CallbackURL string `json:"callbackUrl"`
|
||||||
LogoutURL string `json:"logoutUrl"`
|
LogoutURL string `json:"logoutUrl"`
|
||||||
PostLogoutRedirectURI string `json:"postLogoutRedirectUri"`
|
PostLogoutRedirectURI string `json:"postLogoutRedirectUri"`
|
||||||
SessionEncryptionKey string `json:"sessionEncryptionKey"`
|
SessionEncryptionKey string `json:"sessionEncryptionKey"`
|
||||||
ForceHTTPS bool `json:"forceHttps"`
|
ForceHTTPS bool `json:"forceHttps"`
|
||||||
LogLevel string `json:"logLevel"`
|
LogLevel string `json:"logLevel"`
|
||||||
Scopes []string `json:"scopes"`
|
Scopes []string `json:"scopes"`
|
||||||
OverrideScopes bool `json:"overrideScopes"`
|
OverrideScopes bool `json:"overrideScopes"`
|
||||||
AllowedUsers []string `json:"allowedUsers"`
|
AllowedUsers []string `json:"allowedUsers"`
|
||||||
AllowedUserDomains []string `json:"allowedUserDomains"`
|
AllowedUserDomains []string `json:"allowedUserDomains"`
|
||||||
AllowedRolesAndGroups []string `json:"allowedRolesAndGroups"`
|
AllowedRolesAndGroups []string `json:"allowedRolesAndGroups"`
|
||||||
ExcludedURLs []string `json:"excludedUrls"`
|
ExcludedURLs []string `json:"excludedUrls"`
|
||||||
EnablePKCE bool `json:"enablePkce"`
|
EnablePKCE bool `json:"enablePkce"`
|
||||||
RateLimit int `json:"rateLimit"`
|
RateLimit int `json:"rateLimit"`
|
||||||
RefreshGracePeriodSeconds int `json:"refreshGracePeriodSeconds"`
|
RefreshGracePeriodSeconds int `json:"refreshGracePeriodSeconds"`
|
||||||
Headers []HeaderConfig `json:"headers"`
|
Headers []HeaderConfig `json:"headers"`
|
||||||
HTTPClient *http.Client `json:"-"`
|
HTTPClient *http.Client `json:"-"`
|
||||||
CookieDomain string `json:"cookieDomain"`
|
CookieDomain string `json:"cookieDomain"`
|
||||||
|
SecurityHeaders *SecurityHeadersConfig `json:"securityHeaders,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// HeaderConfig represents header template configuration
|
// HeaderConfig represents header template configuration
|
||||||
@@ -73,6 +77,59 @@ type HeaderConfig struct {
|
|||||||
Value string `json:"value"`
|
Value string `json:"value"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SecurityHeadersConfig configures security headers for the plugin
|
||||||
|
type SecurityHeadersConfig struct {
|
||||||
|
// Enable security headers (default: true)
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
|
||||||
|
// Security profile: "default", "strict", "development", "api", or "custom"
|
||||||
|
Profile string `json:"profile"`
|
||||||
|
|
||||||
|
// Content Security Policy
|
||||||
|
ContentSecurityPolicy string `json:"contentSecurityPolicy,omitempty"`
|
||||||
|
|
||||||
|
// HSTS settings
|
||||||
|
StrictTransportSecurity bool `json:"strictTransportSecurity"`
|
||||||
|
StrictTransportSecurityMaxAge int `json:"strictTransportSecurityMaxAge"` // seconds
|
||||||
|
StrictTransportSecuritySubdomains bool `json:"strictTransportSecuritySubdomains"`
|
||||||
|
StrictTransportSecurityPreload bool `json:"strictTransportSecurityPreload"`
|
||||||
|
|
||||||
|
// Frame options: "DENY", "SAMEORIGIN", or "ALLOW-FROM uri"
|
||||||
|
FrameOptions string `json:"frameOptions,omitempty"`
|
||||||
|
|
||||||
|
// Content type options (default: "nosniff")
|
||||||
|
ContentTypeOptions string `json:"contentTypeOptions,omitempty"`
|
||||||
|
|
||||||
|
// XSS protection (default: "1; mode=block")
|
||||||
|
XSSProtection string `json:"xssProtection,omitempty"`
|
||||||
|
|
||||||
|
// Referrer policy
|
||||||
|
ReferrerPolicy string `json:"referrerPolicy,omitempty"`
|
||||||
|
|
||||||
|
// Permissions policy
|
||||||
|
PermissionsPolicy string `json:"permissionsPolicy,omitempty"`
|
||||||
|
|
||||||
|
// Cross-origin settings
|
||||||
|
CrossOriginEmbedderPolicy string `json:"crossOriginEmbedderPolicy,omitempty"`
|
||||||
|
CrossOriginOpenerPolicy string `json:"crossOriginOpenerPolicy,omitempty"`
|
||||||
|
CrossOriginResourcePolicy string `json:"crossOriginResourcePolicy,omitempty"`
|
||||||
|
|
||||||
|
// CORS settings
|
||||||
|
CORSEnabled bool `json:"corsEnabled"`
|
||||||
|
CORSAllowedOrigins []string `json:"corsAllowedOrigins,omitempty"`
|
||||||
|
CORSAllowedMethods []string `json:"corsAllowedMethods,omitempty"`
|
||||||
|
CORSAllowedHeaders []string `json:"corsAllowedHeaders,omitempty"`
|
||||||
|
CORSAllowCredentials bool `json:"corsAllowCredentials"`
|
||||||
|
CORSMaxAge int `json:"corsMaxAge"` // seconds
|
||||||
|
|
||||||
|
// Custom headers (in addition to standard security headers)
|
||||||
|
CustomHeaders map[string]string `json:"customHeaders,omitempty"`
|
||||||
|
|
||||||
|
// Security features
|
||||||
|
DisableServerHeader bool `json:"disableServerHeader"`
|
||||||
|
DisablePoweredByHeader bool `json:"disablePoweredByHeader"`
|
||||||
|
}
|
||||||
|
|
||||||
// NewSettings creates a new Settings instance
|
// NewSettings creates a new Settings instance
|
||||||
func NewSettings(logger Logger) *Settings {
|
func NewSettings(logger Logger) *Settings {
|
||||||
return &Settings{
|
return &Settings{
|
||||||
@@ -90,5 +147,282 @@ func CreateConfig() *Config {
|
|||||||
RefreshGracePeriodSeconds: 60,
|
RefreshGracePeriodSeconds: 60,
|
||||||
Scopes: []string{"openid", "profile", "email"},
|
Scopes: []string{"openid", "profile", "email"},
|
||||||
Headers: []HeaderConfig{},
|
Headers: []HeaderConfig{},
|
||||||
|
SecurityHeaders: createDefaultSecurityConfig(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// createDefaultSecurityConfig creates a default security headers configuration
|
||||||
|
func createDefaultSecurityConfig() *SecurityHeadersConfig {
|
||||||
|
return &SecurityHeadersConfig{
|
||||||
|
Enabled: true,
|
||||||
|
Profile: "default",
|
||||||
|
|
||||||
|
// Default security headers
|
||||||
|
StrictTransportSecurity: true,
|
||||||
|
StrictTransportSecurityMaxAge: 31536000, // 1 year
|
||||||
|
StrictTransportSecuritySubdomains: true,
|
||||||
|
StrictTransportSecurityPreload: true,
|
||||||
|
|
||||||
|
FrameOptions: "DENY",
|
||||||
|
ContentTypeOptions: "nosniff",
|
||||||
|
XSSProtection: "1; mode=block",
|
||||||
|
ReferrerPolicy: "strict-origin-when-cross-origin",
|
||||||
|
|
||||||
|
// CORS disabled by default
|
||||||
|
CORSEnabled: false,
|
||||||
|
CORSAllowedMethods: []string{"GET", "POST", "OPTIONS"},
|
||||||
|
CORSAllowedHeaders: []string{"Authorization", "Content-Type"},
|
||||||
|
CORSAllowCredentials: false,
|
||||||
|
CORSMaxAge: 86400, // 24 hours
|
||||||
|
|
||||||
|
// Security features
|
||||||
|
DisableServerHeader: true,
|
||||||
|
DisablePoweredByHeader: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToInternalSecurityConfig converts plugin SecurityHeadersConfig to internal security config
|
||||||
|
func (c *SecurityHeadersConfig) ToInternalSecurityConfig() interface{} {
|
||||||
|
if c == nil || !c.Enabled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the internal security config structure
|
||||||
|
config := map[string]interface{}{
|
||||||
|
"DevelopmentMode": false,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply profile-based defaults
|
||||||
|
switch strings.ToLower(c.Profile) {
|
||||||
|
case "strict":
|
||||||
|
applyStrictProfile(config)
|
||||||
|
case "development":
|
||||||
|
applyDevelopmentProfile(config)
|
||||||
|
case "api":
|
||||||
|
applyAPIProfile(config)
|
||||||
|
case "custom":
|
||||||
|
// No defaults, use only what's explicitly configured
|
||||||
|
default: // "default"
|
||||||
|
applyDefaultProfile(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override with explicit configuration
|
||||||
|
if c.ContentSecurityPolicy != "" {
|
||||||
|
config["ContentSecurityPolicy"] = c.ContentSecurityPolicy
|
||||||
|
}
|
||||||
|
|
||||||
|
// HSTS configuration
|
||||||
|
if c.StrictTransportSecurity {
|
||||||
|
config["StrictTransportSecurityMaxAge"] = c.StrictTransportSecurityMaxAge
|
||||||
|
config["StrictTransportSecuritySubdomains"] = c.StrictTransportSecuritySubdomains
|
||||||
|
config["StrictTransportSecurityPreload"] = c.StrictTransportSecurityPreload
|
||||||
|
}
|
||||||
|
|
||||||
|
// Frame options
|
||||||
|
if c.FrameOptions != "" {
|
||||||
|
config["FrameOptions"] = c.FrameOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
// Content type and XSS protection
|
||||||
|
if c.ContentTypeOptions != "" {
|
||||||
|
config["ContentTypeOptions"] = c.ContentTypeOptions
|
||||||
|
}
|
||||||
|
if c.XSSProtection != "" {
|
||||||
|
config["XSSProtection"] = c.XSSProtection
|
||||||
|
}
|
||||||
|
|
||||||
|
// Referrer and permissions policies
|
||||||
|
if c.ReferrerPolicy != "" {
|
||||||
|
config["ReferrerPolicy"] = c.ReferrerPolicy
|
||||||
|
}
|
||||||
|
if c.PermissionsPolicy != "" {
|
||||||
|
config["PermissionsPolicy"] = c.PermissionsPolicy
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cross-origin policies
|
||||||
|
if c.CrossOriginEmbedderPolicy != "" {
|
||||||
|
config["CrossOriginEmbedderPolicy"] = c.CrossOriginEmbedderPolicy
|
||||||
|
}
|
||||||
|
if c.CrossOriginOpenerPolicy != "" {
|
||||||
|
config["CrossOriginOpenerPolicy"] = c.CrossOriginOpenerPolicy
|
||||||
|
}
|
||||||
|
if c.CrossOriginResourcePolicy != "" {
|
||||||
|
config["CrossOriginResourcePolicy"] = c.CrossOriginResourcePolicy
|
||||||
|
}
|
||||||
|
|
||||||
|
// CORS configuration
|
||||||
|
config["CORSEnabled"] = c.CORSEnabled
|
||||||
|
if len(c.CORSAllowedOrigins) > 0 {
|
||||||
|
config["CORSAllowedOrigins"] = c.CORSAllowedOrigins
|
||||||
|
}
|
||||||
|
if len(c.CORSAllowedMethods) > 0 {
|
||||||
|
config["CORSAllowedMethods"] = c.CORSAllowedMethods
|
||||||
|
}
|
||||||
|
if len(c.CORSAllowedHeaders) > 0 {
|
||||||
|
config["CORSAllowedHeaders"] = c.CORSAllowedHeaders
|
||||||
|
}
|
||||||
|
config["CORSAllowCredentials"] = c.CORSAllowCredentials
|
||||||
|
if c.CORSMaxAge > 0 {
|
||||||
|
config["CORSMaxAge"] = c.CORSMaxAge
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom headers
|
||||||
|
if len(c.CustomHeaders) > 0 {
|
||||||
|
config["CustomHeaders"] = c.CustomHeaders
|
||||||
|
}
|
||||||
|
|
||||||
|
// Security features
|
||||||
|
config["DisableServerHeader"] = c.DisableServerHeader
|
||||||
|
config["DisablePoweredByHeader"] = c.DisablePoweredByHeader
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyDefaultProfile applies default security settings
|
||||||
|
func applyDefaultProfile(config map[string]interface{}) {
|
||||||
|
config["ContentSecurityPolicy"] = "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'none';"
|
||||||
|
config["FrameOptions"] = "DENY"
|
||||||
|
config["ContentTypeOptions"] = "nosniff"
|
||||||
|
config["XSSProtection"] = "1; mode=block"
|
||||||
|
config["ReferrerPolicy"] = "strict-origin-when-cross-origin"
|
||||||
|
config["PermissionsPolicy"] = "geolocation=(), microphone=(), camera=(), payment=(), usb=()"
|
||||||
|
config["CrossOriginEmbedderPolicy"] = "require-corp"
|
||||||
|
config["CrossOriginOpenerPolicy"] = "same-origin"
|
||||||
|
config["CrossOriginResourcePolicy"] = "same-origin"
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyStrictProfile applies strict security settings
|
||||||
|
func applyStrictProfile(config map[string]interface{}) {
|
||||||
|
config["ContentSecurityPolicy"] = "default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self'; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self';"
|
||||||
|
config["FrameOptions"] = "DENY"
|
||||||
|
config["ContentTypeOptions"] = "nosniff"
|
||||||
|
config["XSSProtection"] = "1; mode=block"
|
||||||
|
config["ReferrerPolicy"] = "strict-origin-when-cross-origin"
|
||||||
|
config["PermissionsPolicy"] = "geolocation=(), microphone=(), camera=(), payment=(), usb=(), magnetometer=(), gyroscope=(), speaker=()"
|
||||||
|
config["CrossOriginEmbedderPolicy"] = "require-corp"
|
||||||
|
config["CrossOriginOpenerPolicy"] = "same-origin"
|
||||||
|
config["CrossOriginResourcePolicy"] = "same-site"
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyDevelopmentProfile applies development-friendly settings
|
||||||
|
func applyDevelopmentProfile(config map[string]interface{}) {
|
||||||
|
config["ContentSecurityPolicy"] = "default-src 'self' 'unsafe-inline' 'unsafe-eval'; img-src 'self' data: https: http:; connect-src 'self' ws: wss:;"
|
||||||
|
config["FrameOptions"] = "SAMEORIGIN"
|
||||||
|
config["ContentTypeOptions"] = "nosniff"
|
||||||
|
config["XSSProtection"] = "1; mode=block"
|
||||||
|
config["ReferrerPolicy"] = "strict-origin-when-cross-origin"
|
||||||
|
config["CrossOriginOpenerPolicy"] = "unsafe-none"
|
||||||
|
config["CrossOriginResourcePolicy"] = "cross-origin"
|
||||||
|
config["DevelopmentMode"] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyAPIProfile applies API-friendly settings
|
||||||
|
func applyAPIProfile(config map[string]interface{}) {
|
||||||
|
config["ContentSecurityPolicy"] = "default-src 'none'; frame-ancestors 'none';"
|
||||||
|
config["FrameOptions"] = "DENY"
|
||||||
|
config["ContentTypeOptions"] = "nosniff"
|
||||||
|
config["XSSProtection"] = "1; mode=block"
|
||||||
|
config["ReferrerPolicy"] = "strict-origin-when-cross-origin"
|
||||||
|
config["CrossOriginResourcePolicy"] = "cross-origin"
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSecurityHeadersApplier returns a function that applies security headers
|
||||||
|
func (c *Config) GetSecurityHeadersApplier() func(http.ResponseWriter, *http.Request) {
|
||||||
|
if c.SecurityHeaders == nil || !c.SecurityHeaders.Enabled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// This would need to import the internal security package
|
||||||
|
// For now, return a basic implementation
|
||||||
|
return func(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
headers := rw.Header()
|
||||||
|
|
||||||
|
// Apply basic security headers based on configuration
|
||||||
|
if c.SecurityHeaders.FrameOptions != "" {
|
||||||
|
headers.Set("X-Frame-Options", c.SecurityHeaders.FrameOptions)
|
||||||
|
}
|
||||||
|
if c.SecurityHeaders.ContentTypeOptions != "" {
|
||||||
|
headers.Set("X-Content-Type-Options", c.SecurityHeaders.ContentTypeOptions)
|
||||||
|
}
|
||||||
|
if c.SecurityHeaders.XSSProtection != "" {
|
||||||
|
headers.Set("X-XSS-Protection", c.SecurityHeaders.XSSProtection)
|
||||||
|
}
|
||||||
|
if c.SecurityHeaders.ReferrerPolicy != "" {
|
||||||
|
headers.Set("Referrer-Policy", c.SecurityHeaders.ReferrerPolicy)
|
||||||
|
}
|
||||||
|
if c.SecurityHeaders.ContentSecurityPolicy != "" {
|
||||||
|
headers.Set("Content-Security-Policy", c.SecurityHeaders.ContentSecurityPolicy)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HSTS for HTTPS
|
||||||
|
if (req.TLS != nil || req.Header.Get("X-Forwarded-Proto") == "https") && c.SecurityHeaders.StrictTransportSecurity {
|
||||||
|
hstsValue := fmt.Sprintf("max-age=%d", c.SecurityHeaders.StrictTransportSecurityMaxAge)
|
||||||
|
if c.SecurityHeaders.StrictTransportSecuritySubdomains {
|
||||||
|
hstsValue += "; includeSubDomains"
|
||||||
|
}
|
||||||
|
if c.SecurityHeaders.StrictTransportSecurityPreload {
|
||||||
|
hstsValue += "; preload"
|
||||||
|
}
|
||||||
|
headers.Set("Strict-Transport-Security", hstsValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CORS headers
|
||||||
|
if c.SecurityHeaders.CORSEnabled {
|
||||||
|
origin := req.Header.Get("Origin")
|
||||||
|
if origin != "" && isOriginAllowed(origin, c.SecurityHeaders.CORSAllowedOrigins) {
|
||||||
|
headers.Set("Access-Control-Allow-Origin", origin)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(c.SecurityHeaders.CORSAllowedMethods) > 0 {
|
||||||
|
headers.Set("Access-Control-Allow-Methods", strings.Join(c.SecurityHeaders.CORSAllowedMethods, ", "))
|
||||||
|
}
|
||||||
|
if len(c.SecurityHeaders.CORSAllowedHeaders) > 0 {
|
||||||
|
headers.Set("Access-Control-Allow-Headers", strings.Join(c.SecurityHeaders.CORSAllowedHeaders, ", "))
|
||||||
|
}
|
||||||
|
if c.SecurityHeaders.CORSAllowCredentials {
|
||||||
|
headers.Set("Access-Control-Allow-Credentials", "true")
|
||||||
|
}
|
||||||
|
if c.SecurityHeaders.CORSMaxAge > 0 {
|
||||||
|
headers.Set("Access-Control-Max-Age", strconv.Itoa(c.SecurityHeaders.CORSMaxAge))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom headers
|
||||||
|
for name, value := range c.SecurityHeaders.CustomHeaders {
|
||||||
|
headers.Set(name, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove server headers
|
||||||
|
if c.SecurityHeaders.DisableServerHeader {
|
||||||
|
headers.Del("Server")
|
||||||
|
}
|
||||||
|
if c.SecurityHeaders.DisablePoweredByHeader {
|
||||||
|
headers.Del("X-Powered-By")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// isOriginAllowed checks if an origin is in the allowed list
|
||||||
|
func isOriginAllowed(origin string, allowedOrigins []string) bool {
|
||||||
|
for _, allowed := range allowedOrigins {
|
||||||
|
if origin == allowed || allowed == "*" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// Simple wildcard matching for subdomains
|
||||||
|
if strings.Contains(allowed, "*") {
|
||||||
|
if strings.HasPrefix(allowed, "https://*.") {
|
||||||
|
domain := strings.TrimPrefix(allowed, "https://*.")
|
||||||
|
if strings.HasSuffix(origin, "."+domain) || origin == "https://"+domain {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(allowed, "http://*.") {
|
||||||
|
domain := strings.TrimPrefix(allowed, "http://*.")
|
||||||
|
if strings.HasSuffix(origin, "."+domain) || origin == "http://"+domain {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,770 @@
|
|||||||
|
# Provider-Specific Configuration Guide
|
||||||
|
|
||||||
|
This guide covers the configuration requirements and best practices for each supported OIDC provider.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [Google](#google)
|
||||||
|
- [Microsoft Azure AD](#microsoft-azure-ad)
|
||||||
|
- [Auth0](#auth0)
|
||||||
|
- [GitHub](#github)
|
||||||
|
- [GitLab](#gitlab)
|
||||||
|
- [AWS Cognito](#aws-cognito)
|
||||||
|
- [Keycloak](#keycloak)
|
||||||
|
- [Okta](#okta)
|
||||||
|
- [Generic OIDC](#generic-oidc)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Google
|
||||||
|
|
||||||
|
### Provider URL
|
||||||
|
```yaml
|
||||||
|
providerUrl: "https://accounts.google.com"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Required Configuration
|
||||||
|
```yaml
|
||||||
|
clientId: "your-google-client-id.apps.googleusercontent.com"
|
||||||
|
clientSecret: "your-google-client-secret"
|
||||||
|
callbackUrl: "https://your-domain.com/auth/callback"
|
||||||
|
scopes: ["openid", "profile", "email"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Google-Specific Features
|
||||||
|
- **Automatic offline access**: Google provider automatically adds `access_type=offline` and `prompt=consent`
|
||||||
|
- **Scope filtering**: Automatically removes `offline_access` scope (not used by Google)
|
||||||
|
- **Refresh token support**: Fully supported
|
||||||
|
- **Domain restrictions**: Can restrict by Google Workspace domains
|
||||||
|
|
||||||
|
### Example Configuration
|
||||||
|
```yaml
|
||||||
|
# Traefik dynamic configuration
|
||||||
|
http:
|
||||||
|
middlewares:
|
||||||
|
google-oidc:
|
||||||
|
plugin:
|
||||||
|
traefik-oidc:
|
||||||
|
providerUrl: "https://accounts.google.com"
|
||||||
|
clientId: "123456789-abcdef.apps.googleusercontent.com"
|
||||||
|
clientSecret: "GOCSPX-your-client-secret"
|
||||||
|
callbackUrl: "https://app.example.com/auth/callback"
|
||||||
|
logoutUrl: "https://app.example.com/auth/logout"
|
||||||
|
scopes: ["openid", "profile", "email"]
|
||||||
|
allowedUserDomains: ["example.com", "company.org"]
|
||||||
|
forceHttps: true
|
||||||
|
enablePkce: true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Google OAuth Console Setup
|
||||||
|
1. Go to [Google Cloud Console](https://console.cloud.google.com/)
|
||||||
|
2. Create or select a project
|
||||||
|
3. Enable Google+ API
|
||||||
|
4. Create OAuth 2.0 credentials
|
||||||
|
5. Add authorized redirect URIs: `https://your-domain.com/auth/callback`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Microsoft Azure AD
|
||||||
|
|
||||||
|
### Provider URL
|
||||||
|
```yaml
|
||||||
|
# For Azure AD (single tenant)
|
||||||
|
providerUrl: "https://login.microsoftonline.com/{tenant-id}/v2.0"
|
||||||
|
|
||||||
|
# For Azure AD (multi-tenant)
|
||||||
|
providerUrl: "https://login.microsoftonline.com/common/v2.0"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Required Configuration
|
||||||
|
```yaml
|
||||||
|
clientId: "your-azure-application-id"
|
||||||
|
clientSecret: "your-azure-client-secret"
|
||||||
|
callbackUrl: "https://your-domain.com/auth/callback"
|
||||||
|
scopes: ["openid", "profile", "email", "offline_access"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Azure-Specific Features
|
||||||
|
- **Response mode**: Automatically adds `response_mode=query`
|
||||||
|
- **Offline access**: Requires `offline_access` scope for refresh tokens
|
||||||
|
- **Access token validation**: Supports both JWT and opaque access tokens
|
||||||
|
- **Tenant isolation**: Can restrict to specific Azure AD tenants
|
||||||
|
|
||||||
|
### Example Configuration
|
||||||
|
```yaml
|
||||||
|
http:
|
||||||
|
middlewares:
|
||||||
|
azure-oidc:
|
||||||
|
plugin:
|
||||||
|
traefik-oidc:
|
||||||
|
providerUrl: "https://login.microsoftonline.com/common/v2.0"
|
||||||
|
clientId: "12345678-1234-1234-1234-123456789abc"
|
||||||
|
clientSecret: "your-azure-client-secret"
|
||||||
|
callbackUrl: "https://app.example.com/auth/callback"
|
||||||
|
logoutUrl: "https://app.example.com/auth/logout"
|
||||||
|
postLogoutRedirectUri: "https://app.example.com"
|
||||||
|
scopes: ["openid", "profile", "email", "offline_access"]
|
||||||
|
allowedRolesAndGroups: ["App.Users", "Admin.Group"]
|
||||||
|
forceHttps: true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Azure App Registration Setup
|
||||||
|
1. Go to [Azure Portal](https://portal.azure.com/)
|
||||||
|
2. Navigate to "Azure Active Directory" > "App registrations"
|
||||||
|
3. Create new registration
|
||||||
|
4. Add redirect URI: `https://your-domain.com/auth/callback`
|
||||||
|
5. Create client secret in "Certificates & secrets"
|
||||||
|
6. Configure API permissions for required scopes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Auth0
|
||||||
|
|
||||||
|
### Provider URL
|
||||||
|
```yaml
|
||||||
|
providerUrl: "https://your-domain.auth0.com"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Required Configuration
|
||||||
|
```yaml
|
||||||
|
clientId: "your-auth0-client-id"
|
||||||
|
clientSecret: "your-auth0-client-secret"
|
||||||
|
callbackUrl: "https://your-domain.com/auth/callback"
|
||||||
|
scopes: ["openid", "profile", "email", "offline_access"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Auth0-Specific Features
|
||||||
|
- **Custom domains**: Supports Auth0 custom domains
|
||||||
|
- **Rules and hooks**: Leverages Auth0's extensibility
|
||||||
|
- **Social connections**: Works with Auth0's social identity providers
|
||||||
|
- **Offline access**: Requires `offline_access` scope
|
||||||
|
|
||||||
|
### Example Configuration
|
||||||
|
```yaml
|
||||||
|
http:
|
||||||
|
middlewares:
|
||||||
|
auth0-oidc:
|
||||||
|
plugin:
|
||||||
|
traefik-oidc:
|
||||||
|
providerUrl: "https://company.auth0.com"
|
||||||
|
clientId: "abcdef123456789"
|
||||||
|
clientSecret: "your-auth0-client-secret"
|
||||||
|
callbackUrl: "https://app.example.com/auth/callback"
|
||||||
|
logoutUrl: "https://app.example.com/auth/logout"
|
||||||
|
postLogoutRedirectUri: "https://app.example.com"
|
||||||
|
scopes: ["openid", "profile", "email", "offline_access"]
|
||||||
|
allowedUsers: ["[email protected]", "[email protected]"]
|
||||||
|
forceHttps: true
|
||||||
|
enablePkce: true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Auth0 Application Setup
|
||||||
|
1. Go to [Auth0 Dashboard](https://manage.auth0.com/)
|
||||||
|
2. Create new application (Regular Web Application)
|
||||||
|
3. Configure allowed callback URLs: `https://your-domain.com/auth/callback`
|
||||||
|
4. Configure allowed logout URLs: `https://your-domain.com/auth/logout`
|
||||||
|
5. Enable OIDC Conformant in Advanced Settings
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## GitHub
|
||||||
|
|
||||||
|
### Provider URL
|
||||||
|
```yaml
|
||||||
|
providerUrl: "https://github.com"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Required Configuration
|
||||||
|
```yaml
|
||||||
|
clientId: "your-github-client-id"
|
||||||
|
clientSecret: "your-github-client-secret"
|
||||||
|
callbackUrl: "https://your-domain.com/auth/callback"
|
||||||
|
scopes: ["read:user", "user:email"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### GitHub-Specific Features
|
||||||
|
- **Organization membership**: Can restrict by GitHub organization
|
||||||
|
- **Team membership**: Can restrict by specific teams
|
||||||
|
- **Limited OIDC**: GitHub has limited OIDC support
|
||||||
|
- **Email verification**: Requires verified email addresses
|
||||||
|
|
||||||
|
### Example Configuration
|
||||||
|
```yaml
|
||||||
|
http:
|
||||||
|
middlewares:
|
||||||
|
github-oidc:
|
||||||
|
plugin:
|
||||||
|
traefik-oidc:
|
||||||
|
providerUrl: "https://github.com"
|
||||||
|
clientId: "Iv1.abcdef123456"
|
||||||
|
clientSecret: "your-github-client-secret"
|
||||||
|
callbackUrl: "https://app.example.com/auth/callback"
|
||||||
|
logoutUrl: "https://app.example.com/auth/logout"
|
||||||
|
scopes: ["read:user", "user:email"]
|
||||||
|
allowedUsers: ["octocat", "github-user"]
|
||||||
|
forceHttps: true
|
||||||
|
```
|
||||||
|
|
||||||
|
### GitHub OAuth App Setup
|
||||||
|
1. Go to GitHub Settings > Developer settings > OAuth Apps
|
||||||
|
2. Create new OAuth App
|
||||||
|
3. Set Authorization callback URL: `https://your-domain.com/auth/callback`
|
||||||
|
4. Note the Client ID and generate Client Secret
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## GitLab
|
||||||
|
|
||||||
|
### Provider URL
|
||||||
|
```yaml
|
||||||
|
# GitLab.com
|
||||||
|
providerUrl: "https://gitlab.com"
|
||||||
|
|
||||||
|
# Self-hosted GitLab
|
||||||
|
providerUrl: "https://gitlab.your-company.com"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Required Configuration
|
||||||
|
```yaml
|
||||||
|
clientId: "your-gitlab-application-id"
|
||||||
|
clientSecret: "your-gitlab-application-secret"
|
||||||
|
callbackUrl: "https://your-domain.com/auth/callback"
|
||||||
|
scopes: ["openid", "profile", "email"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### GitLab-Specific Features
|
||||||
|
- **Self-hosted support**: Works with self-hosted GitLab instances
|
||||||
|
- **Group membership**: Can restrict by GitLab groups
|
||||||
|
- **Project access**: Can validate project permissions
|
||||||
|
- **Offline access**: Supports refresh tokens with `offline_access`
|
||||||
|
|
||||||
|
### Example Configuration
|
||||||
|
```yaml
|
||||||
|
http:
|
||||||
|
middlewares:
|
||||||
|
gitlab-oidc:
|
||||||
|
plugin:
|
||||||
|
traefik-oidc:
|
||||||
|
providerUrl: "https://gitlab.com"
|
||||||
|
clientId: "abcdef123456789"
|
||||||
|
clientSecret: "your-gitlab-application-secret"
|
||||||
|
callbackUrl: "https://app.example.com/auth/callback"
|
||||||
|
logoutUrl: "https://app.example.com/auth/logout"
|
||||||
|
scopes: ["openid", "profile", "email", "offline_access"]
|
||||||
|
allowedRolesAndGroups: ["developers", "maintainers"]
|
||||||
|
forceHttps: true
|
||||||
|
enablePkce: true
|
||||||
|
```
|
||||||
|
|
||||||
|
### GitLab Application Setup
|
||||||
|
1. Go to GitLab Settings > Applications
|
||||||
|
2. Create new application
|
||||||
|
3. Add scopes: `openid`, `profile`, `email`
|
||||||
|
4. Set redirect URI: `https://your-domain.com/auth/callback`
|
||||||
|
5. Save and note the Application ID and Secret
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## AWS Cognito
|
||||||
|
|
||||||
|
### Provider URL
|
||||||
|
```yaml
|
||||||
|
providerUrl: "https://cognito-idp.{region}.amazonaws.com/{user-pool-id}"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Required Configuration
|
||||||
|
```yaml
|
||||||
|
clientId: "your-cognito-app-client-id"
|
||||||
|
clientSecret: "your-cognito-app-client-secret" # If app client has secret
|
||||||
|
callbackUrl: "https://your-domain.com/auth/callback"
|
||||||
|
scopes: ["openid", "profile", "email"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cognito-Specific Features
|
||||||
|
- **User pools**: Integrates with Cognito User Pools
|
||||||
|
- **Custom attributes**: Supports custom user attributes
|
||||||
|
- **Groups**: Can validate Cognito user group membership
|
||||||
|
- **Regional endpoints**: Requires region-specific URLs
|
||||||
|
|
||||||
|
### Example Configuration
|
||||||
|
```yaml
|
||||||
|
http:
|
||||||
|
middlewares:
|
||||||
|
cognito-oidc:
|
||||||
|
plugin:
|
||||||
|
traefik-oidc:
|
||||||
|
providerUrl: "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_ABCDEF123"
|
||||||
|
clientId: "1234567890abcdefghij"
|
||||||
|
clientSecret: "your-cognito-client-secret"
|
||||||
|
callbackUrl: "https://app.example.com/auth/callback"
|
||||||
|
logoutUrl: "https://app.example.com/auth/logout"
|
||||||
|
scopes: ["openid", "profile", "email"]
|
||||||
|
allowedRolesAndGroups: ["admin", "users"]
|
||||||
|
forceHttps: true
|
||||||
|
```
|
||||||
|
|
||||||
|
### AWS Cognito Setup
|
||||||
|
1. Create Cognito User Pool
|
||||||
|
2. Create App Client with OIDC scopes
|
||||||
|
3. Configure App Client settings:
|
||||||
|
- Callback URLs: `https://your-domain.com/auth/callback`
|
||||||
|
- Sign out URLs: `https://your-domain.com/auth/logout`
|
||||||
|
- OAuth flows: Authorization code grant
|
||||||
|
4. Configure hosted UI domain (optional)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Keycloak
|
||||||
|
|
||||||
|
### Provider URL
|
||||||
|
```yaml
|
||||||
|
providerUrl: "https://keycloak.your-company.com/realms/{realm-name}"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Required Configuration
|
||||||
|
```yaml
|
||||||
|
clientId: "your-keycloak-client-id"
|
||||||
|
clientSecret: "your-keycloak-client-secret"
|
||||||
|
callbackUrl: "https://your-domain.com/auth/callback"
|
||||||
|
scopes: ["openid", "profile", "email"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Keycloak-Specific Features
|
||||||
|
- **Realm support**: Multi-realm deployments
|
||||||
|
- **Custom mappers**: Rich claim mapping capabilities
|
||||||
|
- **Role-based access**: Fine-grained role management
|
||||||
|
- **Offline access**: Full refresh token support
|
||||||
|
|
||||||
|
### Example Configuration
|
||||||
|
```yaml
|
||||||
|
http:
|
||||||
|
middlewares:
|
||||||
|
keycloak-oidc:
|
||||||
|
plugin:
|
||||||
|
traefik-oidc:
|
||||||
|
providerUrl: "https://keycloak.company.com/realms/employees"
|
||||||
|
clientId: "traefik-app"
|
||||||
|
clientSecret: "your-keycloak-client-secret"
|
||||||
|
callbackUrl: "https://app.example.com/auth/callback"
|
||||||
|
logoutUrl: "https://app.example.com/auth/logout"
|
||||||
|
postLogoutRedirectUri: "https://app.example.com"
|
||||||
|
scopes: ["openid", "profile", "email", "offline_access"]
|
||||||
|
allowedRolesAndGroups: ["app-users", "administrators"]
|
||||||
|
forceHttps: true
|
||||||
|
enablePkce: true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Keycloak Client Setup
|
||||||
|
1. Access Keycloak Admin Console
|
||||||
|
2. Select appropriate realm
|
||||||
|
3. Create new client:
|
||||||
|
- Client Protocol: openid-connect
|
||||||
|
- Access Type: confidential
|
||||||
|
- Valid Redirect URIs: `https://your-domain.com/auth/callback`
|
||||||
|
4. Configure client scopes and mappers
|
||||||
|
5. Generate client secret in Credentials tab
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Okta
|
||||||
|
|
||||||
|
### Provider URL
|
||||||
|
```yaml
|
||||||
|
providerUrl: "https://your-domain.okta.com"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Required Configuration
|
||||||
|
```yaml
|
||||||
|
clientId: "your-okta-client-id"
|
||||||
|
clientSecret: "your-okta-client-secret"
|
||||||
|
callbackUrl: "https://your-domain.com/auth/callback"
|
||||||
|
scopes: ["openid", "profile", "email", "offline_access"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Okta-Specific Features
|
||||||
|
- **Custom authorization servers**: Supports custom auth servers
|
||||||
|
- **Group claims**: Rich group membership information
|
||||||
|
- **Universal Directory**: Integrates with Okta's user store
|
||||||
|
- **Offline access**: Requires `offline_access` scope
|
||||||
|
|
||||||
|
### Example Configuration
|
||||||
|
```yaml
|
||||||
|
http:
|
||||||
|
middlewares:
|
||||||
|
okta-oidc:
|
||||||
|
plugin:
|
||||||
|
traefik-oidc:
|
||||||
|
providerUrl: "https://company.okta.com"
|
||||||
|
clientId: "0oa123456789abcdef"
|
||||||
|
clientSecret: "your-okta-client-secret"
|
||||||
|
callbackUrl: "https://app.example.com/auth/callback"
|
||||||
|
logoutUrl: "https://app.example.com/auth/logout"
|
||||||
|
postLogoutRedirectUri: "https://app.example.com"
|
||||||
|
scopes: ["openid", "profile", "email", "offline_access"]
|
||||||
|
allowedRolesAndGroups: ["Everyone", "Administrators"]
|
||||||
|
forceHttps: true
|
||||||
|
enablePkce: true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Okta Application Setup
|
||||||
|
1. Access Okta Admin Console
|
||||||
|
2. Go to Applications > Create App Integration
|
||||||
|
3. Select OIDC - OpenID Connect
|
||||||
|
4. Choose Web Application
|
||||||
|
5. Configure:
|
||||||
|
- Sign-in redirect URIs: `https://your-domain.com/auth/callback`
|
||||||
|
- Sign-out redirect URIs: `https://your-domain.com/auth/logout`
|
||||||
|
- Grant types: Authorization Code, Refresh Token
|
||||||
|
6. Assign users or groups
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Generic OIDC
|
||||||
|
|
||||||
|
### Provider URL
|
||||||
|
```yaml
|
||||||
|
providerUrl: "https://your-oidc-provider.com"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Required Configuration
|
||||||
|
```yaml
|
||||||
|
clientId: "your-client-id"
|
||||||
|
clientSecret: "your-client-secret"
|
||||||
|
callbackUrl: "https://your-domain.com/auth/callback"
|
||||||
|
scopes: ["openid", "profile", "email"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Generic Features
|
||||||
|
- **Standards compliance**: Works with any OIDC-compliant provider
|
||||||
|
- **Auto-discovery**: Uses `.well-known/openid-configuration` endpoint
|
||||||
|
- **Flexible scopes**: Supports custom scope requirements
|
||||||
|
- **Custom claims**: Works with provider-specific claims
|
||||||
|
|
||||||
|
### Example Configuration
|
||||||
|
```yaml
|
||||||
|
http:
|
||||||
|
middlewares:
|
||||||
|
generic-oidc:
|
||||||
|
plugin:
|
||||||
|
traefik-oidc:
|
||||||
|
providerUrl: "https://oidc.your-provider.com"
|
||||||
|
clientId: "your-client-id"
|
||||||
|
clientSecret: "your-client-secret"
|
||||||
|
callbackUrl: "https://app.example.com/auth/callback"
|
||||||
|
logoutUrl: "https://app.example.com/auth/logout"
|
||||||
|
scopes: ["openid", "profile", "email"]
|
||||||
|
forceHttps: true
|
||||||
|
enablePkce: true
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Configuration Options
|
||||||
|
|
||||||
|
### Security Settings
|
||||||
|
```yaml
|
||||||
|
# Force HTTPS (recommended for production)
|
||||||
|
forceHttps: true
|
||||||
|
|
||||||
|
# Enable PKCE (recommended for security)
|
||||||
|
enablePkce: true
|
||||||
|
|
||||||
|
# Session encryption key (32+ characters)
|
||||||
|
sessionEncryptionKey: "your-very-long-encryption-key-here"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Access Control
|
||||||
|
```yaml
|
||||||
|
# Restrict by email addresses
|
||||||
|
allowedUsers: ["[email protected]", "[email protected]"]
|
||||||
|
|
||||||
|
# Restrict by email domains
|
||||||
|
allowedUserDomains: ["company.com", "partner.org"]
|
||||||
|
|
||||||
|
# Restrict by roles/groups (provider-specific)
|
||||||
|
allowedRolesAndGroups: ["admin", "users", "developers"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### URLs and Endpoints
|
||||||
|
```yaml
|
||||||
|
# OAuth callback URL (must match provider config)
|
||||||
|
callbackUrl: "https://your-domain.com/auth/callback"
|
||||||
|
|
||||||
|
# Logout endpoint
|
||||||
|
logoutUrl: "https://your-domain.com/auth/logout"
|
||||||
|
|
||||||
|
# Post-logout redirect (optional)
|
||||||
|
postLogoutRedirectUri: "https://your-domain.com"
|
||||||
|
|
||||||
|
# URLs to exclude from authentication
|
||||||
|
excludedUrls: ["/health", "/metrics", "/public"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Advanced Settings
|
||||||
|
```yaml
|
||||||
|
# Override default scopes
|
||||||
|
overrideScopes: true
|
||||||
|
scopes: ["openid", "custom_scope"]
|
||||||
|
|
||||||
|
# Rate limiting (requests per second)
|
||||||
|
rateLimit: 10
|
||||||
|
|
||||||
|
# Token refresh grace period (seconds)
|
||||||
|
refreshGracePeriodSeconds: 60
|
||||||
|
|
||||||
|
# Cookie domain (for subdomain sharing)
|
||||||
|
cookieDomain: ".example.com"
|
||||||
|
|
||||||
|
# Custom headers to inject
|
||||||
|
headers:
|
||||||
|
- name: "X-User-Email"
|
||||||
|
value: "{{.Claims.email}}"
|
||||||
|
- name: "X-User-Name"
|
||||||
|
value: "{{.Claims.name}}"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
1. **Invalid redirect URI**
|
||||||
|
- Ensure callback URL exactly matches provider configuration
|
||||||
|
- Check for HTTP vs HTTPS mismatches
|
||||||
|
|
||||||
|
2. **Scope errors**
|
||||||
|
- Verify required scopes are configured in provider
|
||||||
|
- Some providers require specific scopes for refresh tokens
|
||||||
|
|
||||||
|
3. **Token validation failures**
|
||||||
|
- Check provider URL format and accessibility
|
||||||
|
- Verify `.well-known/openid-configuration` endpoint is reachable
|
||||||
|
|
||||||
|
4. **Session issues**
|
||||||
|
- Ensure session encryption key is properly configured
|
||||||
|
- Check cookie domain settings for subdomain scenarios
|
||||||
|
|
||||||
|
### Debug Mode
|
||||||
|
Enable debug logging to troubleshoot configuration issues:
|
||||||
|
```yaml
|
||||||
|
logLevel: "debug"
|
||||||
|
```
|
||||||
|
|
||||||
|
This will provide detailed logs of the authentication flow and help identify configuration problems.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security Headers Configuration
|
||||||
|
|
||||||
|
The plugin includes comprehensive security headers support to protect your applications against common web vulnerabilities.
|
||||||
|
|
||||||
|
### Default Security Headers
|
||||||
|
|
||||||
|
By default, the plugin applies these security headers:
|
||||||
|
|
||||||
|
- `X-Frame-Options: DENY` - Prevents clickjacking
|
||||||
|
- `X-Content-Type-Options: nosniff` - Prevents MIME sniffing
|
||||||
|
- `X-XSS-Protection: 1; mode=block` - Enables XSS protection
|
||||||
|
- `Referrer-Policy: strict-origin-when-cross-origin` - Controls referrer information
|
||||||
|
- `Strict-Transport-Security` - Forces HTTPS (when HTTPS is detected)
|
||||||
|
|
||||||
|
### Security Profiles
|
||||||
|
|
||||||
|
Choose from predefined security profiles or create custom configurations:
|
||||||
|
|
||||||
|
#### Default Profile (Recommended)
|
||||||
|
```yaml
|
||||||
|
securityHeaders:
|
||||||
|
enabled: true
|
||||||
|
profile: "default"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Strict Profile (Maximum Security)
|
||||||
|
```yaml
|
||||||
|
securityHeaders:
|
||||||
|
enabled: true
|
||||||
|
profile: "strict"
|
||||||
|
# Additional strict CSP and cross-origin policies
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Development Profile (Local Development)
|
||||||
|
```yaml
|
||||||
|
securityHeaders:
|
||||||
|
enabled: true
|
||||||
|
profile: "development"
|
||||||
|
# Relaxed policies for local development
|
||||||
|
```
|
||||||
|
|
||||||
|
#### API Profile (API Endpoints)
|
||||||
|
```yaml
|
||||||
|
securityHeaders:
|
||||||
|
enabled: true
|
||||||
|
profile: "api"
|
||||||
|
corsEnabled: true
|
||||||
|
corsAllowedOrigins: ["https://your-frontend.com"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Security Configuration
|
||||||
|
|
||||||
|
For complete control, use the custom profile:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
securityHeaders:
|
||||||
|
enabled: true
|
||||||
|
profile: "custom"
|
||||||
|
|
||||||
|
# Content Security Policy
|
||||||
|
contentSecurityPolicy: "default-src 'self'; script-src 'self' 'unsafe-inline'"
|
||||||
|
|
||||||
|
# HSTS Configuration
|
||||||
|
strictTransportSecurity: true
|
||||||
|
strictTransportSecurityMaxAge: 31536000 # 1 year
|
||||||
|
strictTransportSecuritySubdomains: true
|
||||||
|
strictTransportSecurityPreload: true
|
||||||
|
|
||||||
|
# Frame and content protection
|
||||||
|
frameOptions: "DENY" # or "SAMEORIGIN", "ALLOW-FROM uri"
|
||||||
|
contentTypeOptions: "nosniff"
|
||||||
|
xssProtection: "1; mode=block"
|
||||||
|
referrerPolicy: "strict-origin-when-cross-origin"
|
||||||
|
|
||||||
|
# Permissions policy (feature policy)
|
||||||
|
permissionsPolicy: "geolocation=(), microphone=(), camera=()"
|
||||||
|
|
||||||
|
# Cross-origin policies
|
||||||
|
crossOriginEmbedderPolicy: "require-corp"
|
||||||
|
crossOriginOpenerPolicy: "same-origin"
|
||||||
|
crossOriginResourcePolicy: "same-origin"
|
||||||
|
|
||||||
|
# CORS configuration
|
||||||
|
corsEnabled: true
|
||||||
|
corsAllowedOrigins:
|
||||||
|
- "https://app.example.com"
|
||||||
|
- "https://*.api.example.com"
|
||||||
|
corsAllowedMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"]
|
||||||
|
corsAllowedHeaders: ["Authorization", "Content-Type", "X-Requested-With"]
|
||||||
|
corsAllowCredentials: true
|
||||||
|
corsMaxAge: 86400 # 24 hours
|
||||||
|
|
||||||
|
# Custom headers
|
||||||
|
customHeaders:
|
||||||
|
X-Custom-Header: "custom-value"
|
||||||
|
X-API-Version: "v1"
|
||||||
|
|
||||||
|
# Server identification
|
||||||
|
disableServerHeader: true
|
||||||
|
disablePoweredByHeader: true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Complete Example with Security Headers
|
||||||
|
|
||||||
|
Here's a complete configuration example for Google OIDC with custom security headers:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Traefik dynamic configuration
|
||||||
|
http:
|
||||||
|
middlewares:
|
||||||
|
secure-google-oidc:
|
||||||
|
plugin:
|
||||||
|
traefik-oidc:
|
||||||
|
# OIDC Configuration
|
||||||
|
providerUrl: "https://accounts.google.com"
|
||||||
|
clientId: "123456789-abcdef.apps.googleusercontent.com"
|
||||||
|
clientSecret: "GOCSPX-your-client-secret"
|
||||||
|
callbackUrl: "https://your-domain.com/auth/callback"
|
||||||
|
sessionEncryptionKey: "your-32-character-encryption-key-here"
|
||||||
|
|
||||||
|
# Domain restrictions
|
||||||
|
allowedUserDomains: ["your-company.com"]
|
||||||
|
|
||||||
|
# Security Headers
|
||||||
|
securityHeaders:
|
||||||
|
enabled: true
|
||||||
|
profile: "strict"
|
||||||
|
corsEnabled: true
|
||||||
|
corsAllowedOrigins:
|
||||||
|
- "https://your-frontend.com"
|
||||||
|
- "https://*.your-domain.com"
|
||||||
|
corsAllowCredentials: true
|
||||||
|
customHeaders:
|
||||||
|
X-Company: "YourCompany"
|
||||||
|
X-Environment: "production"
|
||||||
|
|
||||||
|
routers:
|
||||||
|
secure-app:
|
||||||
|
rule: "Host(`your-domain.com`)"
|
||||||
|
middlewares:
|
||||||
|
- secure-google-oidc
|
||||||
|
service: your-app-service
|
||||||
|
tls:
|
||||||
|
certResolver: letsencrypt
|
||||||
|
```
|
||||||
|
|
||||||
|
### CORS Configuration Details
|
||||||
|
|
||||||
|
For applications with frontend-backend separation, configure CORS properly:
|
||||||
|
|
||||||
|
#### Simple CORS (Single Origin)
|
||||||
|
```yaml
|
||||||
|
securityHeaders:
|
||||||
|
corsEnabled: true
|
||||||
|
corsAllowedOrigins: ["https://app.example.com"]
|
||||||
|
corsAllowCredentials: true
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Wildcard Subdomains
|
||||||
|
```yaml
|
||||||
|
securityHeaders:
|
||||||
|
corsEnabled: true
|
||||||
|
corsAllowedOrigins: ["https://*.example.com"]
|
||||||
|
corsAllowCredentials: true
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Development with Multiple Ports
|
||||||
|
```yaml
|
||||||
|
securityHeaders:
|
||||||
|
profile: "development"
|
||||||
|
corsEnabled: true
|
||||||
|
corsAllowedOrigins:
|
||||||
|
- "http://localhost:*"
|
||||||
|
- "http://127.0.0.1:*"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Security Best Practices
|
||||||
|
|
||||||
|
1. **Always use HTTPS in production**
|
||||||
|
- Set `forceHttps: true`
|
||||||
|
- Configure proper TLS certificates
|
||||||
|
|
||||||
|
2. **Implement proper CSP**
|
||||||
|
- Start with strict policy
|
||||||
|
- Add exceptions only when necessary
|
||||||
|
- Test thoroughly
|
||||||
|
|
||||||
|
3. **Configure CORS restrictively**
|
||||||
|
- Only allow necessary origins
|
||||||
|
- Use specific domains instead of wildcards when possible
|
||||||
|
|
||||||
|
4. **Enable HSTS**
|
||||||
|
- Use long max-age values (1 year minimum)
|
||||||
|
- Include subdomains when appropriate
|
||||||
|
|
||||||
|
5. **Monitor security headers**
|
||||||
|
- Use browser developer tools to verify headers
|
||||||
|
- Test with security scanning tools
|
||||||
|
- Regularly review and update policies
|
||||||
|
|
||||||
|
### Testing Security Headers
|
||||||
|
|
||||||
|
Use browser developer tools or online tools to verify your security headers:
|
||||||
|
|
||||||
|
1. **Browser DevTools**: Check Network tab → Response Headers
|
||||||
|
2. **Online scanners**: Use securityheaders.com or observatory.mozilla.org
|
||||||
|
3. **Command line**: Use `curl -I https://your-domain.com`
|
||||||
|
|
||||||
|
Example verification:
|
||||||
|
```bash
|
||||||
|
curl -I https://your-domain.com
|
||||||
|
# Should show security headers in response
|
||||||
|
```
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
// Package errors provides unified error handling for OIDC operations
|
||||||
|
package errors
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrorCode represents specific error types
|
||||||
|
type ErrorCode string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Authentication errors
|
||||||
|
ErrCodeAuthenticationFailed ErrorCode = "AUTH_FAILED"
|
||||||
|
ErrCodeTokenExpired ErrorCode = "TOKEN_EXPIRED"
|
||||||
|
ErrCodeTokenInvalid ErrorCode = "TOKEN_INVALID"
|
||||||
|
ErrCodeSessionExpired ErrorCode = "SESSION_EXPIRED"
|
||||||
|
ErrCodeCSRFMismatch ErrorCode = "CSRF_MISMATCH"
|
||||||
|
ErrCodeNonceMismatch ErrorCode = "NONCE_MISMATCH"
|
||||||
|
|
||||||
|
// Configuration errors
|
||||||
|
ErrCodeConfigInvalid ErrorCode = "CONFIG_INVALID"
|
||||||
|
ErrCodeProviderUnreachable ErrorCode = "PROVIDER_UNREACHABLE"
|
||||||
|
ErrCodeMetadataFailed ErrorCode = "METADATA_FAILED"
|
||||||
|
|
||||||
|
// Network errors
|
||||||
|
ErrCodeNetworkTimeout ErrorCode = "NETWORK_TIMEOUT"
|
||||||
|
ErrCodeRateLimited ErrorCode = "RATE_LIMITED"
|
||||||
|
ErrCodeServiceUnavailable ErrorCode = "SERVICE_UNAVAILABLE"
|
||||||
|
|
||||||
|
// Validation errors
|
||||||
|
ErrCodeValidationFailed ErrorCode = "VALIDATION_FAILED"
|
||||||
|
ErrCodeDomainNotAllowed ErrorCode = "DOMAIN_NOT_ALLOWED"
|
||||||
|
ErrCodeUserNotAllowed ErrorCode = "USER_NOT_ALLOWED"
|
||||||
|
ErrCodeRoleNotAllowed ErrorCode = "ROLE_NOT_ALLOWED"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OIDCError represents a structured error with context
|
||||||
|
type OIDCError struct {
|
||||||
|
Code ErrorCode `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Details string `json:"details,omitempty"`
|
||||||
|
HTTPStatus int `json:"http_status"`
|
||||||
|
Internal error `json:"-"` // Internal error, not exposed
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error implements the error interface
|
||||||
|
func (e *OIDCError) Error() string {
|
||||||
|
if e.Details != "" {
|
||||||
|
return fmt.Sprintf("%s: %s (%s)", e.Code, e.Message, e.Details)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s: %s", e.Code, e.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unwrap returns the internal error for error wrapping
|
||||||
|
func (e *OIDCError) Unwrap() error {
|
||||||
|
return e.Internal
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsRetryable indicates if the error is temporary and can be retried
|
||||||
|
func (e *OIDCError) IsRetryable() bool {
|
||||||
|
return e.Code == ErrCodeNetworkTimeout ||
|
||||||
|
e.Code == ErrCodeServiceUnavailable ||
|
||||||
|
e.Code == ErrCodeProviderUnreachable
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAuthenticationError indicates if this is an authentication-related error
|
||||||
|
func (e *OIDCError) IsAuthenticationError() bool {
|
||||||
|
return e.Code == ErrCodeAuthenticationFailed ||
|
||||||
|
e.Code == ErrCodeTokenExpired ||
|
||||||
|
e.Code == ErrCodeTokenInvalid ||
|
||||||
|
e.Code == ErrCodeSessionExpired ||
|
||||||
|
e.Code == ErrCodeCSRFMismatch ||
|
||||||
|
e.Code == ErrCodeNonceMismatch
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAuthorizationError indicates if this is an authorization-related error
|
||||||
|
func (e *OIDCError) IsAuthorizationError() bool {
|
||||||
|
return e.Code == ErrCodeDomainNotAllowed ||
|
||||||
|
e.Code == ErrCodeUserNotAllowed ||
|
||||||
|
e.Code == ErrCodeRoleNotAllowed
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToJSON converts the error to a JSON response
|
||||||
|
func (e *OIDCError) ToJSON() map[string]any {
|
||||||
|
result := map[string]any{
|
||||||
|
"error": map[string]any{
|
||||||
|
"code": string(e.Code),
|
||||||
|
"message": e.Message,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Details != "" {
|
||||||
|
result["error"].(map[string]any)["details"] = e.Details
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error constructors for common scenarios
|
||||||
|
|
||||||
|
// NewAuthenticationError creates an authentication-related error
|
||||||
|
func NewAuthenticationError(code ErrorCode, message string, internal error) *OIDCError {
|
||||||
|
status := http.StatusUnauthorized
|
||||||
|
if code == ErrCodeSessionExpired {
|
||||||
|
status = http.StatusForbidden
|
||||||
|
}
|
||||||
|
|
||||||
|
return &OIDCError{
|
||||||
|
Code: code,
|
||||||
|
Message: message,
|
||||||
|
HTTPStatus: status,
|
||||||
|
Internal: internal,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAuthorizationError creates an authorization-related error
|
||||||
|
func NewAuthorizationError(code ErrorCode, message string, details string) *OIDCError {
|
||||||
|
return &OIDCError{
|
||||||
|
Code: code,
|
||||||
|
Message: message,
|
||||||
|
Details: details,
|
||||||
|
HTTPStatus: http.StatusForbidden,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewConfigurationError creates a configuration-related error
|
||||||
|
func NewConfigurationError(code ErrorCode, message string, internal error) *OIDCError {
|
||||||
|
return &OIDCError{
|
||||||
|
Code: code,
|
||||||
|
Message: message,
|
||||||
|
HTTPStatus: http.StatusInternalServerError,
|
||||||
|
Internal: internal,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewNetworkError creates a network-related error
|
||||||
|
func NewNetworkError(code ErrorCode, message string, internal error) *OIDCError {
|
||||||
|
status := http.StatusServiceUnavailable
|
||||||
|
if code == ErrCodeRateLimited {
|
||||||
|
status = http.StatusTooManyRequests
|
||||||
|
}
|
||||||
|
|
||||||
|
return &OIDCError{
|
||||||
|
Code: code,
|
||||||
|
Message: message,
|
||||||
|
HTTPStatus: status,
|
||||||
|
Internal: internal,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewValidationError creates a validation-related error
|
||||||
|
func NewValidationError(code ErrorCode, message string, details string) *OIDCError {
|
||||||
|
return &OIDCError{
|
||||||
|
Code: code,
|
||||||
|
Message: message,
|
||||||
|
Details: details,
|
||||||
|
HTTPStatus: http.StatusBadRequest,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convenience functions for common error patterns
|
||||||
|
|
||||||
|
// WrapAuthenticationError wraps an existing error as an authentication error
|
||||||
|
func WrapAuthenticationError(err error, message string) *OIDCError {
|
||||||
|
return NewAuthenticationError(ErrCodeAuthenticationFailed, message, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WrapTokenError wraps a token-related error
|
||||||
|
func WrapTokenError(err error, tokenType string) *OIDCError {
|
||||||
|
message := fmt.Sprintf("Token validation failed: %s", tokenType)
|
||||||
|
return NewAuthenticationError(ErrCodeTokenInvalid, message, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WrapProviderError wraps a provider communication error
|
||||||
|
func WrapProviderError(err error, providerURL string) *OIDCError {
|
||||||
|
message := fmt.Sprintf("Provider communication failed: %s", providerURL)
|
||||||
|
return NewNetworkError(ErrCodeProviderUnreachable, message, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsOIDCError checks if an error is an OIDCError
|
||||||
|
func IsOIDCError(err error) (*OIDCError, bool) {
|
||||||
|
oidcErr, ok := err.(*OIDCError)
|
||||||
|
return oidcErr, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHTTPStatus extracts HTTP status from error, defaulting to 500
|
||||||
|
func GetHTTPStatus(err error) int {
|
||||||
|
if oidcErr, ok := IsOIDCError(err); ok {
|
||||||
|
return oidcErr.HTTPStatus
|
||||||
|
}
|
||||||
|
return http.StatusInternalServerError
|
||||||
|
}
|
||||||
|
|
||||||
|
// FormatUserMessage creates a user-friendly error message
|
||||||
|
func FormatUserMessage(err error) string {
|
||||||
|
if oidcErr, ok := IsOIDCError(err); ok {
|
||||||
|
switch oidcErr.Code {
|
||||||
|
case ErrCodeDomainNotAllowed:
|
||||||
|
return "Your email domain is not authorized for this application"
|
||||||
|
case ErrCodeUserNotAllowed:
|
||||||
|
return "Your account is not authorized for this application"
|
||||||
|
case ErrCodeRoleNotAllowed:
|
||||||
|
return "You do not have the required permissions for this application"
|
||||||
|
case ErrCodeSessionExpired:
|
||||||
|
return "Your session has expired. Please log in again"
|
||||||
|
case ErrCodeTokenExpired:
|
||||||
|
return "Your authentication has expired. Please log in again"
|
||||||
|
case ErrCodeProviderUnreachable:
|
||||||
|
return "Authentication service is temporarily unavailable. Please try again later"
|
||||||
|
case ErrCodeRateLimited:
|
||||||
|
return "Too many requests. Please wait a moment and try again"
|
||||||
|
default:
|
||||||
|
return "Authentication failed. Please try again"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "An unexpected error occurred. Please try again"
|
||||||
|
}
|
||||||
@@ -0,0 +1,224 @@
|
|||||||
|
// Package handlers provides authentication flow management
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AuthFlowHandler manages the complete OIDC authentication flow
|
||||||
|
type AuthFlowHandler struct {
|
||||||
|
sessionHandler *SessionHandler
|
||||||
|
tokenHandler TokenHandler
|
||||||
|
logger Logger
|
||||||
|
excludedURLs map[string]struct{}
|
||||||
|
initComplete chan struct{}
|
||||||
|
issuerURL string
|
||||||
|
}
|
||||||
|
|
||||||
|
// TokenHandler interface for token operations
|
||||||
|
type TokenHandler interface {
|
||||||
|
VerifyToken(token string) error
|
||||||
|
RefreshToken(refreshToken string) (*TokenResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TokenResponse represents token exchange response
|
||||||
|
type TokenResponse struct {
|
||||||
|
IDToken string `json:"id_token"`
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
RefreshToken string `json:"refresh_token"`
|
||||||
|
ExpiresIn int `json:"expires_in"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthFlowResult represents the result of authentication flow processing
|
||||||
|
type AuthFlowResult struct {
|
||||||
|
Authenticated bool
|
||||||
|
RequiresAuth bool
|
||||||
|
RequiresRefresh bool
|
||||||
|
Error error
|
||||||
|
RedirectURL string
|
||||||
|
StatusCode int
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAuthFlowHandler creates a new authentication flow handler
|
||||||
|
func NewAuthFlowHandler(sessionHandler *SessionHandler, tokenHandler TokenHandler, logger Logger, excludedURLs map[string]struct{}, initComplete chan struct{}, issuerURL string) *AuthFlowHandler {
|
||||||
|
return &AuthFlowHandler{
|
||||||
|
sessionHandler: sessionHandler,
|
||||||
|
tokenHandler: tokenHandler,
|
||||||
|
logger: logger,
|
||||||
|
excludedURLs: excludedURLs,
|
||||||
|
initComplete: initComplete,
|
||||||
|
issuerURL: issuerURL,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessRequest handles the main authentication flow
|
||||||
|
func (h *AuthFlowHandler) ProcessRequest(rw http.ResponseWriter, req *http.Request) AuthFlowResult {
|
||||||
|
// Check if URL should be excluded
|
||||||
|
if h.shouldExcludeURL(req.URL.Path) {
|
||||||
|
h.logger.Debugf("Request path %s excluded by configuration, bypassing OIDC", req.URL.Path)
|
||||||
|
return AuthFlowResult{Authenticated: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for streaming requests
|
||||||
|
if h.isStreamingRequest(req) {
|
||||||
|
h.logger.Debugf("Streaming request detected, bypassing OIDC")
|
||||||
|
return AuthFlowResult{Authenticated: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for initialization
|
||||||
|
if !h.waitForInitialization(req) {
|
||||||
|
return AuthFlowResult{
|
||||||
|
Error: ErrInitializationTimeout,
|
||||||
|
StatusCode: http.StatusServiceUnavailable,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get and validate session
|
||||||
|
session, err := h.sessionHandler.sessionManager.GetSession(req)
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Errorf("Error getting session: %v", err)
|
||||||
|
return AuthFlowResult{
|
||||||
|
RequiresAuth: true,
|
||||||
|
Error: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defer session.ReturnToPoolSafely()
|
||||||
|
|
||||||
|
// Clean up old cookies
|
||||||
|
h.sessionHandler.sessionManager.CleanupOldCookies(rw, req)
|
||||||
|
|
||||||
|
// Validate session
|
||||||
|
validationResult := h.sessionHandler.ValidateSession(session)
|
||||||
|
if !validationResult.Valid {
|
||||||
|
if validationResult.NeedsAuth {
|
||||||
|
return AuthFlowResult{RequiresAuth: true}
|
||||||
|
}
|
||||||
|
return AuthFlowResult{
|
||||||
|
Error: ErrSessionInvalid,
|
||||||
|
StatusCode: http.StatusUnauthorized,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check token validity and refresh if needed
|
||||||
|
return h.validateAndRefreshTokens(session, req, rw)
|
||||||
|
}
|
||||||
|
|
||||||
|
// shouldExcludeURL checks if a URL should bypass authentication
|
||||||
|
func (h *AuthFlowHandler) shouldExcludeURL(path string) bool {
|
||||||
|
for excludedURL := range h.excludedURLs {
|
||||||
|
if len(path) >= len(excludedURL) && path[:len(excludedURL)] == excludedURL {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// isStreamingRequest checks if request is a streaming request that should bypass auth
|
||||||
|
func (h *AuthFlowHandler) isStreamingRequest(req *http.Request) bool {
|
||||||
|
acceptHeader := req.Header.Get("Accept")
|
||||||
|
return acceptHeader == "text/event-stream"
|
||||||
|
}
|
||||||
|
|
||||||
|
// waitForInitialization waits for OIDC provider initialization
|
||||||
|
func (h *AuthFlowHandler) waitForInitialization(req *http.Request) bool {
|
||||||
|
select {
|
||||||
|
case <-h.initComplete:
|
||||||
|
if h.issuerURL == "" {
|
||||||
|
h.logger.Error("OIDC provider metadata initialization failed")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
case <-req.Context().Done():
|
||||||
|
h.logger.Debug("Request cancelled while waiting for OIDC initialization")
|
||||||
|
return false
|
||||||
|
case <-time.After(30 * time.Second):
|
||||||
|
h.logger.Error("Timeout waiting for OIDC initialization")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateAndRefreshTokens handles token validation and refresh logic
|
||||||
|
func (h *AuthFlowHandler) validateAndRefreshTokens(session Session, req *http.Request, rw http.ResponseWriter) AuthFlowResult {
|
||||||
|
// Check access token if present
|
||||||
|
if accessToken := session.GetAccessToken(); accessToken != "" {
|
||||||
|
if err := h.tokenHandler.VerifyToken(accessToken); err != nil {
|
||||||
|
h.logger.Errorf("Access token validation failed: %v", err)
|
||||||
|
|
||||||
|
// Try refresh if refresh token is available
|
||||||
|
if refreshToken := session.GetRefreshToken(); refreshToken != "" {
|
||||||
|
return h.attemptTokenRefresh(session, req, rw)
|
||||||
|
}
|
||||||
|
|
||||||
|
return AuthFlowResult{RequiresAuth: true}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check ID token
|
||||||
|
if idToken := session.GetIDToken(); idToken != "" {
|
||||||
|
if err := h.tokenHandler.VerifyToken(idToken); err != nil {
|
||||||
|
h.logger.Errorf("ID token validation failed: %v", err)
|
||||||
|
|
||||||
|
// Try refresh if refresh token is available
|
||||||
|
if refreshToken := session.GetRefreshToken(); refreshToken != "" {
|
||||||
|
return h.attemptTokenRefresh(session, req, rw)
|
||||||
|
}
|
||||||
|
|
||||||
|
return AuthFlowResult{RequiresAuth: true}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return AuthFlowResult{Authenticated: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
// attemptTokenRefresh tries to refresh tokens
|
||||||
|
func (h *AuthFlowHandler) attemptTokenRefresh(session Session, req *http.Request, rw http.ResponseWriter) AuthFlowResult {
|
||||||
|
refreshToken := session.GetRefreshToken()
|
||||||
|
if refreshToken == "" {
|
||||||
|
return AuthFlowResult{RequiresAuth: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this is an AJAX request
|
||||||
|
if h.sessionHandler.IsAjaxRequest(req) {
|
||||||
|
return AuthFlowResult{
|
||||||
|
Error: ErrSessionExpiredAjax,
|
||||||
|
StatusCode: http.StatusUnauthorized,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := h.tokenHandler.RefreshToken(refreshToken)
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Errorf("Token refresh failed: %v", err)
|
||||||
|
return AuthFlowResult{RequiresAuth: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update session with new tokens would be handled here
|
||||||
|
// Implementation depends on the actual session interface
|
||||||
|
|
||||||
|
if err := session.Save(req, rw); err != nil {
|
||||||
|
h.logger.Errorf("Failed to save refreshed session: %v", err)
|
||||||
|
return AuthFlowResult{
|
||||||
|
Error: err,
|
||||||
|
StatusCode: http.StatusInternalServerError,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return AuthFlowResult{Authenticated: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Common errors
|
||||||
|
var (
|
||||||
|
ErrInitializationTimeout = &AuthFlowError{Code: "INIT_TIMEOUT", Message: "OIDC initialization timeout"}
|
||||||
|
ErrSessionInvalid = &AuthFlowError{Code: "SESSION_INVALID", Message: "Invalid session"}
|
||||||
|
ErrSessionExpiredAjax = &AuthFlowError{Code: "SESSION_EXPIRED_AJAX", Message: "Session expired for AJAX request"}
|
||||||
|
)
|
||||||
|
|
||||||
|
// AuthFlowError represents authentication flow errors
|
||||||
|
type AuthFlowError struct {
|
||||||
|
Code string
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *AuthFlowError) Error() string {
|
||||||
|
return e.Message
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
// Package optimization provides memory and performance optimizations
|
||||||
|
package optimization
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StringBuilderPool provides a pool of reusable string builders
|
||||||
|
type StringBuilderPool struct {
|
||||||
|
pool sync.Pool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStringBuilderPool creates a new string builder pool
|
||||||
|
func NewStringBuilderPool() *StringBuilderPool {
|
||||||
|
return &StringBuilderPool{
|
||||||
|
pool: sync.Pool{
|
||||||
|
New: func() any {
|
||||||
|
buf := make([]byte, 0, 256) // Pre-allocate 256 bytes
|
||||||
|
return &buf
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get retrieves a string builder from the pool
|
||||||
|
func (p *StringBuilderPool) Get() []byte {
|
||||||
|
bufPtr := p.pool.Get().(*[]byte)
|
||||||
|
return *bufPtr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put returns a string builder to the pool
|
||||||
|
func (p *StringBuilderPool) Put(buf []byte) {
|
||||||
|
if cap(buf) < 4096 { // Don't pool overly large buffers
|
||||||
|
buf = buf[:0] // Reset length but keep capacity
|
||||||
|
p.pool.Put(&buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ByteSlicePool provides a pool of reusable byte slices
|
||||||
|
type ByteSlicePool struct {
|
||||||
|
pool sync.Pool
|
||||||
|
size int
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewByteSlicePool creates a new byte slice pool with specified size
|
||||||
|
func NewByteSlicePool(size int) *ByteSlicePool {
|
||||||
|
return &ByteSlicePool{
|
||||||
|
size: size,
|
||||||
|
pool: sync.Pool{
|
||||||
|
New: func() any {
|
||||||
|
buf := make([]byte, size)
|
||||||
|
return &buf
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get retrieves a byte slice from the pool
|
||||||
|
func (p *ByteSlicePool) Get() []byte {
|
||||||
|
bufPtr := p.pool.Get().(*[]byte)
|
||||||
|
return *bufPtr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put returns a byte slice to the pool
|
||||||
|
func (p *ByteSlicePool) Put(buf []byte) {
|
||||||
|
if len(buf) == p.size {
|
||||||
|
p.pool.Put(&buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global pools for common use cases
|
||||||
|
var (
|
||||||
|
globalStringBuilderPool = NewStringBuilderPool()
|
||||||
|
globalByteSlicePool = NewByteSlicePool(2048)
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetStringBuilder gets a string builder from the global pool
|
||||||
|
func GetStringBuilder() []byte {
|
||||||
|
return globalStringBuilderPool.Get()
|
||||||
|
}
|
||||||
|
|
||||||
|
// PutStringBuilder returns a string builder to the global pool
|
||||||
|
func PutStringBuilder(buf []byte) {
|
||||||
|
globalStringBuilderPool.Put(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByteSlice gets a byte slice from the global pool
|
||||||
|
func GetByteSlice() []byte {
|
||||||
|
return globalByteSlicePool.Get()
|
||||||
|
}
|
||||||
|
|
||||||
|
// PutByteSlice returns a byte slice to the global pool
|
||||||
|
func PutByteSlice(buf []byte) {
|
||||||
|
globalByteSlicePool.Put(buf)
|
||||||
|
}
|
||||||
@@ -0,0 +1,309 @@
|
|||||||
|
// Package patterns provides cached compiled regex patterns for performance optimization
|
||||||
|
package patterns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RegexCache manages compiled regex patterns with thread-safe access
|
||||||
|
type RegexCache struct {
|
||||||
|
patterns map[string]*regexp.Regexp
|
||||||
|
mu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRegexCache creates a new regex cache instance
|
||||||
|
func NewRegexCache() *RegexCache {
|
||||||
|
return &RegexCache{
|
||||||
|
patterns: make(map[string]*regexp.Regexp),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get retrieves a compiled regex pattern, compiling and caching it if not present
|
||||||
|
func (c *RegexCache) Get(pattern string) (*regexp.Regexp, error) {
|
||||||
|
// First try read lock for existing pattern
|
||||||
|
c.mu.RLock()
|
||||||
|
if regex, exists := c.patterns[pattern]; exists {
|
||||||
|
c.mu.RUnlock()
|
||||||
|
return regex, nil
|
||||||
|
}
|
||||||
|
c.mu.RUnlock()
|
||||||
|
|
||||||
|
// Pattern not found, acquire write lock to compile and cache
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
// Double-check in case another goroutine compiled it while we waited
|
||||||
|
if regex, exists := c.patterns[pattern]; exists {
|
||||||
|
return regex, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compile the pattern
|
||||||
|
regex, err := regexp.Compile(pattern)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache the compiled pattern
|
||||||
|
c.patterns[pattern] = regex
|
||||||
|
return regex, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustGet is like Get but panics if the pattern cannot be compiled
|
||||||
|
func (c *RegexCache) MustGet(pattern string) *regexp.Regexp {
|
||||||
|
regex, err := c.Get(pattern)
|
||||||
|
if err != nil {
|
||||||
|
panic("regex compilation failed for pattern '" + pattern + "': " + err.Error())
|
||||||
|
}
|
||||||
|
return regex
|
||||||
|
}
|
||||||
|
|
||||||
|
// Precompile compiles and caches multiple patterns at once
|
||||||
|
func (c *RegexCache) Precompile(patterns []string) error {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
for _, pattern := range patterns {
|
||||||
|
if _, exists := c.patterns[pattern]; !exists {
|
||||||
|
regex, err := regexp.Compile(pattern)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.patterns[pattern] = regex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size returns the number of cached patterns
|
||||||
|
func (c *RegexCache) Size() int {
|
||||||
|
c.mu.RLock()
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
return len(c.patterns)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear removes all cached patterns
|
||||||
|
func (c *RegexCache) Clear() {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
c.patterns = make(map[string]*regexp.Regexp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global regex cache instance
|
||||||
|
var globalCache = NewRegexCache()
|
||||||
|
|
||||||
|
// Common regex patterns used throughout the OIDC implementation
|
||||||
|
const (
|
||||||
|
// Email validation pattern (RFC 5322 compliant)
|
||||||
|
EmailPattern = `^[a-zA-Z0-9.!#$%&'*+/=?^_` + "`" + `{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$`
|
||||||
|
|
||||||
|
// Domain validation pattern
|
||||||
|
DomainPattern = `^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$`
|
||||||
|
|
||||||
|
// URL validation pattern (http/https)
|
||||||
|
URLPattern = `^https?://[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*(/.*)?$`
|
||||||
|
|
||||||
|
// JWT token pattern (three base64url parts separated by dots)
|
||||||
|
JWTPattern = `^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$`
|
||||||
|
|
||||||
|
// Bearer token pattern (Authorization header)
|
||||||
|
BearerTokenPattern = `^Bearer\s+([A-Za-z0-9._~+/-]+=*)$`
|
||||||
|
|
||||||
|
// Client ID pattern (alphanumeric with common separators)
|
||||||
|
ClientIDPattern = `^[a-zA-Z0-9._-]+$`
|
||||||
|
|
||||||
|
// Scope pattern (space-separated alphanumeric with underscores)
|
||||||
|
ScopePattern = `^[a-zA-Z0-9_]+(\s+[a-zA-Z0-9_]+)*$`
|
||||||
|
|
||||||
|
// Session ID pattern (hexadecimal)
|
||||||
|
SessionIDPattern = `^[a-fA-F0-9]{32,128}$`
|
||||||
|
|
||||||
|
// CSRF token pattern (base64url)
|
||||||
|
CSRFTokenPattern = `^[A-Za-z0-9_-]+$`
|
||||||
|
|
||||||
|
// Nonce pattern (base64url)
|
||||||
|
NoncePattern = `^[A-Za-z0-9_-]+$`
|
||||||
|
|
||||||
|
// Code verifier pattern for PKCE (base64url, 43-128 chars)
|
||||||
|
CodeVerifierPattern = `^[A-Za-z0-9_-]{43,128}$`
|
||||||
|
|
||||||
|
// Authorization code pattern (base64url)
|
||||||
|
AuthCodePattern = `^[A-Za-z0-9._~+/-]+=*$`
|
||||||
|
|
||||||
|
// Redirect URI validation (must be absolute HTTP/HTTPS URL)
|
||||||
|
RedirectURIPattern = `^https?://[^\s/$.?#].[^\s]*$`
|
||||||
|
|
||||||
|
// User-Agent pattern for bot detection
|
||||||
|
BotUserAgentPattern = `(?i)(bot|crawler|spider|scraper|curl|wget|python|java|go-http)`
|
||||||
|
|
||||||
|
// IP address pattern (IPv4)
|
||||||
|
IPv4Pattern = `^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$`
|
||||||
|
|
||||||
|
// Tenant ID pattern (UUID format for Azure, etc.)
|
||||||
|
TenantIDPattern = `^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`
|
||||||
|
)
|
||||||
|
|
||||||
|
// Precompiled common patterns for immediate use
|
||||||
|
var (
|
||||||
|
EmailRegex *regexp.Regexp
|
||||||
|
DomainRegex *regexp.Regexp
|
||||||
|
URLRegex *regexp.Regexp
|
||||||
|
JWTRegex *regexp.Regexp
|
||||||
|
BearerTokenRegex *regexp.Regexp
|
||||||
|
ClientIDRegex *regexp.Regexp
|
||||||
|
ScopeRegex *regexp.Regexp
|
||||||
|
SessionIDRegex *regexp.Regexp
|
||||||
|
CSRFTokenRegex *regexp.Regexp
|
||||||
|
NonceRegex *regexp.Regexp
|
||||||
|
CodeVerifierRegex *regexp.Regexp
|
||||||
|
AuthCodeRegex *regexp.Regexp
|
||||||
|
RedirectURIRegex *regexp.Regexp
|
||||||
|
BotUserAgentRegex *regexp.Regexp
|
||||||
|
IPv4Regex *regexp.Regexp
|
||||||
|
TenantIDRegex *regexp.Regexp
|
||||||
|
)
|
||||||
|
|
||||||
|
// Initialize precompiled patterns
|
||||||
|
func init() {
|
||||||
|
commonPatterns := []string{
|
||||||
|
EmailPattern,
|
||||||
|
DomainPattern,
|
||||||
|
URLPattern,
|
||||||
|
JWTPattern,
|
||||||
|
BearerTokenPattern,
|
||||||
|
ClientIDPattern,
|
||||||
|
ScopePattern,
|
||||||
|
SessionIDPattern,
|
||||||
|
CSRFTokenPattern,
|
||||||
|
NoncePattern,
|
||||||
|
CodeVerifierPattern,
|
||||||
|
AuthCodePattern,
|
||||||
|
RedirectURIPattern,
|
||||||
|
BotUserAgentPattern,
|
||||||
|
IPv4Pattern,
|
||||||
|
TenantIDPattern,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := globalCache.Precompile(commonPatterns); err != nil {
|
||||||
|
panic("Failed to precompile common regex patterns: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assign precompiled patterns to global variables for easy access
|
||||||
|
EmailRegex = globalCache.MustGet(EmailPattern)
|
||||||
|
DomainRegex = globalCache.MustGet(DomainPattern)
|
||||||
|
URLRegex = globalCache.MustGet(URLPattern)
|
||||||
|
JWTRegex = globalCache.MustGet(JWTPattern)
|
||||||
|
BearerTokenRegex = globalCache.MustGet(BearerTokenPattern)
|
||||||
|
ClientIDRegex = globalCache.MustGet(ClientIDPattern)
|
||||||
|
ScopeRegex = globalCache.MustGet(ScopePattern)
|
||||||
|
SessionIDRegex = globalCache.MustGet(SessionIDPattern)
|
||||||
|
CSRFTokenRegex = globalCache.MustGet(CSRFTokenPattern)
|
||||||
|
NonceRegex = globalCache.MustGet(NoncePattern)
|
||||||
|
CodeVerifierRegex = globalCache.MustGet(CodeVerifierPattern)
|
||||||
|
AuthCodeRegex = globalCache.MustGet(AuthCodePattern)
|
||||||
|
RedirectURIRegex = globalCache.MustGet(RedirectURIPattern)
|
||||||
|
BotUserAgentRegex = globalCache.MustGet(BotUserAgentPattern)
|
||||||
|
IPv4Regex = globalCache.MustGet(IPv4Pattern)
|
||||||
|
TenantIDRegex = globalCache.MustGet(TenantIDPattern)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global helper functions for common validations
|
||||||
|
|
||||||
|
// ValidateEmail checks if an email address is valid
|
||||||
|
func ValidateEmail(email string) bool {
|
||||||
|
return EmailRegex.MatchString(email)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateDomain checks if a domain name is valid
|
||||||
|
func ValidateDomain(domain string) bool {
|
||||||
|
return DomainRegex.MatchString(domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateURL checks if a URL is valid (http/https)
|
||||||
|
func ValidateURL(url string) bool {
|
||||||
|
return URLRegex.MatchString(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateJWT checks if a token has valid JWT format
|
||||||
|
func ValidateJWT(token string) bool {
|
||||||
|
return JWTRegex.MatchString(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtractBearerToken extracts the token from a Bearer authorization header
|
||||||
|
func ExtractBearerToken(authHeader string) (string, bool) {
|
||||||
|
matches := BearerTokenRegex.FindStringSubmatch(authHeader)
|
||||||
|
if len(matches) == 2 {
|
||||||
|
return matches[1], true
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateClientID checks if a client ID has valid format
|
||||||
|
func ValidateClientID(clientID string) bool {
|
||||||
|
return ClientIDRegex.MatchString(clientID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateScopes checks if scopes string has valid format
|
||||||
|
func ValidateScopes(scopes string) bool {
|
||||||
|
return ScopeRegex.MatchString(scopes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateSessionID checks if a session ID has valid format
|
||||||
|
func ValidateSessionID(sessionID string) bool {
|
||||||
|
return SessionIDRegex.MatchString(sessionID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateCSRFToken checks if a CSRF token has valid format
|
||||||
|
func ValidateCSRFToken(token string) bool {
|
||||||
|
return CSRFTokenRegex.MatchString(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateNonce checks if a nonce has valid format
|
||||||
|
func ValidateNonce(nonce string) bool {
|
||||||
|
return NonceRegex.MatchString(nonce)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateCodeVerifier checks if a PKCE code verifier has valid format
|
||||||
|
func ValidateCodeVerifier(verifier string) bool {
|
||||||
|
return CodeVerifierRegex.MatchString(verifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateAuthCode checks if an authorization code has valid format
|
||||||
|
func ValidateAuthCode(code string) bool {
|
||||||
|
return AuthCodeRegex.MatchString(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateRedirectURI checks if a redirect URI is valid
|
||||||
|
func ValidateRedirectURI(uri string) bool {
|
||||||
|
return RedirectURIRegex.MatchString(uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsBotUserAgent checks if a User-Agent suggests an automated client
|
||||||
|
func IsBotUserAgent(userAgent string) bool {
|
||||||
|
return BotUserAgentRegex.MatchString(userAgent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateIPv4 checks if an IP address is valid IPv4
|
||||||
|
func ValidateIPv4(ip string) bool {
|
||||||
|
return IPv4Regex.MatchString(ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateTenantID checks if a tenant ID has valid UUID format
|
||||||
|
func ValidateTenantID(tenantID string) bool {
|
||||||
|
return TenantIDRegex.MatchString(tenantID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetGlobalCache returns the global regex cache instance
|
||||||
|
func GetGlobalCache() *RegexCache {
|
||||||
|
return globalCache
|
||||||
|
}
|
||||||
|
|
||||||
|
// CompilePattern compiles a pattern using the global cache
|
||||||
|
func CompilePattern(pattern string) (*regexp.Regexp, error) {
|
||||||
|
return globalCache.Get(pattern)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustCompilePattern compiles a pattern using the global cache, panicking on error
|
||||||
|
func MustCompilePattern(pattern string) *regexp.Regexp {
|
||||||
|
return globalCache.MustGet(pattern)
|
||||||
|
}
|
||||||
@@ -0,0 +1,225 @@
|
|||||||
|
package patterns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRegexCache_Get(t *testing.T) {
|
||||||
|
cache := NewRegexCache()
|
||||||
|
|
||||||
|
pattern := `^test\d+$`
|
||||||
|
|
||||||
|
// First call should compile and cache
|
||||||
|
regex1, err := cache.Get(pattern)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get regex: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Second call should return cached version
|
||||||
|
regex2, err := cache.Get(pattern)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get cached regex: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should be the same instance
|
||||||
|
if regex1 != regex2 {
|
||||||
|
t.Error("Expected same regex instance from cache")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test the regex works
|
||||||
|
if !regex1.MatchString("test123") {
|
||||||
|
t.Error("Regex should match 'test123'")
|
||||||
|
}
|
||||||
|
|
||||||
|
if regex1.MatchString("test") {
|
||||||
|
t.Error("Regex should not match 'test'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegexCache_ConcurrentAccess(t *testing.T) {
|
||||||
|
cache := NewRegexCache()
|
||||||
|
pattern := `^concurrent\d+$`
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
results := make([]*regexp.Regexp, 10)
|
||||||
|
errors := make([]error, 10)
|
||||||
|
|
||||||
|
// Launch multiple goroutines to access the same pattern
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(index int) {
|
||||||
|
defer wg.Done()
|
||||||
|
regex, err := cache.Get(pattern)
|
||||||
|
results[index] = regex
|
||||||
|
errors[index] = err
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
// Check all succeeded
|
||||||
|
for i, err := range errors {
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Goroutine %d failed: %v", i, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// All should return the same instance
|
||||||
|
first := results[0]
|
||||||
|
for i, regex := range results[1:] {
|
||||||
|
if regex != first {
|
||||||
|
t.Errorf("Goroutine %d got different regex instance", i+1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegexCache_InvalidPattern(t *testing.T) {
|
||||||
|
cache := NewRegexCache()
|
||||||
|
|
||||||
|
_, err := cache.Get(`[invalid`)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected error for invalid regex pattern")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegexCache_Precompile(t *testing.T) {
|
||||||
|
cache := NewRegexCache()
|
||||||
|
|
||||||
|
patterns := []string{
|
||||||
|
`^test1$`,
|
||||||
|
`^test2$`,
|
||||||
|
`^test3$`,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cache.Precompile(patterns)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to precompile patterns: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cache.Size() != 3 {
|
||||||
|
t.Errorf("Expected cache size 3, got %d", cache.Size())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should be able to get precompiled patterns without error
|
||||||
|
for _, pattern := range patterns {
|
||||||
|
_, err := cache.Get(pattern)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Failed to get precompiled pattern %s: %v", pattern, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidationFunctions(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
function func(string) bool
|
||||||
|
valid []string
|
||||||
|
invalid []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "ValidateEmail",
|
||||||
|
function: ValidateEmail,
|
||||||
|
valid: []string{"[email protected]", "[email protected]", "[email protected]"},
|
||||||
|
invalid: []string{"invalid-email", "@domain.com", "user@", ""},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ValidateDomain",
|
||||||
|
function: ValidateDomain,
|
||||||
|
valid: []string{"example.com", "sub.domain.org", "test.co.uk"},
|
||||||
|
invalid: []string{"", "invalid..domain", ".example.com", "domain."},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ValidateJWT",
|
||||||
|
function: ValidateJWT,
|
||||||
|
valid: []string{"eyJ0.eyJ1.sig", "a.b.c"},
|
||||||
|
invalid: []string{"invalid", "a.b", "a.b.c.d", ""},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ValidateClientID",
|
||||||
|
function: ValidateClientID,
|
||||||
|
valid: []string{"client123", "my-client_id", "123.456"},
|
||||||
|
invalid: []string{"", "client with spaces", "client@invalid"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
for _, valid := range tt.valid {
|
||||||
|
if !tt.function(valid) {
|
||||||
|
t.Errorf("%s should be valid: %s", tt.name, valid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, invalid := range tt.invalid {
|
||||||
|
if tt.function(invalid) {
|
||||||
|
t.Errorf("%s should be invalid: %s", tt.name, invalid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractBearerToken(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
header string
|
||||||
|
expected string
|
||||||
|
valid bool
|
||||||
|
}{
|
||||||
|
{"Bearer abc123", "abc123", true},
|
||||||
|
{"Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9", "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9", true},
|
||||||
|
{"bearer token123", "", false}, // case sensitive
|
||||||
|
{"Basic abc123", "", false},
|
||||||
|
{"Bearer", "", false},
|
||||||
|
{"", "", false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
token, valid := ExtractBearerToken(tt.header)
|
||||||
|
if valid != tt.valid {
|
||||||
|
t.Errorf("ExtractBearerToken(%q) valid = %v, want %v", tt.header, valid, tt.valid)
|
||||||
|
}
|
||||||
|
if token != tt.expected {
|
||||||
|
t.Errorf("ExtractBearerToken(%q) token = %q, want %q", tt.header, token, tt.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkRegexCache_Get(b *testing.B) {
|
||||||
|
cache := NewRegexCache()
|
||||||
|
pattern := `^benchmark\d+$`
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
b.RunParallel(func(pb *testing.PB) {
|
||||||
|
for pb.Next() {
|
||||||
|
_, err := cache.Get(pattern)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkRegexCache_Validation(b *testing.B) {
|
||||||
|
email := "[email protected]"
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
b.RunParallel(func(pb *testing.PB) {
|
||||||
|
for pb.Next() {
|
||||||
|
ValidateEmail(email)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkRegex_DirectCompile(b *testing.B) {
|
||||||
|
pattern := `^benchmark\d+$`
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_, err := regexp.Compile(pattern)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,403 @@
|
|||||||
|
// Package security provides security-related middleware and utilities
|
||||||
|
package security
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SecurityHeadersConfig configures security headers
|
||||||
|
type SecurityHeadersConfig struct {
|
||||||
|
// Content Security Policy
|
||||||
|
ContentSecurityPolicy string
|
||||||
|
|
||||||
|
// HSTS settings
|
||||||
|
StrictTransportSecurity string
|
||||||
|
StrictTransportSecurityMaxAge int // seconds
|
||||||
|
StrictTransportSecuritySubdomains bool
|
||||||
|
StrictTransportSecurityPreload bool
|
||||||
|
|
||||||
|
// Frame options
|
||||||
|
FrameOptions string // DENY, SAMEORIGIN, or ALLOW-FROM uri
|
||||||
|
|
||||||
|
// Content type options
|
||||||
|
ContentTypeOptions string // nosniff
|
||||||
|
|
||||||
|
// XSS protection
|
||||||
|
XSSProtection string // 1; mode=block
|
||||||
|
|
||||||
|
// Referrer policy
|
||||||
|
ReferrerPolicy string
|
||||||
|
|
||||||
|
// Permissions policy
|
||||||
|
PermissionsPolicy string
|
||||||
|
|
||||||
|
// Cross-origin settings
|
||||||
|
CrossOriginEmbedderPolicy string
|
||||||
|
CrossOriginOpenerPolicy string
|
||||||
|
CrossOriginResourcePolicy string
|
||||||
|
|
||||||
|
// CORS settings
|
||||||
|
CORSEnabled bool
|
||||||
|
CORSAllowedOrigins []string
|
||||||
|
CORSAllowedMethods []string
|
||||||
|
CORSAllowedHeaders []string
|
||||||
|
CORSAllowCredentials bool
|
||||||
|
CORSMaxAge int // seconds
|
||||||
|
|
||||||
|
// Custom headers
|
||||||
|
CustomHeaders map[string]string
|
||||||
|
|
||||||
|
// Security features
|
||||||
|
DisableServerHeader bool
|
||||||
|
DisablePoweredByHeader bool
|
||||||
|
|
||||||
|
// Development mode (less strict for local development)
|
||||||
|
DevelopmentMode bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultSecurityConfig returns a secure default configuration
|
||||||
|
func DefaultSecurityConfig() *SecurityHeadersConfig {
|
||||||
|
return &SecurityHeadersConfig{
|
||||||
|
ContentSecurityPolicy: "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'none';",
|
||||||
|
|
||||||
|
StrictTransportSecurityMaxAge: 31536000, // 1 year
|
||||||
|
StrictTransportSecuritySubdomains: true,
|
||||||
|
StrictTransportSecurityPreload: true,
|
||||||
|
|
||||||
|
FrameOptions: "DENY",
|
||||||
|
ContentTypeOptions: "nosniff",
|
||||||
|
XSSProtection: "1; mode=block",
|
||||||
|
ReferrerPolicy: "strict-origin-when-cross-origin",
|
||||||
|
|
||||||
|
PermissionsPolicy: "geolocation=(), microphone=(), camera=(), payment=(), usb=(), magnetometer=(), gyroscope=(), speaker=()",
|
||||||
|
|
||||||
|
CrossOriginEmbedderPolicy: "require-corp",
|
||||||
|
CrossOriginOpenerPolicy: "same-origin",
|
||||||
|
CrossOriginResourcePolicy: "same-origin",
|
||||||
|
|
||||||
|
CORSEnabled: false,
|
||||||
|
CORSAllowedMethods: []string{"GET", "POST", "OPTIONS"},
|
||||||
|
CORSAllowedHeaders: []string{"Authorization", "Content-Type", "X-Requested-With"},
|
||||||
|
CORSMaxAge: 86400, // 24 hours
|
||||||
|
|
||||||
|
DisableServerHeader: true,
|
||||||
|
DisablePoweredByHeader: true,
|
||||||
|
|
||||||
|
DevelopmentMode: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DevelopmentSecurityConfig returns a configuration suitable for development
|
||||||
|
func DevelopmentSecurityConfig() *SecurityHeadersConfig {
|
||||||
|
config := DefaultSecurityConfig()
|
||||||
|
|
||||||
|
// Relax CSP for development
|
||||||
|
config.ContentSecurityPolicy = "default-src 'self' 'unsafe-inline' 'unsafe-eval'; img-src 'self' data: https: http:; connect-src 'self' ws: wss:;"
|
||||||
|
|
||||||
|
// Allow framing for development tools
|
||||||
|
config.FrameOptions = "SAMEORIGIN"
|
||||||
|
|
||||||
|
// Enable CORS for local development
|
||||||
|
config.CORSEnabled = true
|
||||||
|
config.CORSAllowedOrigins = []string{"http://localhost:*", "http://127.0.0.1:*"}
|
||||||
|
config.CORSAllowCredentials = true
|
||||||
|
|
||||||
|
// Relax cross-origin policies
|
||||||
|
config.CrossOriginEmbedderPolicy = ""
|
||||||
|
config.CrossOriginOpenerPolicy = "unsafe-none"
|
||||||
|
config.CrossOriginResourcePolicy = "cross-origin"
|
||||||
|
|
||||||
|
config.DevelopmentMode = true
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
// SecurityHeadersMiddleware applies security headers to HTTP responses
|
||||||
|
type SecurityHeadersMiddleware struct {
|
||||||
|
config *SecurityHeadersConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSecurityHeadersMiddleware creates a new security headers middleware
|
||||||
|
func NewSecurityHeadersMiddleware(config *SecurityHeadersConfig) *SecurityHeadersMiddleware {
|
||||||
|
if config == nil {
|
||||||
|
config = DefaultSecurityConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
return &SecurityHeadersMiddleware{
|
||||||
|
config: config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply applies security headers to the response
|
||||||
|
func (m *SecurityHeadersMiddleware) Apply(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
headers := rw.Header()
|
||||||
|
|
||||||
|
// Content Security Policy
|
||||||
|
if m.config.ContentSecurityPolicy != "" {
|
||||||
|
headers.Set("Content-Security-Policy", m.config.ContentSecurityPolicy)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HSTS (only for HTTPS)
|
||||||
|
if req.TLS != nil || req.Header.Get("X-Forwarded-Proto") == "https" {
|
||||||
|
hstsValue := m.buildHSTSHeader()
|
||||||
|
if hstsValue != "" {
|
||||||
|
headers.Set("Strict-Transport-Security", hstsValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Frame options
|
||||||
|
if m.config.FrameOptions != "" {
|
||||||
|
headers.Set("X-Frame-Options", m.config.FrameOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Content type options
|
||||||
|
if m.config.ContentTypeOptions != "" {
|
||||||
|
headers.Set("X-Content-Type-Options", m.config.ContentTypeOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
// XSS protection
|
||||||
|
if m.config.XSSProtection != "" {
|
||||||
|
headers.Set("X-XSS-Protection", m.config.XSSProtection)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Referrer policy
|
||||||
|
if m.config.ReferrerPolicy != "" {
|
||||||
|
headers.Set("Referrer-Policy", m.config.ReferrerPolicy)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Permissions policy
|
||||||
|
if m.config.PermissionsPolicy != "" {
|
||||||
|
headers.Set("Permissions-Policy", m.config.PermissionsPolicy)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cross-origin policies
|
||||||
|
if m.config.CrossOriginEmbedderPolicy != "" {
|
||||||
|
headers.Set("Cross-Origin-Embedder-Policy", m.config.CrossOriginEmbedderPolicy)
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.config.CrossOriginOpenerPolicy != "" {
|
||||||
|
headers.Set("Cross-Origin-Opener-Policy", m.config.CrossOriginOpenerPolicy)
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.config.CrossOriginResourcePolicy != "" {
|
||||||
|
headers.Set("Cross-Origin-Resource-Policy", m.config.CrossOriginResourcePolicy)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CORS headers
|
||||||
|
if m.config.CORSEnabled {
|
||||||
|
m.applyCORSHeaders(rw, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom headers
|
||||||
|
for name, value := range m.config.CustomHeaders {
|
||||||
|
headers.Set(name, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove server identification headers
|
||||||
|
if m.config.DisableServerHeader {
|
||||||
|
headers.Del("Server")
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.config.DisablePoweredByHeader {
|
||||||
|
headers.Del("X-Powered-By")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add security timestamp for debugging
|
||||||
|
if m.config.DevelopmentMode {
|
||||||
|
headers.Set("X-Security-Headers-Applied", time.Now().UTC().Format(time.RFC3339))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildHSTSHeader constructs the HSTS header value
|
||||||
|
func (m *SecurityHeadersMiddleware) buildHSTSHeader() string {
|
||||||
|
if m.config.StrictTransportSecurityMaxAge <= 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := []string{
|
||||||
|
"max-age=" + string(rune(m.config.StrictTransportSecurityMaxAge)),
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.config.StrictTransportSecuritySubdomains {
|
||||||
|
parts = append(parts, "includeSubDomains")
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.config.StrictTransportSecurityPreload {
|
||||||
|
parts = append(parts, "preload")
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(parts, "; ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyCORSHeaders applies CORS headers based on the request
|
||||||
|
func (m *SecurityHeadersMiddleware) applyCORSHeaders(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
headers := rw.Header()
|
||||||
|
origin := req.Header.Get("Origin")
|
||||||
|
|
||||||
|
// Check if origin is allowed
|
||||||
|
if origin != "" && m.isOriginAllowed(origin) {
|
||||||
|
headers.Set("Access-Control-Allow-Origin", origin)
|
||||||
|
} else if len(m.config.CORSAllowedOrigins) == 1 && m.config.CORSAllowedOrigins[0] == "*" {
|
||||||
|
headers.Set("Access-Control-Allow-Origin", "*")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set other CORS headers
|
||||||
|
if len(m.config.CORSAllowedMethods) > 0 {
|
||||||
|
headers.Set("Access-Control-Allow-Methods", strings.Join(m.config.CORSAllowedMethods, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(m.config.CORSAllowedHeaders) > 0 {
|
||||||
|
headers.Set("Access-Control-Allow-Headers", strings.Join(m.config.CORSAllowedHeaders, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.config.CORSAllowCredentials {
|
||||||
|
headers.Set("Access-Control-Allow-Credentials", "true")
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.config.CORSMaxAge > 0 {
|
||||||
|
headers.Set("Access-Control-Max-Age", string(rune(m.config.CORSMaxAge)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle preflight requests
|
||||||
|
if req.Method == "OPTIONS" {
|
||||||
|
headers.Set("Access-Control-Allow-Methods", strings.Join(m.config.CORSAllowedMethods, ", "))
|
||||||
|
headers.Set("Access-Control-Allow-Headers", strings.Join(m.config.CORSAllowedHeaders, ", "))
|
||||||
|
rw.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// isOriginAllowed checks if the origin is in the allowed list
|
||||||
|
func (m *SecurityHeadersMiddleware) isOriginAllowed(origin string) bool {
|
||||||
|
for _, allowed := range m.config.CORSAllowedOrigins {
|
||||||
|
if m.matchOrigin(origin, allowed) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// matchOrigin checks if an origin matches an allowed pattern
|
||||||
|
func (m *SecurityHeadersMiddleware) matchOrigin(origin, pattern string) bool {
|
||||||
|
// Exact match
|
||||||
|
if origin == pattern {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wildcard subdomain match (e.g., "https://*.example.com")
|
||||||
|
if strings.Contains(pattern, "*") {
|
||||||
|
// Simple wildcard matching for subdomains
|
||||||
|
if strings.HasPrefix(pattern, "https://*.") {
|
||||||
|
domain := strings.TrimPrefix(pattern, "https://*.")
|
||||||
|
if strings.HasSuffix(origin, "."+domain) || origin == "https://"+domain {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(pattern, "http://*.") {
|
||||||
|
domain := strings.TrimPrefix(pattern, "http://*.")
|
||||||
|
if strings.HasSuffix(origin, "."+domain) || origin == "http://"+domain {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Port wildcard match (e.g., "http://localhost:*")
|
||||||
|
if strings.HasSuffix(pattern, ":*") {
|
||||||
|
prefix := strings.TrimSuffix(pattern, ":*")
|
||||||
|
if strings.HasPrefix(origin, prefix+":") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap wraps an HTTP handler with security headers
|
||||||
|
func (m *SecurityHeadersMiddleware) Wrap(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
m.Apply(rw, req)
|
||||||
|
next.ServeHTTP(rw, req)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SecurityHeadersHandler is a convenience function that creates and applies security headers
|
||||||
|
func SecurityHeadersHandler(config *SecurityHeadersConfig) func(http.ResponseWriter, *http.Request) {
|
||||||
|
middleware := NewSecurityHeadersMiddleware(config)
|
||||||
|
return middleware.Apply
|
||||||
|
}
|
||||||
|
|
||||||
|
// Common security header presets
|
||||||
|
|
||||||
|
// StrictSecurityConfig returns a very strict security configuration
|
||||||
|
func StrictSecurityConfig() *SecurityHeadersConfig {
|
||||||
|
config := DefaultSecurityConfig()
|
||||||
|
|
||||||
|
// Very strict CSP
|
||||||
|
config.ContentSecurityPolicy = "default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self'; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self';"
|
||||||
|
|
||||||
|
// Stricter frame options
|
||||||
|
config.FrameOptions = "DENY"
|
||||||
|
|
||||||
|
// Disable CORS entirely
|
||||||
|
config.CORSEnabled = false
|
||||||
|
|
||||||
|
// Very strict cross-origin policies
|
||||||
|
config.CrossOriginEmbedderPolicy = "require-corp"
|
||||||
|
config.CrossOriginOpenerPolicy = "same-origin"
|
||||||
|
config.CrossOriginResourcePolicy = "same-site"
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
// APISecurityConfig returns a configuration suitable for APIs
|
||||||
|
func APISecurityConfig() *SecurityHeadersConfig {
|
||||||
|
config := DefaultSecurityConfig()
|
||||||
|
|
||||||
|
// API-friendly CSP
|
||||||
|
config.ContentSecurityPolicy = "default-src 'none'; frame-ancestors 'none';"
|
||||||
|
|
||||||
|
// Enable CORS for APIs
|
||||||
|
config.CORSEnabled = true
|
||||||
|
config.CORSAllowedMethods = []string{"GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"}
|
||||||
|
config.CORSAllowedHeaders = []string{"Authorization", "Content-Type", "X-Requested-With", "X-API-Key"}
|
||||||
|
|
||||||
|
// API-appropriate policies
|
||||||
|
config.CrossOriginResourcePolicy = "cross-origin"
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateConfig validates the security configuration
|
||||||
|
func (c *SecurityHeadersConfig) Validate() error {
|
||||||
|
// Validate HSTS max age
|
||||||
|
if c.StrictTransportSecurityMaxAge < 0 {
|
||||||
|
c.StrictTransportSecurityMaxAge = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate CORS max age
|
||||||
|
if c.CORSMaxAge < 0 {
|
||||||
|
c.CORSMaxAge = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate frame options
|
||||||
|
validFrameOptions := []string{"DENY", "SAMEORIGIN", ""}
|
||||||
|
isValidFrameOption := false
|
||||||
|
for _, valid := range validFrameOptions {
|
||||||
|
if c.FrameOptions == valid || strings.HasPrefix(c.FrameOptions, "ALLOW-FROM ") {
|
||||||
|
isValidFrameOption = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !isValidFrameOption {
|
||||||
|
c.FrameOptions = "DENY"
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplyToResponseWriter is a helper function to quickly apply security headers
|
||||||
|
func ApplySecurityHeaders(rw http.ResponseWriter, req *http.Request, config *SecurityHeadersConfig) {
|
||||||
|
middleware := NewSecurityHeadersMiddleware(config)
|
||||||
|
middleware.Apply(rw, req)
|
||||||
|
}
|
||||||
@@ -0,0 +1,350 @@
|
|||||||
|
package security
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDefaultSecurityConfig(t *testing.T) {
|
||||||
|
config := DefaultSecurityConfig()
|
||||||
|
|
||||||
|
if config.ContentSecurityPolicy == "" {
|
||||||
|
t.Error("Expected default CSP to be set")
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.FrameOptions != "DENY" {
|
||||||
|
t.Errorf("Expected frame options to be DENY, got %s", config.FrameOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !config.DisableServerHeader {
|
||||||
|
t.Error("Expected server header to be disabled by default")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSecurityHeadersMiddleware_Apply(t *testing.T) {
|
||||||
|
config := DefaultSecurityConfig()
|
||||||
|
middleware := NewSecurityHeadersMiddleware(config)
|
||||||
|
|
||||||
|
// Create a mock request (HTTPS)
|
||||||
|
req := httptest.NewRequest("GET", "https://example.com/test", nil)
|
||||||
|
req.TLS = &tls.ConnectionState{} // Mock TLS
|
||||||
|
|
||||||
|
// Create a response recorder
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
|
// Apply security headers
|
||||||
|
middleware.Apply(rr, req)
|
||||||
|
|
||||||
|
headers := rr.Header()
|
||||||
|
|
||||||
|
// Check that security headers are set
|
||||||
|
if headers.Get("Content-Security-Policy") == "" {
|
||||||
|
t.Error("Expected CSP header to be set")
|
||||||
|
}
|
||||||
|
|
||||||
|
if headers.Get("X-Frame-Options") != "DENY" {
|
||||||
|
t.Errorf("Expected X-Frame-Options to be DENY, got %s", headers.Get("X-Frame-Options"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if headers.Get("X-Content-Type-Options") != "nosniff" {
|
||||||
|
t.Errorf("Expected X-Content-Type-Options to be nosniff, got %s", headers.Get("X-Content-Type-Options"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if headers.Get("X-XSS-Protection") != "1; mode=block" {
|
||||||
|
t.Errorf("Expected X-XSS-Protection to be '1; mode=block', got %s", headers.Get("X-XSS-Protection"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check HSTS for HTTPS requests
|
||||||
|
hsts := headers.Get("Strict-Transport-Security")
|
||||||
|
if hsts == "" {
|
||||||
|
t.Error("Expected HSTS header for HTTPS request")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(hsts, "max-age=") {
|
||||||
|
t.Error("Expected HSTS header to contain max-age")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSecurityHeadersMiddleware_HTTPSOnly(t *testing.T) {
|
||||||
|
config := DefaultSecurityConfig()
|
||||||
|
middleware := NewSecurityHeadersMiddleware(config)
|
||||||
|
|
||||||
|
// Test HTTP request (no HSTS)
|
||||||
|
req := httptest.NewRequest("GET", "http://example.com/test", nil)
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
|
middleware.Apply(rr, req)
|
||||||
|
|
||||||
|
if rr.Header().Get("Strict-Transport-Security") != "" {
|
||||||
|
t.Error("Expected no HSTS header for HTTP request")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test HTTPS request (with HSTS)
|
||||||
|
req = httptest.NewRequest("GET", "https://example.com/test", nil)
|
||||||
|
req.TLS = &tls.ConnectionState{}
|
||||||
|
rr = httptest.NewRecorder()
|
||||||
|
|
||||||
|
middleware.Apply(rr, req)
|
||||||
|
|
||||||
|
if rr.Header().Get("Strict-Transport-Security") == "" {
|
||||||
|
t.Error("Expected HSTS header for HTTPS request")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCORSHeaders(t *testing.T) {
|
||||||
|
config := DefaultSecurityConfig()
|
||||||
|
config.CORSEnabled = true
|
||||||
|
config.CORSAllowedOrigins = []string{"https://example.com", "https://*.test.com"}
|
||||||
|
config.CORSAllowCredentials = true
|
||||||
|
|
||||||
|
middleware := NewSecurityHeadersMiddleware(config)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
origin string
|
||||||
|
expectedOrigin string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "exact match",
|
||||||
|
origin: "https://example.com",
|
||||||
|
expectedOrigin: "https://example.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wildcard subdomain match",
|
||||||
|
origin: "https://api.test.com",
|
||||||
|
expectedOrigin: "https://api.test.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no match",
|
||||||
|
origin: "https://malicious.com",
|
||||||
|
expectedOrigin: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
req := httptest.NewRequest("GET", "https://example.com/test", nil)
|
||||||
|
if tt.origin != "" {
|
||||||
|
req.Header.Set("Origin", tt.origin)
|
||||||
|
}
|
||||||
|
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
middleware.Apply(rr, req)
|
||||||
|
|
||||||
|
actualOrigin := rr.Header().Get("Access-Control-Allow-Origin")
|
||||||
|
if actualOrigin != tt.expectedOrigin {
|
||||||
|
t.Errorf("Expected origin %s, got %s", tt.expectedOrigin, actualOrigin)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.expectedOrigin != "" {
|
||||||
|
// Should have credentials header
|
||||||
|
if rr.Header().Get("Access-Control-Allow-Credentials") != "true" {
|
||||||
|
t.Error("Expected credentials header for allowed origin")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCORSPreflight(t *testing.T) {
|
||||||
|
config := DefaultSecurityConfig()
|
||||||
|
config.CORSEnabled = true
|
||||||
|
config.CORSAllowedOrigins = []string{"*"}
|
||||||
|
config.CORSAllowedMethods = []string{"GET", "POST", "OPTIONS"}
|
||||||
|
|
||||||
|
middleware := NewSecurityHeadersMiddleware(config)
|
||||||
|
|
||||||
|
req := httptest.NewRequest("OPTIONS", "https://example.com/test", nil)
|
||||||
|
req.Header.Set("Origin", "https://other.com")
|
||||||
|
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
middleware.Apply(rr, req)
|
||||||
|
|
||||||
|
if rr.Header().Get("Access-Control-Allow-Origin") != "*" {
|
||||||
|
t.Error("Expected wildcard origin for preflight request")
|
||||||
|
}
|
||||||
|
|
||||||
|
if rr.Header().Get("Access-Control-Allow-Methods") == "" {
|
||||||
|
t.Error("Expected methods header for preflight request")
|
||||||
|
}
|
||||||
|
|
||||||
|
if rr.Code != http.StatusOK {
|
||||||
|
t.Errorf("Expected status 200 for preflight, got %d", rr.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOriginMatching(t *testing.T) {
|
||||||
|
config := &SecurityHeadersConfig{
|
||||||
|
CORSEnabled: true,
|
||||||
|
CORSAllowedOrigins: []string{
|
||||||
|
"https://example.com",
|
||||||
|
"https://*.example.com",
|
||||||
|
"http://localhost:*",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
middleware := NewSecurityHeadersMiddleware(config)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
origin string
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{"https://example.com", true},
|
||||||
|
{"https://api.example.com", true},
|
||||||
|
{"https://sub.api.example.com", true},
|
||||||
|
{"http://localhost:3000", true},
|
||||||
|
{"http://localhost:8080", true},
|
||||||
|
{"https://malicious.com", false},
|
||||||
|
{"http://example.com", false}, // Different scheme
|
||||||
|
{"https://example.com.evil.com", false}, // Domain suffix attack
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.origin, func(t *testing.T) {
|
||||||
|
result := middleware.isOriginAllowed(tt.origin)
|
||||||
|
if result != tt.expected {
|
||||||
|
t.Errorf("Origin %s: expected %v, got %v", tt.origin, tt.expected, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDevelopmentMode(t *testing.T) {
|
||||||
|
config := DevelopmentSecurityConfig()
|
||||||
|
|
||||||
|
if !config.DevelopmentMode {
|
||||||
|
t.Error("Expected development mode to be enabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !config.CORSEnabled {
|
||||||
|
t.Error("Expected CORS to be enabled in development mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.FrameOptions != "SAMEORIGIN" {
|
||||||
|
t.Errorf("Expected frame options to be SAMEORIGIN in dev mode, got %s", config.FrameOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should be less strict CSP
|
||||||
|
if strings.Contains(config.ContentSecurityPolicy, "'none'") {
|
||||||
|
t.Error("Expected less strict CSP in development mode")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStrictSecurityConfig(t *testing.T) {
|
||||||
|
config := StrictSecurityConfig()
|
||||||
|
|
||||||
|
if !strings.Contains(config.ContentSecurityPolicy, "'none'") {
|
||||||
|
t.Error("Expected very strict CSP with 'none' defaults")
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.CORSEnabled {
|
||||||
|
t.Error("Expected CORS to be disabled in strict mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.FrameOptions != "DENY" {
|
||||||
|
t.Error("Expected frame options to be DENY in strict mode")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPISecurityConfig(t *testing.T) {
|
||||||
|
config := APISecurityConfig()
|
||||||
|
|
||||||
|
if !config.CORSEnabled {
|
||||||
|
t.Error("Expected CORS to be enabled for API config")
|
||||||
|
}
|
||||||
|
|
||||||
|
methods := config.CORSAllowedMethods
|
||||||
|
expectedMethods := []string{"GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"}
|
||||||
|
|
||||||
|
for _, method := range expectedMethods {
|
||||||
|
found := false
|
||||||
|
for _, allowed := range methods {
|
||||||
|
if allowed == method {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Errorf("Expected method %s to be allowed in API config", method)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiddlewareWrap(t *testing.T) {
|
||||||
|
config := DefaultSecurityConfig()
|
||||||
|
middleware := NewSecurityHeadersMiddleware(config)
|
||||||
|
|
||||||
|
// Create a simple handler
|
||||||
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte("OK"))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Wrap with security middleware
|
||||||
|
wrappedHandler := middleware.Wrap(handler)
|
||||||
|
|
||||||
|
req := httptest.NewRequest("GET", "https://example.com/test", nil)
|
||||||
|
req.TLS = &tls.ConnectionState{}
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
|
wrappedHandler.ServeHTTP(rr, req)
|
||||||
|
|
||||||
|
// Check response
|
||||||
|
if rr.Code != http.StatusOK {
|
||||||
|
t.Errorf("Expected status 200, got %d", rr.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
if rr.Body.String() != "OK" {
|
||||||
|
t.Errorf("Expected body 'OK', got %s", rr.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check security headers were applied
|
||||||
|
if rr.Header().Get("X-Frame-Options") == "" {
|
||||||
|
t.Error("Expected security headers to be applied by wrapper")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigValidation(t *testing.T) {
|
||||||
|
config := &SecurityHeadersConfig{
|
||||||
|
StrictTransportSecurityMaxAge: -1,
|
||||||
|
CORSMaxAge: -1,
|
||||||
|
FrameOptions: "INVALID",
|
||||||
|
}
|
||||||
|
|
||||||
|
err := config.Validate()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Unexpected validation error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should fix invalid values
|
||||||
|
if config.StrictTransportSecurityMaxAge != 0 {
|
||||||
|
t.Error("Expected negative HSTS max age to be reset to 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.CORSMaxAge != 0 {
|
||||||
|
t.Error("Expected negative CORS max age to be reset to 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.FrameOptions != "DENY" {
|
||||||
|
t.Error("Expected invalid frame options to be reset to DENY")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkSecurityHeadersApply(b *testing.B) {
|
||||||
|
config := DefaultSecurityConfig()
|
||||||
|
middleware := NewSecurityHeadersMiddleware(config)
|
||||||
|
|
||||||
|
req := httptest.NewRequest("GET", "https://example.com/test", nil)
|
||||||
|
req.TLS = &tls.ConnectionState{}
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
b.RunParallel(func(pb *testing.PB) {
|
||||||
|
for pb.Next() {
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
middleware.Apply(rr, req)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -404,6 +404,7 @@ func NewWithContext(ctx context.Context, config *Config, next http.Handler, name
|
|||||||
ctx: pluginCtx,
|
ctx: pluginCtx,
|
||||||
cancelFunc: cancelFunc,
|
cancelFunc: cancelFunc,
|
||||||
suppressDiagnosticLogs: isTestMode(),
|
suppressDiagnosticLogs: isTestMode(),
|
||||||
|
securityHeadersApplier: config.GetSecurityHeadersApplier(),
|
||||||
}
|
}
|
||||||
|
|
||||||
t.sessionManager, _ = NewSessionManager(config.SessionEncryptionKey, config.ForceHTTPS, config.CookieDomain, t.logger)
|
t.sessionManager, _ = NewSessionManager(config.SessionEncryptionKey, config.ForceHTTPS, config.CookieDomain, t.logger)
|
||||||
@@ -940,22 +941,15 @@ func (t *TraefikOidc) processAuthorizedRequest(rw http.ResponseWriter, req *http
|
|||||||
t.logger.Debug("Session not dirty, skipping save in processAuthorizedRequest")
|
t.logger.Debug("Session not dirty, skipping save in processAuthorizedRequest")
|
||||||
}
|
}
|
||||||
|
|
||||||
rw.Header().Set("X-Frame-Options", "DENY")
|
// Apply security headers if configured
|
||||||
rw.Header().Set("X-Content-Type-Options", "nosniff")
|
if t.securityHeadersApplier != nil {
|
||||||
rw.Header().Set("X-XSS-Protection", "1; mode=block")
|
t.securityHeadersApplier(rw, req)
|
||||||
rw.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
|
} else {
|
||||||
|
// Fallback to basic security headers
|
||||||
origin := req.Header.Get("Origin")
|
rw.Header().Set("X-Frame-Options", "DENY")
|
||||||
if origin != "" {
|
rw.Header().Set("X-Content-Type-Options", "nosniff")
|
||||||
rw.Header().Set("Access-Control-Allow-Origin", origin)
|
rw.Header().Set("X-XSS-Protection", "1; mode=block")
|
||||||
rw.Header().Set("Access-Control-Allow-Credentials", "true")
|
rw.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
|
||||||
rw.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
|
||||||
rw.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type")
|
|
||||||
|
|
||||||
if req.Method == "OPTIONS" {
|
|
||||||
rw.WriteHeader(http.StatusOK)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
t.logger.Debugf("Request authorized for user %s, forwarding to next handler", email)
|
t.logger.Debugf("Request authorized for user %s, forwarding to next handler", email)
|
||||||
|
|||||||
+208
-23
@@ -7,6 +7,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -26,29 +27,83 @@ type TemplatedHeader struct {
|
|||||||
// It provides all necessary settings to configure OpenID Connect authentication
|
// It provides all necessary settings to configure OpenID Connect authentication
|
||||||
// with various providers like Auth0, Logto, or any standard OIDC provider.
|
// with various providers like Auth0, Logto, or any standard OIDC provider.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
HTTPClient *http.Client `json:"-"`
|
HTTPClient *http.Client `json:"-"`
|
||||||
OIDCEndSessionURL string `json:"oidcEndSessionURL"`
|
OIDCEndSessionURL string `json:"oidcEndSessionURL"`
|
||||||
CookieDomain string `json:"cookieDomain"`
|
CookieDomain string `json:"cookieDomain"`
|
||||||
CallbackURL string `json:"callbackURL"`
|
CallbackURL string `json:"callbackURL"`
|
||||||
LogoutURL string `json:"logoutURL"`
|
LogoutURL string `json:"logoutURL"`
|
||||||
ClientID string `json:"clientID"`
|
ClientID string `json:"clientID"`
|
||||||
ClientSecret string `json:"clientSecret"`
|
ClientSecret string `json:"clientSecret"`
|
||||||
PostLogoutRedirectURI string `json:"postLogoutRedirectURI"`
|
PostLogoutRedirectURI string `json:"postLogoutRedirectURI"`
|
||||||
LogLevel string `json:"logLevel"`
|
LogLevel string `json:"logLevel"`
|
||||||
SessionEncryptionKey string `json:"sessionEncryptionKey"`
|
SessionEncryptionKey string `json:"sessionEncryptionKey"`
|
||||||
ProviderURL string `json:"providerURL"`
|
ProviderURL string `json:"providerURL"`
|
||||||
RevocationURL string `json:"revocationURL"`
|
RevocationURL string `json:"revocationURL"`
|
||||||
ExcludedURLs []string `json:"excludedURLs"`
|
ExcludedURLs []string `json:"excludedURLs"`
|
||||||
AllowedUserDomains []string `json:"allowedUserDomains"`
|
AllowedUserDomains []string `json:"allowedUserDomains"`
|
||||||
AllowedUsers []string `json:"allowedUsers"`
|
AllowedUsers []string `json:"allowedUsers"`
|
||||||
Scopes []string `json:"scopes"`
|
Scopes []string `json:"scopes"`
|
||||||
Headers []TemplatedHeader `json:"headers"`
|
Headers []TemplatedHeader `json:"headers"`
|
||||||
AllowedRolesAndGroups []string `json:"allowedRolesAndGroups"`
|
AllowedRolesAndGroups []string `json:"allowedRolesAndGroups"`
|
||||||
RateLimit int `json:"rateLimit"`
|
RateLimit int `json:"rateLimit"`
|
||||||
RefreshGracePeriodSeconds int `json:"refreshGracePeriodSeconds"`
|
RefreshGracePeriodSeconds int `json:"refreshGracePeriodSeconds"`
|
||||||
ForceHTTPS bool `json:"forceHTTPS"`
|
ForceHTTPS bool `json:"forceHTTPS"`
|
||||||
EnablePKCE bool `json:"enablePKCE"`
|
EnablePKCE bool `json:"enablePKCE"`
|
||||||
OverrideScopes bool `json:"overrideScopes"`
|
OverrideScopes bool `json:"overrideScopes"`
|
||||||
|
SecurityHeaders *SecurityHeadersConfig `json:"securityHeaders,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SecurityHeadersConfig configures security headers for the plugin
|
||||||
|
type SecurityHeadersConfig struct {
|
||||||
|
// Enable security headers (default: true)
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
|
||||||
|
// Security profile: "default", "strict", "development", "api", or "custom"
|
||||||
|
Profile string `json:"profile"`
|
||||||
|
|
||||||
|
// Content Security Policy
|
||||||
|
ContentSecurityPolicy string `json:"contentSecurityPolicy,omitempty"`
|
||||||
|
|
||||||
|
// HSTS settings
|
||||||
|
StrictTransportSecurity bool `json:"strictTransportSecurity"`
|
||||||
|
StrictTransportSecurityMaxAge int `json:"strictTransportSecurityMaxAge"` // seconds
|
||||||
|
StrictTransportSecuritySubdomains bool `json:"strictTransportSecuritySubdomains"`
|
||||||
|
StrictTransportSecurityPreload bool `json:"strictTransportSecurityPreload"`
|
||||||
|
|
||||||
|
// Frame options: "DENY", "SAMEORIGIN", or "ALLOW-FROM uri"
|
||||||
|
FrameOptions string `json:"frameOptions,omitempty"`
|
||||||
|
|
||||||
|
// Content type options (default: "nosniff")
|
||||||
|
ContentTypeOptions string `json:"contentTypeOptions,omitempty"`
|
||||||
|
|
||||||
|
// XSS protection (default: "1; mode=block")
|
||||||
|
XSSProtection string `json:"xssProtection,omitempty"`
|
||||||
|
|
||||||
|
// Referrer policy
|
||||||
|
ReferrerPolicy string `json:"referrerPolicy,omitempty"`
|
||||||
|
|
||||||
|
// Permissions policy
|
||||||
|
PermissionsPolicy string `json:"permissionsPolicy,omitempty"`
|
||||||
|
|
||||||
|
// Cross-origin settings
|
||||||
|
CrossOriginEmbedderPolicy string `json:"crossOriginEmbedderPolicy,omitempty"`
|
||||||
|
CrossOriginOpenerPolicy string `json:"crossOriginOpenerPolicy,omitempty"`
|
||||||
|
CrossOriginResourcePolicy string `json:"crossOriginResourcePolicy,omitempty"`
|
||||||
|
|
||||||
|
// CORS settings
|
||||||
|
CORSEnabled bool `json:"corsEnabled"`
|
||||||
|
CORSAllowedOrigins []string `json:"corsAllowedOrigins,omitempty"`
|
||||||
|
CORSAllowedMethods []string `json:"corsAllowedMethods,omitempty"`
|
||||||
|
CORSAllowedHeaders []string `json:"corsAllowedHeaders,omitempty"`
|
||||||
|
CORSAllowCredentials bool `json:"corsAllowCredentials"`
|
||||||
|
CORSMaxAge int `json:"corsMaxAge"` // seconds
|
||||||
|
|
||||||
|
// Custom headers (in addition to standard security headers)
|
||||||
|
CustomHeaders map[string]string `json:"customHeaders,omitempty"`
|
||||||
|
|
||||||
|
// Security features
|
||||||
|
DisableServerHeader bool `json:"disableServerHeader"`
|
||||||
|
DisablePoweredByHeader bool `json:"disablePoweredByHeader"`
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -91,11 +146,42 @@ func CreateConfig() *Config {
|
|||||||
EnablePKCE: false, // PKCE is opt-in
|
EnablePKCE: false, // PKCE is opt-in
|
||||||
OverrideScopes: false, // Default to appending scopes, not overriding
|
OverrideScopes: false, // Default to appending scopes, not overriding
|
||||||
RefreshGracePeriodSeconds: 60, // Default grace period of 60 seconds
|
RefreshGracePeriodSeconds: 60, // Default grace period of 60 seconds
|
||||||
|
SecurityHeaders: createDefaultSecurityConfig(),
|
||||||
}
|
}
|
||||||
|
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// createDefaultSecurityConfig creates a default security headers configuration
|
||||||
|
func createDefaultSecurityConfig() *SecurityHeadersConfig {
|
||||||
|
return &SecurityHeadersConfig{
|
||||||
|
Enabled: true,
|
||||||
|
Profile: "default",
|
||||||
|
|
||||||
|
// Default security headers
|
||||||
|
StrictTransportSecurity: true,
|
||||||
|
StrictTransportSecurityMaxAge: 31536000, // 1 year
|
||||||
|
StrictTransportSecuritySubdomains: true,
|
||||||
|
StrictTransportSecurityPreload: true,
|
||||||
|
|
||||||
|
FrameOptions: "DENY",
|
||||||
|
ContentTypeOptions: "nosniff",
|
||||||
|
XSSProtection: "1; mode=block",
|
||||||
|
ReferrerPolicy: "strict-origin-when-cross-origin",
|
||||||
|
|
||||||
|
// CORS disabled by default
|
||||||
|
CORSEnabled: false,
|
||||||
|
CORSAllowedMethods: []string{"GET", "POST", "OPTIONS"},
|
||||||
|
CORSAllowedHeaders: []string{"Authorization", "Content-Type"},
|
||||||
|
CORSAllowCredentials: false,
|
||||||
|
CORSMaxAge: 86400, // 24 hours
|
||||||
|
|
||||||
|
// Security features
|
||||||
|
DisableServerHeader: true,
|
||||||
|
DisablePoweredByHeader: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Validate checks the configuration settings for validity.
|
// Validate checks the configuration settings for validity.
|
||||||
// It ensures that required fields (ProviderURL, CallbackURL, ClientID, ClientSecret, SessionEncryptionKey)
|
// It ensures that required fields (ProviderURL, CallbackURL, ClientID, ClientSecret, SessionEncryptionKey)
|
||||||
// are present and that URLs are well-formed (HTTPS where required). It also validates
|
// are present and that URLs are well-formed (HTTPS where required). It also validates
|
||||||
@@ -580,3 +666,102 @@ func handleError(w http.ResponseWriter, message string, code int, logger *Logger
|
|||||||
logger.Error("%s", message)
|
logger.Error("%s", message)
|
||||||
http.Error(w, message, code)
|
http.Error(w, message, code)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetSecurityHeadersApplier returns a function that applies security headers
|
||||||
|
func (c *Config) GetSecurityHeadersApplier() func(http.ResponseWriter, *http.Request) {
|
||||||
|
if c.SecurityHeaders == nil || !c.SecurityHeaders.Enabled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
headers := rw.Header()
|
||||||
|
|
||||||
|
// Apply basic security headers based on configuration
|
||||||
|
if c.SecurityHeaders.FrameOptions != "" {
|
||||||
|
headers.Set("X-Frame-Options", c.SecurityHeaders.FrameOptions)
|
||||||
|
}
|
||||||
|
if c.SecurityHeaders.ContentTypeOptions != "" {
|
||||||
|
headers.Set("X-Content-Type-Options", c.SecurityHeaders.ContentTypeOptions)
|
||||||
|
}
|
||||||
|
if c.SecurityHeaders.XSSProtection != "" {
|
||||||
|
headers.Set("X-XSS-Protection", c.SecurityHeaders.XSSProtection)
|
||||||
|
}
|
||||||
|
if c.SecurityHeaders.ReferrerPolicy != "" {
|
||||||
|
headers.Set("Referrer-Policy", c.SecurityHeaders.ReferrerPolicy)
|
||||||
|
}
|
||||||
|
if c.SecurityHeaders.ContentSecurityPolicy != "" {
|
||||||
|
headers.Set("Content-Security-Policy", c.SecurityHeaders.ContentSecurityPolicy)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HSTS for HTTPS
|
||||||
|
if (req.TLS != nil || req.Header.Get("X-Forwarded-Proto") == "https") && c.SecurityHeaders.StrictTransportSecurity {
|
||||||
|
hstsValue := fmt.Sprintf("max-age=%d", c.SecurityHeaders.StrictTransportSecurityMaxAge)
|
||||||
|
if c.SecurityHeaders.StrictTransportSecuritySubdomains {
|
||||||
|
hstsValue += "; includeSubDomains"
|
||||||
|
}
|
||||||
|
if c.SecurityHeaders.StrictTransportSecurityPreload {
|
||||||
|
hstsValue += "; preload"
|
||||||
|
}
|
||||||
|
headers.Set("Strict-Transport-Security", hstsValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CORS headers
|
||||||
|
if c.SecurityHeaders.CORSEnabled {
|
||||||
|
origin := req.Header.Get("Origin")
|
||||||
|
if origin != "" && isOriginAllowed(origin, c.SecurityHeaders.CORSAllowedOrigins) {
|
||||||
|
headers.Set("Access-Control-Allow-Origin", origin)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(c.SecurityHeaders.CORSAllowedMethods) > 0 {
|
||||||
|
headers.Set("Access-Control-Allow-Methods", strings.Join(c.SecurityHeaders.CORSAllowedMethods, ", "))
|
||||||
|
}
|
||||||
|
if len(c.SecurityHeaders.CORSAllowedHeaders) > 0 {
|
||||||
|
headers.Set("Access-Control-Allow-Headers", strings.Join(c.SecurityHeaders.CORSAllowedHeaders, ", "))
|
||||||
|
}
|
||||||
|
if c.SecurityHeaders.CORSAllowCredentials {
|
||||||
|
headers.Set("Access-Control-Allow-Credentials", "true")
|
||||||
|
}
|
||||||
|
if c.SecurityHeaders.CORSMaxAge > 0 {
|
||||||
|
headers.Set("Access-Control-Max-Age", strconv.Itoa(c.SecurityHeaders.CORSMaxAge))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom headers
|
||||||
|
for name, value := range c.SecurityHeaders.CustomHeaders {
|
||||||
|
headers.Set(name, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove server headers
|
||||||
|
if c.SecurityHeaders.DisableServerHeader {
|
||||||
|
headers.Del("Server")
|
||||||
|
}
|
||||||
|
if c.SecurityHeaders.DisablePoweredByHeader {
|
||||||
|
headers.Del("X-Powered-By")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// isOriginAllowed checks if an origin is in the allowed list
|
||||||
|
func isOriginAllowed(origin string, allowedOrigins []string) bool {
|
||||||
|
for _, allowed := range allowedOrigins {
|
||||||
|
if origin == allowed || allowed == "*" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// Simple wildcard matching for subdomains
|
||||||
|
if strings.Contains(allowed, "*") {
|
||||||
|
if strings.HasPrefix(allowed, "https://*.") {
|
||||||
|
domain := strings.TrimPrefix(allowed, "https://*.")
|
||||||
|
if strings.HasSuffix(origin, "."+domain) || origin == "https://"+domain {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(allowed, "http://*.") {
|
||||||
|
domain := strings.TrimPrefix(allowed, "http://*.")
|
||||||
|
if strings.HasSuffix(origin, "."+domain) || origin == "http://"+domain {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,15 +13,15 @@ import (
|
|||||||
|
|
||||||
// CacheInterface defines the common cache operations
|
// CacheInterface defines the common cache operations
|
||||||
type CacheInterface interface {
|
type CacheInterface interface {
|
||||||
Set(key string, value interface{}, ttl time.Duration)
|
Set(key string, value any, ttl time.Duration)
|
||||||
Get(key string) (interface{}, bool)
|
Get(key string) (any, bool)
|
||||||
Delete(key string)
|
Delete(key string)
|
||||||
SetMaxSize(size int)
|
SetMaxSize(size int)
|
||||||
Size() int
|
Size() int
|
||||||
Clear()
|
Clear()
|
||||||
Cleanup()
|
Cleanup()
|
||||||
Close()
|
Close()
|
||||||
GetStats() map[string]interface{} // For testing and monitoring
|
GetStats() map[string]any // For testing and monitoring
|
||||||
}
|
}
|
||||||
|
|
||||||
// TokenVerifier interface defines token verification capabilities.
|
// TokenVerifier interface defines token verification capabilities.
|
||||||
@@ -75,7 +75,7 @@ type TraefikOidc struct {
|
|||||||
sessionManager *SessionManager
|
sessionManager *SessionManager
|
||||||
tokenCleanupStopChan chan struct{}
|
tokenCleanupStopChan chan struct{}
|
||||||
excludedURLs map[string]struct{}
|
excludedURLs map[string]struct{}
|
||||||
extractClaimsFunc func(tokenString string) (map[string]interface{}, error)
|
extractClaimsFunc func(tokenString string) (map[string]any, error)
|
||||||
initiateAuthenticationFunc func(rw http.ResponseWriter, req *http.Request, session *SessionData, redirectURL string)
|
initiateAuthenticationFunc func(rw http.ResponseWriter, req *http.Request, session *SessionData, redirectURL string)
|
||||||
metadataCache *MetadataCache
|
metadataCache *MetadataCache
|
||||||
allowedRolesAndGroups map[string]struct{}
|
allowedRolesAndGroups map[string]struct{}
|
||||||
@@ -114,4 +114,5 @@ type TraefikOidc struct {
|
|||||||
suppressDiagnosticLogs bool
|
suppressDiagnosticLogs bool
|
||||||
firstRequestReceived bool
|
firstRequestReceived bool
|
||||||
metadataRefreshStarted bool
|
metadataRefreshStarted bool
|
||||||
|
securityHeadersApplier func(http.ResponseWriter, *http.Request)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user