Cleanup [dec2025] (#101)

* Cleanup excessive comments.

* Remove leftovers hanging around from previous refactor

* Improve test coverage
This commit is contained in:
2025-12-09 01:38:02 +00:00
committed by GitHub
parent 9126c74723
commit c474bbafd6
172 changed files with 25141 additions and 52206 deletions
+456
View File
@@ -0,0 +1,456 @@
# Configuration Reference
Complete reference for all Traefik OIDC middleware configuration options.
## Table of Contents
- [Required Parameters](#required-parameters)
- [Optional Parameters](#optional-parameters)
- [Security Options](#security-options)
- [Session Management](#session-management)
- [Access Control](#access-control)
- [Headers Configuration](#headers-configuration)
- [Security Headers](#security-headers)
- [Scope Configuration](#scope-configuration)
- [Advanced Options](#advanced-options)
---
## Required Parameters
| Parameter | Type | Description | Example |
|-----------|------|-------------|---------|
| `providerURL` | string | Base URL of the OIDC provider | `https://accounts.google.com` |
| `clientID` | string | OAuth 2.0 client identifier | `1234567890.apps.googleusercontent.com` |
| `clientSecret` | string | OAuth 2.0 client secret | `your-client-secret` |
| `sessionEncryptionKey` | string | Key for encrypting session data (min 32 bytes) | `your-32-byte-encryption-key-here` |
| `callbackURL` | string | Path where provider redirects after authentication | `/oauth2/callback` |
### Basic Configuration Example
```yaml
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: oidc-auth
spec:
plugin:
traefikoidc:
providerURL: https://accounts.google.com
clientID: your-client-id.apps.googleusercontent.com
clientSecret: your-client-secret
sessionEncryptionKey: your-32-byte-encryption-key-here
callbackURL: /oauth2/callback
```
---
## Optional Parameters
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `logoutURL` | string | `callbackURL + "/logout"` | Path for logout requests |
| `postLogoutRedirectURI` | string | `/` | Redirect URL after logout |
| `logLevel` | string | `info` | Logging verbosity (`debug`, `info`, `error`) |
| `forceHTTPS` | bool | `false` | Force HTTPS for redirect URIs |
| `rateLimit` | int | `100` | Maximum requests per second |
| `excludedURLs` | []string | none | Paths that bypass authentication |
| `revocationURL` | string | auto-discovered | Token revocation endpoint |
| `oidcEndSessionURL` | string | auto-discovered | Provider's end session endpoint |
| `enablePKCE` | bool | `false` | Enable PKCE for authorization code flow |
| `minimalHeaders` | bool | `false` | Reduce forwarded headers |
### TLS Termination at Load Balancer
If running Traefik behind a load balancer (AWS ALB, Google Cloud LB, Azure App Gateway) that terminates TLS:
```yaml
forceHTTPS: true # Required for correct redirect URIs
```
Without this setting, redirect URIs will use `http://` instead of `https://`, causing OAuth callback failures.
---
## Security Options
### Audience Validation
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `audience` | string | `clientID` | Expected audience for access token validation |
| `strictAudienceValidation` | bool | `false` | Reject sessions with audience mismatch |
| `allowOpaqueTokens` | bool | `false` | Enable opaque token support via RFC 7662 |
| `requireTokenIntrospection` | bool | `false` | Require introspection for opaque tokens |
#### Production Security Configuration
```yaml
audience: "https://my-api.example.com"
strictAudienceValidation: true
```
#### Opaque Token Support
```yaml
allowOpaqueTokens: true
requireTokenIntrospection: true
strictAudienceValidation: true
```
### Other Security Options
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `disableReplayDetection` | bool | `false` | Disable JTI-based replay attack detection |
| `allowPrivateIPAddresses` | bool | `false` | Allow private IPs in provider URLs |
---
## Session Management
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `sessionMaxAge` | int | `86400` (24h) | Maximum session age in seconds |
| `refreshGracePeriodSeconds` | int | `60` | Seconds before expiry to attempt refresh |
| `cookieDomain` | string | auto-detected | Domain for session cookies |
| `cookiePrefix` | string | `_oidc_raczylo_` | Prefix for cookie names |
### Multi-Subdomain Setup
```yaml
cookieDomain: .example.com # Share cookies across subdomains
```
### Multiple Middleware Instances
When running multiple middleware instances with different authorization requirements, use unique prefixes:
```yaml
# User authentication middleware
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: oidc-userauth
spec:
plugin:
traefikoidc:
cookiePrefix: "_oidc_userauth_"
sessionEncryptionKey: user-encryption-key-min-32-bytes
# ... other config
---
# Admin authentication middleware
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: oidc-adminauth
spec:
plugin:
traefikoidc:
cookiePrefix: "_oidc_adminauth_"
sessionEncryptionKey: admin-encryption-key-min-32-bytes
allowedUsers:
- admin@example.com
# ... other config
```
### Extended Session Duration
```yaml
sessionMaxAge: 604800 # 7 days
# Common values:
# 3600 - 1 hour (high security)
# 86400 - 1 day (default)
# 259200 - 3 days
# 604800 - 7 days
# 2592000 - 30 days
```
---
## Access Control
### User Restrictions
| Parameter | Type | Description |
|-----------|------|-------------|
| `allowedUserDomains` | []string | Restrict to specific email domains |
| `allowedUsers` | []string | Specific email addresses allowed |
| `allowedRolesAndGroups` | []string | Required roles or groups |
| `roleClaimName` | string | JWT claim for roles (default: `roles`) |
| `groupClaimName` | string | JWT claim for groups (default: `groups`) |
| `userIdentifierClaim` | string | Claim for user ID (default: `email`) |
### Domain Restriction
```yaml
allowedUserDomains:
- company.com
- subsidiary.com
```
### Specific User Access
```yaml
allowedUsers:
- user@example.com
- contractor@external.org
```
### Role-Based Access Control
```yaml
allowedRolesAndGroups:
- admin
- developer
roleClaimName: "https://myapp.com/roles" # For namespaced claims (Auth0)
```
### Access Control Logic
- If only `allowedUsers` is set: Only specified emails can access
- If only `allowedUserDomains` is set: Only specified domains can access
- If both are set: Access granted if email is in `allowedUsers` OR domain is in `allowedUserDomains`
- If neither is set: Any authenticated user can access
### Users Without Email (Azure AD)
For Azure AD service accounts or users without email:
```yaml
userIdentifierClaim: sub # Options: sub, oid, upn, preferred_username
allowedUsers:
- "abc12345-6789-0abc-def0-123456789abc" # User object ID
```
---
## Headers Configuration
### Default Headers
The middleware sets these headers for downstream services:
| Header | Description |
|--------|-------------|
| `X-Forwarded-User` | User's email address |
| `X-User-Groups` | Comma-separated user groups |
| `X-User-Roles` | Comma-separated user roles |
| `X-Auth-Request-Redirect` | Original request URI |
| `X-Auth-Request-User` | User's email address |
| `X-Auth-Request-Token` | User's ID token |
### Minimal Headers Mode
For "431 Request Header Fields Too Large" errors:
```yaml
minimalHeaders: true # Only forwards X-Forwarded-User
```
### Custom Templated Headers
```yaml
headers:
- name: "X-User-Email"
value: "{{{{.Claims.email}}}}"
- name: "X-User-ID"
value: "{{{{.Claims.sub}}}}"
- name: "Authorization"
value: "Bearer {{{{.AccessToken}}}}"
- name: "X-User-Roles"
value: "{{{{range $i, $e := .Claims.roles}}}}{{{{if $i}}}},{{{{end}}}}{{{{$e}}}}{{{{end}}}}"
```
**Template Variables:**
- `{{.Claims.field}}` - ID token claims
- `{{.AccessToken}}` - Raw access token
- `{{.IdToken}}` - Raw ID token
- `{{.RefreshToken}}` - Raw refresh token
**Important:** Use double curly braces (`{{{{` and `}}}}`) to escape templates in YAML.
---
## Security Headers
### Security Profiles
| Profile | Use Case | Security Level |
|---------|----------|----------------|
| `default` | Standard web apps | High |
| `strict` | Maximum security | Very High |
| `development` | Local development | Medium |
| `api` | API endpoints | High |
| `custom` | Custom requirements | Configurable |
### Basic Configuration
```yaml
securityHeaders:
enabled: true
profile: "default"
```
### API with CORS
```yaml
securityHeaders:
enabled: true
profile: "api"
corsEnabled: true
corsAllowedOrigins:
- "https://your-frontend.com"
- "https://*.example.com"
corsAllowCredentials: true
```
### Custom Security Configuration
```yaml
securityHeaders:
enabled: true
profile: "custom"
# Content Security Policy
contentSecurityPolicy: "default-src 'self'; script-src 'self'"
# HSTS
strictTransportSecurity: true
strictTransportSecurityMaxAge: 31536000
strictTransportSecuritySubdomains: true
strictTransportSecurityPreload: true
# Frame and Content Protection
frameOptions: "DENY"
contentTypeOptions: "nosniff"
xssProtection: "1; mode=block"
referrerPolicy: "strict-origin-when-cross-origin"
# CORS
corsEnabled: true
corsAllowedOrigins: ["https://app.example.com"]
corsAllowedMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"]
corsAllowedHeaders: ["Authorization", "Content-Type"]
corsAllowCredentials: true
corsMaxAge: 86400
# Custom Headers
customHeaders:
X-Custom-Header: "value"
# Server Identification
disableServerHeader: true
disablePoweredByHeader: true
```
### CORS Origin Patterns
```yaml
corsAllowedOrigins:
- "https://example.com" # Exact match
- "https://*.example.com" # Subdomain wildcard
- "http://localhost:*" # Port wildcard (development)
```
---
## Scope Configuration
### Default Behavior (Append Mode)
```yaml
scopes:
- roles
- custom_scope
# Result: ["openid", "profile", "email", "roles", "custom_scope"]
```
### Override Mode
```yaml
overrideScopes: true
scopes:
- openid
- profile
- custom_scope
# Result: ["openid", "profile", "custom_scope"]
```
---
## Advanced Options
### Dynamic Client Registration (RFC 7591)
```yaml
dynamicClientRegistration:
enabled: true
initialAccessToken: "your-token" # Optional
persistCredentials: true
credentialsFile: "/tmp/oidc-credentials.json"
clientMetadata:
redirect_uris:
- "https://your-app.com/oauth2/callback"
client_name: "My Application"
application_type: "web"
grant_types:
- "authorization_code"
- "refresh_token"
```
### Multi-Replica Deployment
Without Redis, disable replay detection:
```yaml
disableReplayDetection: true
```
With Redis (recommended):
```yaml
redis:
enabled: true
address: "redis:6379"
cacheMode: "hybrid"
```
See [REDIS.md](REDIS.md) for complete Redis configuration.
---
## Kubernetes Secrets
Reference secrets instead of hardcoding sensitive values:
```yaml
providerURL: urn:k8s:secret:oidc-secret:ISSUER
clientID: urn:k8s:secret:oidc-secret:CLIENT_ID
clientSecret: urn:k8s:secret:oidc-secret:SECRET
```
Create the secret:
```bash
kubectl create secret generic oidc-secret \
--from-literal=ISSUER=https://accounts.google.com \
--from-literal=CLIENT_ID=your-client-id \
--from-literal=SECRET=your-client-secret \
-n traefik
```
---
## Environment Variable Naming
**Important:** Avoid using "API" as a substring in environment variable names when using `${VAR}` syntax in Traefik configuration. Traefik reserves `TRAEFIK_API_*` variables and the substring may cause conflicts.
```yaml
# Bad - may cause issues
sessionEncryptionKey: ${OIDC_SECRET_API}
# Good
sessionEncryptionKey: ${OIDC_SECRET_SVC}
```
+455
View File
@@ -0,0 +1,455 @@
# Development Guide
Guide for local development, testing, and contributing to the Traefik OIDC middleware.
## Table of Contents
- [Prerequisites](#prerequisites)
- [Local Development Setup](#local-development-setup)
- [Running Tests](#running-tests)
- [Test Categories](#test-categories)
- [CI/CD Pipeline](#cicd-pipeline)
- [Code Quality](#code-quality)
- [Contributing](#contributing)
---
## Prerequisites
- **Go 1.23+** for plugin compilation
- **Docker & Docker Compose** for local testing
- **OIDC Provider** credentials (Google, Azure, etc.)
### Required Development Tools
```bash
# golangci-lint (comprehensive linting)
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
# staticcheck (static analysis)
go install honnef.co/go/tools/cmd/staticcheck@latest
# gosec (security scanning)
go install github.com/securego/gosec/v2/cmd/gosec@latest
# govulncheck (vulnerability scanning)
go install golang.org/x/vuln/cmd/govulncheck@latest
```
---
## Local Development Setup
### Docker Compose Environment
The repository includes a Docker Compose setup for testing the plugin locally.
#### 1. Host Configuration
Add to `/etc/hosts`:
```bash
127.0.0.1 hello.localhost
127.0.0.1 traefik.localhost
```
#### 2. Plugin Configuration
The plugin is loaded using Traefik's **local plugins mode**:
- Plugin source: Parent directory (`../`)
- Mount path: `/plugins-local/src/github.com/lukaszraczylo/traefikoidc`
- Configuration: `experimental.localPlugins` in `traefik.yml`
#### 3. OIDC Provider Setup
Edit `docker/dynamic.yml` with your provider details:
**Google:**
```yaml
http:
middlewares:
oidc-auth:
plugin:
traefikoidc:
providerURL: "https://accounts.google.com"
clientID: "your-client-id.apps.googleusercontent.com"
clientSecret: "your-google-client-secret"
sessionEncryptionKey: "your-32-character-encryption-key"
callbackURL: "/oauth2/callback"
logoutURL: "/oauth2/logout"
scopes:
- "openid"
- "email"
- "profile"
```
**Azure AD:**
```yaml
http:
middlewares:
oidc-auth:
plugin:
traefikoidc:
providerURL: "https://login.microsoftonline.com/your-tenant-id/v2.0"
clientID: "your-azure-client-id"
clientSecret: "your-azure-client-secret"
sessionEncryptionKey: "your-32-character-encryption-key"
callbackURL: "/oauth2/callback"
scopes:
- "openid"
- "email"
- "profile"
```
#### 4. Start Environment
```bash
cd docker
docker-compose up -d
```
#### 5. Test Plugin
- **Protected App**: http://hello.localhost (redirects to OIDC)
- **Traefik Dashboard**: http://traefik.localhost:8080
### Development Workflow
1. **Edit plugin code** in the project root
2. **Build and test** (optional syntax check):
```bash
go mod tidy
go build .
go test ./...
```
3. **Restart Traefik** to reload plugin:
```bash
docker-compose restart traefik
```
4. **Test changes** at http://hello.localhost
### Debugging
**View plugin logs:**
```bash
docker-compose logs -f traefik | grep traefikoidc
```
**Check plugin loading:**
```bash
docker-compose logs traefik | grep -i plugin
```
**Verify plugin directory:**
```bash
docker-compose exec traefik ls -la /plugins-local/src/github.com/lukaszraczylo/traefikoidc/
```
---
## Running Tests
### Quick Start
```bash
# Fast development testing (< 30 seconds)
go test ./... -short
# Standard tests with race detector
go test -race -timeout=15m ./...
# With coverage report
go test -coverprofile=coverage.out ./...
go tool cover -func=coverage.out
```
### Test Modes
| Mode | Command | Duration | Use Case |
|------|---------|----------|----------|
| Quick | `go test ./... -short` | < 30s | During development |
| Extended | `RUN_EXTENDED_TESTS=1 go test ./...` | 2-5 min | Before commits |
| Long | `RUN_LONG_TESTS=1 go test ./...` | 5-15 min | Release validation |
| Stress | `RUN_STRESS_TESTS=1 go test ./...` | 10-30 min | Performance testing |
### Environment Variables
```bash
# Enable specific test types
export RUN_EXTENDED_TESTS=1
export RUN_LONG_TESTS=1
export RUN_STRESS_TESTS=1
# Disable specific features
export DISABLE_LEAK_DETECTION=1
# Customize test parameters
export TEST_MAX_CONCURRENCY=10
export TEST_MAX_ITERATIONS=50
export TEST_MEMORY_THRESHOLD_MB=25.5
```
---
## Test Categories
### Quick Tests (Default)
- Basic functionality verification
- Limited iterations (1-3)
- Small data sets
- Essential memory leak checks
**Configuration:**
- Max Iterations: 3
- Max Concurrency: 5
- Memory Threshold: 2.0 MB
- Timeout: 10 seconds
### Extended Tests
- Comprehensive testing before commits
- More iterations (5-10)
- Enhanced memory leak detection
**Configuration:**
- Max Iterations: 10
- Max Concurrency: 20
- Memory Threshold: 10.0 MB
- Timeout: 30 seconds
### Long Tests
- Performance validation
- High iteration counts (50-100)
- Large data sets
**Configuration:**
- Max Iterations: 100
- Max Concurrency: 50
- Memory Threshold: 50.0 MB
- Timeout: 60 seconds
### Stress Tests
- Maximum load testing
- Edge case validation
- Extreme parameters
**Configuration:**
- Max Iterations: 500
- Max Concurrency: 100
- Memory Threshold: 100.0 MB
- Timeout: 120 seconds
### Running Specific Test Suites
```bash
# Memory leak tests
go test -v -run='.*Leak.*' ./...
# Integration tests
go test -v -run='.*Integration.*' ./...
# Regression tests
go test -v -run='.*Regression.*' ./...
# Provider-specific tests
go test -v -run='.*Azure.*' ./...
go test -v -run='.*Google.*' ./...
```
### Benchmarks
```bash
# Quick benchmarks
go test -bench=. -short
# Extended benchmarks
RUN_EXTENDED_TESTS=1 go test -bench=.
# Memory profiling
go test -bench=. -memprofile=mem.prof
go tool pprof mem.prof
```
---
## CI/CD Pipeline
The repository uses GitHub Actions for comprehensive validation with 20+ parallel checks.
### Triggered On
- Pull requests to `main` branch
- Pushes to `main` branch
### Parallel Jobs
#### Code Quality (3 checks)
- **Format & Basic Checks** - gofmt, go vet, go mod
- **golangci-lint** - 30+ linters
- **Staticcheck** - Advanced static analysis
#### Security (3 checks)
- **Gosec** - Security vulnerability scanning
- **Govulncheck** - Go vulnerability database
- **CodeQL** - GitHub's semantic code analysis
#### Testing (9 suites)
- Race Detector
- Coverage (75% threshold)
- Memory Leaks
- Integration Tests
- Regression Tests
- Security Edge Cases
- Session Tests
- Token Tests
- CSRF Tests
#### Provider Testing (9 providers)
Tests run in parallel for:
- Google
- Azure AD
- Auth0
- Okta
- Keycloak
- AWS Cognito
- GitLab
- GitHub
- Generic OIDC
#### Performance & Build (3 checks)
- Benchmarks
- Multi-platform Build (linux/darwin x amd64/arm64)
- Go Version Compatibility (Go 1.23 & 1.24)
### Quality Gates
All PRs must pass:
- All parallel checks
- 75% test coverage minimum
- Zero security vulnerabilities
- No race conditions
- No memory leaks
- All providers tested
- Builds on all platforms
---
## Code Quality
### Pre-Commit Checklist
```bash
# Run before every commit
gofmt -s -w . && \
go mod tidy && \
golangci-lint run && \
go test -race -short ./... && \
echo "Ready to commit!"
```
### Local Validation
```bash
# Format code
gofmt -s -w .
# Run linter
golangci-lint run
# Static analysis
staticcheck ./...
# Security scan
gosec ./...
# Vulnerability check
govulncheck ./...
# Tests with race detector
go test -race -timeout=15m -count=1 ./...
# Coverage report
go test -coverprofile=coverage.out ./...
go tool cover -func=coverage.out
# View coverage in browser
go tool cover -html=coverage.out
```
### Troubleshooting
**Coverage Below Threshold:**
```bash
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out # See uncovered lines
```
**Race Condition Found:**
```bash
go test -race -v -run=TestName ./...
```
**Linter Errors:**
```bash
golangci-lint run -v
golangci-lint run --fix # Auto-fix some issues
```
**Provider Test Fails:**
```bash
go test -v -run='.*Azure.*' ./...
```
---
## Contributing
### Development Guidelines
1. **Memory Management**: Ensure all goroutines can be cancelled and resources are bounded
2. **Testing**: Add tests for new features, including memory leak tests where appropriate
3. **Race Conditions**: Run tests with `-race` flag to detect race conditions
4. **Documentation**: Update README and configuration files for new options
### Pull Request Template
PRs should include:
- Description of changes
- Type of change (bug fix, feature, breaking change, etc.)
- Related issues
- Provider impact (which providers are affected)
- Testing performed
- Security considerations
- Performance impact
- Breaking changes (if any)
### Checklist
Before submitting:
- [ ] Code follows project style
- [ ] Self-review completed
- [ ] Tests added for new functionality
- [ ] All tests pass locally
- [ ] Documentation updated
- [ ] No new warnings generated
### Code Owners
The repository uses CODEOWNERS for automatic PR reviewer assignment based on file paths.
### Dependabot
Automated dependency updates run weekly (Mondays 9 AM) with security updates prioritized.
---
## Additional Resources
- [golangci-lint Rules](.golangci.yml)
- [PR Template](.github/PULL_REQUEST_TEMPLATE.md)
- [Workflow Documentation](.github/workflows/README.md)
- [GitHub Actions Documentation](https://docs.github.com/en/actions)
+580
View File
@@ -0,0 +1,580 @@
# OIDC Provider Configuration Guide
Configuration reference for each supported OIDC provider.
## Table of Contents
- [Provider Support Matrix](#provider-support-matrix)
- [Google](#google)
- [Microsoft Azure AD](#microsoft-azure-ad)
- [Auth0](#auth0)
- [Okta](#okta)
- [Keycloak](#keycloak)
- [AWS Cognito](#aws-cognito)
- [GitLab](#gitlab)
- [GitHub](#github)
- [Generic OIDC](#generic-oidc)
- [Automatic Scope Filtering](#automatic-scope-filtering)
---
## Provider Support Matrix
| Provider | OIDC Support | Refresh Tokens | Auto-Detection | ID Tokens |
|----------|-------------|----------------|----------------|-----------|
| Google | Full | Yes | `accounts.google.com` | Yes |
| Azure AD | Full | Yes | `login.microsoftonline.com` | Yes |
| Auth0 | Full | Yes | `*.auth0.com` | Yes |
| Okta | Full | Yes | `*.okta.com` | Yes |
| Keycloak | Full | Yes | `/auth/realms/` path | Yes |
| AWS Cognito | Full | Yes | `cognito-idp.*.amazonaws.com` | Yes |
| GitLab | Full | Yes | `gitlab.com` | Yes |
| GitHub | OAuth 2.0 Only | No | `github.com` | No |
| Generic | Full | Yes | Any OIDC endpoint | Yes |
---
## Google
### Provider URL
```yaml
providerURL: "https://accounts.google.com"
```
### Configuration
```yaml
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: oidc-google
spec:
plugin:
traefikoidc:
providerURL: "https://accounts.google.com"
clientID: "your-id.apps.googleusercontent.com"
clientSecret: "your-client-secret"
callbackURL: "/oauth2/callback"
sessionEncryptionKey: "your-32-char-encryption-key-here"
scopes:
- openid
- email
- profile
allowedUserDomains:
- "your-gsuite-domain.com" # Optional: Workspace restriction
forceHttps: true
enablePkce: true
```
### Google-Specific Features
- **Automatic offline access**: Middleware adds `access_type=offline` and `prompt=consent`
- **Scope filtering**: Automatically removes unsupported `offline_access` scope
- **Workspace domains**: Restrict to specific Google Workspace domains via `hd` claim
### Google Cloud Console Setup
1. Go to [Google Cloud Console](https://console.cloud.google.com/)
2. Create or select a project
3. Navigate to APIs & Services > Credentials
4. Create OAuth 2.0 Client ID (Web application)
5. Add authorized redirect URI: `https://your-domain.com/oauth2/callback`
6. Configure OAuth consent screen (must be "Published" for production)
---
## Microsoft Azure AD
### Provider URL
```yaml
# Single tenant
providerURL: "https://login.microsoftonline.com/{tenant-id}/v2.0"
# Multi-tenant
providerURL: "https://login.microsoftonline.com/common/v2.0"
```
### Basic Configuration
```yaml
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: oidc-azure
spec:
plugin:
traefikoidc:
providerURL: "https://login.microsoftonline.com/common/v2.0"
clientID: "your-azure-client-id"
clientSecret: "your-azure-client-secret"
callbackURL: "/oauth2/callback"
sessionEncryptionKey: "your-32-char-encryption-key-here"
scopes:
- openid
- profile
- email
- offline_access
allowedRolesAndGroups:
- "App.Users"
- "Admin.Group"
forceHttps: true
```
### With Application ID URI (API Access)
```yaml
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: oidc-azure-api
spec:
plugin:
traefikoidc:
providerURL: "https://login.microsoftonline.com/common/v2.0"
clientID: "your-azure-client-id"
clientSecret: "your-azure-client-secret"
audience: "api://your-azure-client-id" # Application ID URI
callbackURL: "/oauth2/callback"
sessionEncryptionKey: "your-32-char-encryption-key-here"
forceHttps: true
```
### Users Without Email
```yaml
userIdentifierClaim: sub # Options: sub, oid, upn, preferred_username
allowedUsers:
- "user-object-id-1"
- "user-object-id-2"
```
### Azure AD 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/oauth2/callback`
5. Create client secret in Certificates & secrets
6. Configure Token Configuration for group claims
---
## Auth0
### Provider URL
```yaml
providerURL: "https://your-domain.auth0.com"
```
### Basic Configuration
```yaml
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: oidc-auth0
spec:
plugin:
traefikoidc:
providerURL: "https://your-domain.auth0.com"
clientID: "your-auth0-client-id"
clientSecret: "your-auth0-client-secret"
callbackURL: "/oauth2/callback"
sessionEncryptionKey: "your-32-char-encryption-key-here"
scopes:
- openid
- profile
- email
- offline_access
postLogoutRedirectUri: "https://your-app.com"
forceHttps: true
enablePkce: true
```
### With Custom API Audience
```yaml
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: oidc-auth0-api
spec:
plugin:
traefikoidc:
providerURL: "https://your-domain.auth0.com"
clientID: "your-auth0-client-id"
clientSecret: "your-auth0-client-secret"
audience: "https://api.your-domain.com" # API identifier
strictAudienceValidation: true
callbackURL: "/oauth2/callback"
sessionEncryptionKey: "your-32-char-encryption-key-here"
roleClaimName: "https://your-app.com/roles" # Namespaced claim
groupClaimName: "https://your-app.com/groups"
allowedRolesAndGroups:
- admin
- editor
```
### Auth0 Action for Custom Claims
```javascript
exports.onExecutePostLogin = async (event, api) => {
const namespace = 'https://your-app.com/';
if (event.authorization) {
api.idToken.setCustomClaim(namespace + 'roles', event.authorization.roles);
api.idToken.setCustomClaim('email', event.user.email);
}
};
```
### Auth0 Setup
1. Go to [Auth0 Dashboard](https://manage.auth0.com/)
2. Create Regular Web Application
3. Configure Allowed Callback URLs: `https://your-domain.com/oauth2/callback`
4. Configure Allowed Logout URLs: `https://your-domain.com/oauth2/logout`
5. Enable OIDC Conformant in Advanced Settings
6. Create API in APIs section for custom audiences
See [AUTH0_AUDIENCE_GUIDE.md](AUTH0_AUDIENCE_GUIDE.md) for detailed audience configuration.
---
## Okta
### Provider URL
```yaml
providerURL: "https://your-domain.okta.com"
# Or with custom authorization server:
providerURL: "https://your-domain.okta.com/oauth2/default"
```
### Configuration
```yaml
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: oidc-okta
spec:
plugin:
traefikoidc:
providerURL: "https://your-domain.okta.com"
clientID: "your-okta-client-id"
clientSecret: "your-okta-client-secret"
callbackURL: "/oauth2/callback"
sessionEncryptionKey: "your-32-char-encryption-key-here"
scopes:
- openid
- profile
- email
- groups
- offline_access
allowedRolesAndGroups:
- admin
- "Everyone"
forceHttps: true
enablePkce: true
```
### Okta Setup
1. Access Okta Admin Console
2. Go to Applications > Create App Integration
3. Select OIDC - OpenID Connect > Web Application
4. Configure Sign-in redirect URIs: `https://your-domain.com/oauth2/callback`
5. Configure Sign-out redirect URIs: `https://your-domain.com/oauth2/logout`
6. Enable Authorization Code and Refresh Token grant types
7. Configure Groups claim in authorization server
---
## Keycloak
### Provider URL
```yaml
providerURL: "https://keycloak.your-domain.com/realms/{realm-name}"
```
### Configuration
```yaml
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: oidc-keycloak
spec:
plugin:
traefikoidc:
providerURL: "https://keycloak.company.com/realms/your-realm"
clientID: "your-keycloak-client-id"
clientSecret: "your-keycloak-client-secret"
callbackURL: "/oauth2/callback"
sessionEncryptionKey: "your-32-char-encryption-key-here"
scopes:
- openid
- profile
- email
- roles
- groups
- offline_access
allowedRolesAndGroups:
- admin
- editor
forceHttps: true
enablePkce: true
```
### Internal Network Deployment
For private IP addresses (Docker networks, Kubernetes):
```yaml
providerURL: "https://192.168.1.100:8443/realms/your-realm"
allowPrivateIPAddresses: true # Required for private IPs
```
### Keycloak Client Setup
1. Access Keycloak Admin Console
2. Select your realm
3. Go to Clients > Create client
4. Set Client Protocol: openid-connect
5. Set Access Type: confidential
6. Add Valid Redirect URIs: `https://your-domain.com/oauth2/callback`
7. Generate client secret in Credentials tab
8. Configure mappers to add claims to ID Token:
- Email: User Property mapper with "Add to ID token" enabled
- Roles: User Client Role mapper with "Add to ID token" enabled
- Groups: Group Membership mapper with "Add to ID token" enabled
---
## AWS Cognito
### Provider URL
```yaml
providerURL: "https://cognito-idp.{region}.amazonaws.com/{user-pool-id}"
```
### Configuration
```yaml
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: oidc-cognito
spec:
plugin:
traefikoidc:
providerURL: "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_ABCDEF123"
clientID: "your-cognito-client-id"
clientSecret: "your-cognito-client-secret"
callbackURL: "/oauth2/callback"
sessionEncryptionKey: "your-32-char-encryption-key-here"
scopes:
- openid
- profile
- email
- aws.cognito.signin.user.admin
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/oauth2/callback`
- Sign out URLs: `https://your-domain.com/oauth2/logout`
- OAuth flows: Authorization code grant
4. Configure hosted UI domain (optional)
5. Set up groups for role-based access
---
## GitLab
### Provider URL
```yaml
# GitLab.com
providerURL: "https://gitlab.com"
# Self-hosted
providerURL: "https://gitlab.your-company.com"
```
### Configuration
```yaml
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: oidc-gitlab
spec:
plugin:
traefikoidc:
providerURL: "https://gitlab.com"
clientID: "your-gitlab-application-id"
clientSecret: "your-gitlab-application-secret"
callbackURL: "/oauth2/callback"
sessionEncryptionKey: "your-32-char-encryption-key-here"
scopes:
- openid
- profile
- email
# Note: GitLab doesn't require offline_access scope
# Refresh tokens are issued automatically with openid
allowedRolesAndGroups:
- developers
- maintainers
forceHttps: true
enablePkce: true
```
### GitLab 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/oauth2/callback`
5. Save and note Application ID and Secret
---
## GitHub
### Provider URL
```yaml
providerURL: "https://github.com"
```
### Configuration
```yaml
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: oauth-github
spec:
plugin:
traefikoidc:
providerURL: "https://github.com/login/oauth"
clientID: "your-github-client-id"
clientSecret: "your-github-client-secret"
callbackURL: "/oauth2/callback"
sessionEncryptionKey: "your-32-char-encryption-key-here"
scopes:
- user:email
- read:user
allowedUsers:
- "github-username"
forceHttps: true
```
### Limitations
- **OAuth 2.0 only** - Not OpenID Connect
- **No ID tokens** - Only access tokens for API calls
- **No refresh tokens** - Users must re-authenticate on expiry
- **No standard claims** - User info requires API calls
Use GitHub only for API access, not for user authentication with claims.
### GitHub Setup
1. Go to GitHub Settings > Developer settings > OAuth Apps
2. Create new OAuth App
3. Set Authorization callback URL: `https://your-domain.com/oauth2/callback`
4. Note Client ID and generate Client Secret
---
## Generic OIDC
For any OIDC-compliant provider not listed above.
### Configuration
```yaml
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: oidc-generic
spec:
plugin:
traefikoidc:
providerURL: "https://oidc.your-provider.com"
clientID: "your-client-id"
clientSecret: "your-client-secret"
callbackURL: "/oauth2/callback"
sessionEncryptionKey: "your-32-char-encryption-key-here"
scopes:
- openid
- profile
- email
forceHttps: true
enablePkce: true
```
### Requirements
- Provider must expose `.well-known/openid-configuration` endpoint
- Must support authorization code flow
- ID tokens must contain required claims (email, sub, etc.)
---
## Automatic Scope Filtering
The middleware automatically filters OAuth scopes based on the provider's declared capabilities.
### How It Works
1. Fetches provider's `.well-known/openid-configuration`
2. Extracts `scopes_supported` field
3. Filters requested scopes to only include supported ones
4. Falls back to all requested scopes if provider doesn't declare supported scopes
### Example: Self-Hosted GitLab
Self-hosted GitLab may reject `offline_access` scope:
```yaml
scopes:
- openid
- profile
- email
- offline_access # Will be automatically filtered out if unsupported
```
The middleware will:
1. Read GitLab's discovery document
2. Detect `offline_access` is NOT in `scopes_supported`
3. Filter it out automatically
4. Authentication succeeds
### Logging
```
INFO: ScopeFilter: Filtered unsupported scopes: [offline_access]
DEBUG: ScopeFilter: Final filtered scopes: [openid profile email]
```
### Troubleshooting
If a provider rejects scopes even after filtering:
1. Check the provider's discovery document: `curl https://provider/.well-known/openid-configuration`
2. Use `overrideScopes: true` with only supported scopes
3. Review middleware debug logs for filtering decisions
-970
View File
@@ -1,970 +0,0 @@
# 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
- **Application ID URI**: Supports custom audience for protected APIs
### Example Configuration (Basic)
```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 AD API Configuration (Application ID URI)
When exposing your application as an API with a custom Application ID URI, you need to specify the `audience` parameter. Azure AD includes the Application ID URI in the JWT `aud` claim.
```yaml
http:
middlewares:
azure-api-oidc:
plugin:
traefik-oidc:
providerUrl: "https://login.microsoftonline.com/common/v2.0"
clientId: "12345678-1234-1234-1234-123456789abc"
clientSecret: "your-azure-client-secret"
# Specify the Application ID URI as audience
audience: "api://12345678-1234-1234-1234-123456789abc"
callbackUrl: "https://app.example.com/auth/callback"
logoutUrl: "https://app.example.com/auth/logout"
scopes: ["openid", "profile", "email", "offline_access"]
forceHttps: true
```
**Important**:
- The `audience` parameter should match your Application ID URI (typically `api://{app-id}`)
- Find your Application ID URI in Azure Portal → App Registration → Expose an API → Application ID URI
- Without the `audience` parameter, access tokens with custom audiences will be rejected
- For ID token validation only (no API access), you can omit the `audience` parameter
### 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
### Azure AD API Exposure Setup (for custom audiences)
1. In your App Registration, go to "Expose an API"
2. Set the Application ID URI (e.g., `api://12345678-1234-1234-1234-123456789abc`)
3. Add any custom scopes your API exposes
4. Update the middleware configuration to include the `audience` parameter with this URI
---
## 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
- **API audiences**: Supports custom audience for API access tokens
### Example Configuration (Basic)
```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: ["user@example.com", "admin@company.com"]
forceHttps: true
enablePkce: true
```
### Auth0 API Configuration (Custom Audience)
When using Auth0 APIs with custom audience parameters, you need to specify the `audience` field. Auth0 includes the API identifier in the JWT `aud` claim instead of the `clientId`.
```yaml
http:
middlewares:
auth0-api-oidc:
plugin:
traefik-oidc:
providerUrl: "https://company.auth0.com"
clientId: "abcdef123456789"
clientSecret: "your-auth0-client-secret"
# Specify the Auth0 API identifier as audience
audience: "https://api.company.com"
callbackUrl: "https://app.example.com/auth/callback"
logoutUrl: "https://app.example.com/auth/logout"
scopes: ["openid", "profile", "email", "offline_access"]
forceHttps: true
enablePkce: true
```
**Important**:
- The `audience` parameter should match your Auth0 API identifier (not the client ID)
- Find your API identifier in Auth0 Dashboard → APIs → Your API → Settings → Identifier
- Without the `audience` parameter, access tokens with custom audiences will be rejected with "invalid audience" error
- For ID token validation only (no APIs), you can omit the `audience` parameter
### 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
### Auth0 API Setup (for custom audiences)
1. Go to Auth0 Dashboard → APIs
2. Create a new API or select existing API
3. Note the "Identifier" field (e.g., `https://api.company.com`) - this is your `audience` value
4. In API Settings → Machine to Machine Applications, authorize your application
5. Configure API permissions/scopes as needed
6. Use the API identifier as the `audience` parameter in your configuration
---
## 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 without requiring `offline_access` scope
### 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"]
# Note: GitLab doesn't support the offline_access scope.
# Refresh tokens are issued automatically for the openid scope.
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
### Internal Network Deployment
If your Keycloak instance runs on an internal network with private IP addresses (e.g., Docker networks, Kubernetes internal services), set `allowPrivateIPAddresses: true`:
```yaml
traefikoidc:
providerUrl: "https://192.168.1.100:8443/auth/realms/your-realm" # Private IP
allowPrivateIPAddresses: true # Required for private IP addresses
clientId: "your-client-id"
clientSecret: "your-client-secret"
# ... other config
```
> **Security Warning**: Only enable `allowPrivateIPAddresses` in trusted network environments where you control the OIDC provider. This setting reduces SSRF protection.
---
## 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
```
---
## Automatic Scope Filtering
### Overview
The middleware automatically filters OAuth scopes based on the provider's capabilities declared in their OIDC discovery document (`.well-known/openid-configuration`). This prevents authentication failures when providers reject unsupported scopes.
### How It Works
1. **Discovery Document Parsing**: The middleware fetches the provider's discovery document and extracts the `scopes_supported` field
2. **Intelligent Filtering**: Requested scopes are filtered to only include those the provider supports
3. **Fallback Behavior**: If the provider doesn't declare `scopes_supported`, all requested scopes are used (backward compatible)
4. **Provider-Specific Handling**: Special logic for Google and Azure is preserved and applied after filtering
### Example Scenarios
#### Self-Hosted GitLab
**Problem**: Self-hosted GitLab instances reject the `offline_access` scope with error:
```
The requested scope is invalid, unknown, or malformed.
```
**Solution**: The middleware automatically detects this by:
1. Reading GitLab's discovery document at `https://gitlab.example.com/.well-known/openid-configuration`
2. Observing that `offline_access` is NOT in the `scopes_supported` list
3. Filtering out `offline_access` from the request
4. Authentication succeeds
**Configuration**:
```yaml
http:
middlewares:
gitlab-oidc:
plugin:
traefik-oidc:
providerUrl: "https://gitlab.example.com"
clientId: "your-gitlab-application-id"
clientSecret: "your-gitlab-application-secret"
callbackUrl: "https://app.example.com/auth/callback"
scopes: ["openid", "profile", "email", "offline_access"]
# Even though offline_access is listed, it will be automatically
# filtered out if GitLab doesn't support it
```
#### Auth0 or Keycloak
These providers typically support `offline_access` and it will be included:
```yaml
# Auth0 scopes_supported: ["openid", "profile", "email", "offline_access", ...]
# Result: All requested scopes are sent
```
### Benefits
1. **Self-Hosted Support**: Works seamlessly with self-hosted provider instances
2. **No Manual Configuration**: No need to know which scopes each provider supports
3. **Error Prevention**: Eliminates "invalid scope" authentication failures
4. **Standards Compliant**: Uses official OIDC discovery specification (RFC 8414)
5. **Backward Compatible**: Existing configurations continue to work
### Logging
The middleware provides detailed logging for scope filtering:
```
INFO: ScopeFilter: Filtered unsupported scopes for https://gitlab.example.com: [offline_access]
DEBUG: ScopeFilter: Provider https://gitlab.example.com supported scopes: [openid profile email read_user read_api]
DEBUG: ScopeFilter: Final filtered scopes: [openid profile email]
```
### Troubleshooting
**Issue**: Provider rejects scope even after filtering
**Possible Causes**:
1. Provider's discovery document is outdated
2. Provider doesn't properly implement `scopes_supported`
3. Custom authorization server with non-standard behavior
**Solutions**:
1. Use `overrideScopes: true` and explicitly list only supported scopes
2. Check the provider's discovery document manually: `curl https://your-provider/.well-known/openid-configuration`
3. Review middleware debug logs for filtering decisions
---
## Common Configuration Options
### Audience Configuration
The `audience` parameter specifies the expected JWT audience claim value. This is particularly important when using Auth0 APIs, Azure AD Application ID URIs, or other providers with custom audience requirements.
```yaml
# Optional: Custom audience for JWT validation
# If not set, defaults to clientID for backward compatibility
audience: "https://api.example.com" # Auth0 API identifier
# OR
audience: "api://12345-guid" # Azure AD Application ID URI
```
**When to use**:
- **Auth0**: When using Auth0 APIs with custom audience parameters
- **Azure AD**: When exposing your app as an API with Application ID URI
- **Keycloak**: When using audience-restricted tokens
- **Okta**: When using custom authorization servers with API audiences
**When to omit**:
- For standard ID token validation (default behavior)
- When the provider sets `aud` claim to your `clientID`
- For backward compatibility with existing configurations
**Security Note**: The `audience` parameter prevents token confusion attacks by ensuring tokens issued for one service cannot be used at another service.
### 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: ["user1@example.com", "user2@example.com"]
# 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
```
+546
View File
@@ -0,0 +1,546 @@
# Redis Cache for Distributed Deployments
Redis cache support for multi-replica Traefik deployments with shared state.
## Table of Contents
- [Overview](#overview)
- [Why Use Redis Cache?](#why-use-redis-cache)
- [Configuration](#configuration)
- [Cache Modes](#cache-modes)
- [Deployment Examples](#deployment-examples)
- [Performance Tuning](#performance-tuning)
- [Monitoring](#monitoring)
- [Troubleshooting](#troubleshooting)
- [Migration Guide](#migration-guide)
---
## Overview
The Redis cache feature provides distributed caching for the Traefik OIDC plugin, enabling seamless operation across multiple Traefik instances.
### Key Features
- **Distributed JTI Replay Detection**: Prevents token replay attacks across all instances
- **Shared Session Management**: Consistent user sessions across replicas
- **Circuit Breaker**: Automatic fallback to memory cache during Redis outages
- **Health Checking**: Continuous monitoring of Redis connectivity
- **Flexible Cache Modes**: Memory, Redis, or hybrid caching strategies
- **Pure-Go Implementation**: Yaegi-compatible, works with dynamic plugin loading
### Architecture
```
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Traefik #1 │ │ Traefik #2 │ │ Traefik #3 │
│ (Plugin) │ │ (Plugin) │ │ (Plugin) │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
└────────────────────┼────────────────────┘
┌──────▼──────┐
│ Redis │
│ (Shared │
│ Cache) │
└─────────────┘
```
---
## Why Use Redis Cache?
### The Problem
When running multiple Traefik instances without shared cache:
1. **False Positive Replay Detection**
- User authenticates → Token stored in Instance A's JTI cache
- Next request → Load balancer routes to Instance B
- Instance B doesn't have the JTI → Falsely detects replay attack
2. **Session Inconsistency**
- User session created on Instance A
- Subsequent request routed to Instance B
- Instance B has no knowledge of the session
3. **Token Metadata Fragmentation**
- Token refresh happens on Instance A
- Other instances continue using old tokens
### The Solution
Redis provides centralized cache that all instances share, ensuring:
- **Consistent Authentication**: All instances share authentication state
- **True Replay Detection**: JTI cache shared across all instances
- **Seamless Scaling**: Add/remove instances without affecting sessions
- **High Availability**: Circuit breaker with automatic fallback
---
## Configuration
### Basic Configuration
```yaml
redis:
enabled: true
address: "redis:6379"
password: "your-password" # Optional
db: 0
keyPrefix: "traefikoidc:"
cacheMode: "hybrid"
```
### All Configuration Options
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `enabled` | bool | `false` | Enable Redis caching |
| `address` | string | - | Redis server address (`host:port`) |
| `password` | string | - | Redis password (optional) |
| `db` | int | `0` | Redis database number (0-15) |
| `keyPrefix` | string | `traefikoidc:` | Prefix for all Redis keys |
| `cacheMode` | string | `redis` | Cache mode: `memory`, `redis`, `hybrid` |
| `poolSize` | int | `10` | Connection pool size |
| `connectTimeout` | int | `5` | Connection timeout (seconds) |
| `readTimeout` | int | `3` | Read timeout (seconds) |
| `writeTimeout` | int | `3` | Write timeout (seconds) |
| `enableTLS` | bool | `false` | Enable TLS for connections |
| `tlsSkipVerify` | bool | `false` | Skip TLS certificate verification |
| `enableCircuitBreaker` | bool | `true` | Enable circuit breaker |
| `circuitBreakerThreshold` | int | `5` | Failures before circuit opens |
| `circuitBreakerTimeout` | int | `60` | Circuit reset timeout (seconds) |
| `enableHealthCheck` | bool | `true` | Enable periodic health checks |
| `healthCheckInterval` | int | `30` | Health check interval (seconds) |
| `hybridL1Size` | int | `500` | Max items in L1 cache (hybrid mode) |
| `hybridL1MemoryMB` | int64 | `10` | Max memory for L1 cache in MB |
### Environment Variables (Fallback)
If not configured through Traefik, these environment variables are used:
```bash
REDIS_ENABLED=true
REDIS_ADDRESS=redis:6379
REDIS_PASSWORD=your-password
REDIS_DB=0
REDIS_KEY_PREFIX=traefikoidc:
REDIS_CACHE_MODE=hybrid
REDIS_POOL_SIZE=10
REDIS_CONNECT_TIMEOUT=5
REDIS_READ_TIMEOUT=3
REDIS_WRITE_TIMEOUT=3
REDIS_ENABLE_TLS=false
REDIS_TLS_SKIP_VERIFY=false
```
---
## Cache Modes
### Memory Mode (Default without Redis)
```yaml
redis:
cacheMode: "memory"
```
- Uses only in-memory cache
- Suitable for single-instance deployments
- No Redis dependency
- Fastest performance
### Redis Mode
```yaml
redis:
enabled: true
address: "redis:6379"
cacheMode: "redis"
```
- All operations go directly to Redis
- Ensures consistency across replicas
- Slightly higher latency
### Hybrid Mode (Recommended)
```yaml
redis:
enabled: true
address: "redis:6379"
cacheMode: "hybrid"
```
Two-tier caching strategy:
```
┌─────────────────────────────────────────┐
│ Client Request │
└────────────────┬────────────────────────┘
┌────────────────┐
│ Local Cache │ ← L1 Cache (Fast)
│ (Memory) │
└────────┬───────┘
│ Miss
┌────────────────┐
│ Remote Cache │ ← L2 Cache (Shared)
│ (Redis) │
└────────────────┘
```
**Read Path:**
1. Check local memory cache (L1)
2. On miss, check Redis (L2)
3. On hit in Redis, populate L1
4. Return value
**Write Path:**
1. Write to Redis (L2) for durability
2. Write to local cache (L1) for speed
### Performance Comparison
| Operation | Memory Mode | Redis Mode | Hybrid Mode |
|-----------|------------|------------|-------------|
| Read (p50) | 0.1ms | 2ms | 0.2ms |
| Read (p99) | 0.5ms | 10ms | 5ms |
| Write (p50) | 0.2ms | 3ms | 3ms |
| Throughput | 100k/s | 20k/s | 80k/s |
---
## Deployment Examples
### Docker Compose
```yaml
version: '3.8'
services:
redis:
image: redis:7-alpine
command: redis-server --requirepass ${REDIS_PASSWORD}
volumes:
- redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
interval: 30s
timeout: 3s
retries: 3
traefik:
image: traefik:v3.2
deploy:
replicas: 3
labels:
- "traefik.http.middlewares.oidc.plugin.traefikoidc.redis.enabled=true"
- "traefik.http.middlewares.oidc.plugin.traefikoidc.redis.address=redis:6379"
- "traefik.http.middlewares.oidc.plugin.traefikoidc.redis.password=${REDIS_PASSWORD}"
- "traefik.http.middlewares.oidc.plugin.traefikoidc.redis.cacheMode=hybrid"
depends_on:
redis:
condition: service_healthy
volumes:
redis-data:
```
### Kubernetes
```yaml
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: oidc-with-redis
spec:
plugin:
traefikoidc:
providerURL: https://accounts.google.com
clientID: your-client-id
clientSecret: your-client-secret
sessionEncryptionKey: your-encryption-key
callbackURL: /oauth2/callback
redis:
enabled: true
address: "redis-service.redis-namespace:6379"
password: "urn:k8s:secret:redis-secret:password"
db: 0
keyPrefix: "traefikoidc:"
cacheMode: "hybrid"
poolSize: 20
enableCircuitBreaker: true
circuitBreakerThreshold: 5
```
### AWS ElastiCache
```yaml
redis:
enabled: true
address: "your-cache.abc123.cache.amazonaws.com:6379"
cacheMode: "hybrid"
enableTLS: true
password: "your-elasticache-auth-token"
```
---
## Performance Tuning
### Connection Pool Sizing
```yaml
redis:
poolSize: 20 # Formula: 2 * CPU cores * replicas
# For 4 cores, 3 replicas: poolSize = 24
```
### TTL Strategy
The plugin automatically sets TTLs based on token lifetimes:
- **JTI Cache**: Matches token lifetime (typically 1 hour)
- **Session**: Matches `sessionMaxAge` configuration
- **Token Metadata**: 5 minutes (short-lived)
### Redis Server Configuration
```bash
# Recommended Redis settings for cache
maxmemory 512mb
maxmemory-policy allkeys-lru # Evict least recently used
# For cache data, disable persistence for better performance
save ""
appendonly no
```
### Hybrid Mode Tuning
```yaml
redis:
cacheMode: "hybrid"
hybridL1Size: 500 # Max items in local cache
hybridL1MemoryMB: 10 # Max memory for local cache
```
---
## Monitoring
### Key Metrics
- **Cache hit rate** (target: >90% for hybrid mode)
- **Redis latency** (target: <10ms p99)
- **Circuit breaker state**
- **Connection pool utilization
### Redis Commands for Monitoring
```bash
# Monitor commands in real-time
redis-cli MONITOR
# Check slow queries
redis-cli SLOWLOG GET 10
# Memory usage
redis-cli INFO memory
# Key statistics
redis-cli DBSIZE
# List keys with prefix
redis-cli --scan --pattern "traefikoidc:*"
# Check key TTL
redis-cli TTL "traefikoidc:session:abc123"
```
### Health Check Endpoint
The plugin provides health information including:
```json
{
"status": "healthy",
"cache": {
"mode": "hybrid",
"redis": {
"connected": true,
"latency": "2ms"
},
"circuit_breaker": {
"state": "closed",
"failures": 0
}
}
}
```
---
## Troubleshooting
### Connection Refused
**Symptoms:** `dial tcp: connection refused`
**Solutions:**
1. Verify Redis is running: `redis-cli ping`
2. Check network connectivity: `telnet redis-host 6379`
3. Verify address configuration
### Authentication Failure
**Symptoms:** `NOAUTH Authentication required`
**Solutions:**
1. Set Redis password in configuration
2. Verify password is correct
### Circuit Breaker Open
**Symptoms:** `Circuit breaker is open`, falling back to memory
**Solutions:**
1. Check Redis health: `redis-cli INFO server`
2. Review network latency: `redis-cli --latency`
3. Adjust circuit breaker thresholds if needed
### High Memory Usage
**Symptoms:** Redis memory constantly growing, OOM errors
**Solutions:**
1. Configure eviction policy:
```bash
CONFIG SET maxmemory 512mb
CONFIG SET maxmemory-policy allkeys-lru
```
2. Review key count: `redis-cli DBSIZE`
3. Check for large keys: `redis-cli --bigkeys`
### Inconsistent Cache State
**Symptoms:** Different responses from different replicas
**Solutions:**
1. Verify all instances use the same Redis address
2. Check cache mode consistency across instances
3. Verify time synchronization on all hosts
---
## Migration Guide
### From Memory-Only to Redis
#### Phase 1: Preparation
1. Deploy Redis infrastructure
2. Test Redis connectivity
3. Configure monitoring
#### Phase 2: Gradual Rollout
1. Enable Redis on one instance:
```yaml
redis:
enabled: true
address: "redis:6379"
cacheMode: "hybrid"
```
2. Monitor for errors
3. Gradually enable on more instances
#### Phase 3: Full Migration
1. Enable Redis on all instances
2. Remove `disableReplayDetection: true` if set
3. Monitor for issues
### Rollback Plan
If issues occur:
1. Set `redis.enabled: false`
2. Plugin falls back to memory cache automatically
3. Investigate and resolve issues
### Migration Checklist
- [ ] Redis deployed and accessible
- [ ] Redis password configured
- [ ] Network connectivity verified
- [ ] Monitoring configured
- [ ] Backup plan prepared
- [ ] Test environment validated
- [ ] Gradual rollout planned
---
## Best Practices
### Security
- Always use Redis password authentication
- Enable TLS for production deployments
- Use network segmentation (private subnets)
- Rotate Redis passwords regularly
### High Availability
- Use Redis Sentinel or Cluster for HA
- Configure appropriate circuit breaker thresholds
- Implement proper health checks
- Use connection pooling
### Performance
- Use hybrid cache mode for best performance
- Monitor cache hit rates
- Size Redis memory appropriately
- Disable persistence for cache-only usage
### Operations
- Implement comprehensive monitoring
- Set up alerting for circuit breaker state
- Document Redis configuration
- Test failover scenarios
---
## FAQ
### Is Redis required?
No, Redis is optional. The plugin works with in-memory cache for single-instance deployments.
### What happens if Redis goes down?
The circuit breaker opens after threshold failures, and the plugin falls back to in-memory cache. It periodically attempts to reconnect.
### Which cache mode should I use?
For production multi-replica deployments, use `hybrid` mode for best performance and consistency.
### How much memory does Redis need?
Depends on active sessions and token sizes:
- Small (1-1000 users): 128MB
- Medium (1000-10000 users): 256-512MB
- Large (10000+ users): 1GB+
### Can I use managed Redis services?
Yes, the plugin works with AWS ElastiCache, Azure Cache for Redis, Google Cloud Memorystore, and Redis Enterprise Cloud.
### Is data encrypted in Redis?
Session data is encrypted before storing using `sessionEncryptionKey`. Additionally, you can enable TLS for Redis connections.
-1125
View File
File diff suppressed because it is too large Load Diff
-413
View File
@@ -1,413 +0,0 @@
# Redis Cache Backend Test Suite
## Overview
This document describes the comprehensive test suite created for the Redis cache backend feature in the Traefik OIDC plugin. The test suite ensures reliability, performance, and correctness of the caching infrastructure.
## Test Structure
### Directory Organization
```
internal/cache/
├── backend/
│ ├── interface.go # CacheBackend interface definition
│ ├── interface_test.go # Contract tests for all backends
│ ├── memory.go # In-memory backend implementation
│ ├── memory_test.go # Memory backend unit tests
│ ├── redis.go # Redis backend implementation
│ ├── redis_test.go # Redis backend unit tests
│ ├── errors.go # Error definitions
│ └── test_helpers_test.go # Test infrastructure and helpers
└── resilience/
├── circuit_breaker.go # Circuit breaker implementation
├── circuit_breaker_test.go # Circuit breaker tests
├── health_check.go # Health checker implementation
└── health_check_test.go # Health check tests
redis_integration_test.go # End-to-end integration tests
```
## Test Categories
### 1. Interface Contract Tests (`interface_test.go`)
**Purpose:** Ensure all backend implementations (Memory, Redis, Hybrid) comply with the CacheBackend interface contract.
**Test Cases:**
- `TestCacheBackendContract` - Runs all contract tests against each backend type
- `testBasicSetGet` - Verifies basic set/get operations
- `testGetNonExistent` - Tests behavior for non-existent keys
- `testUpdateExisting` - Validates updating existing keys
- `testDelete` - Tests delete operations
- `testDeleteNonExistent` - Delete non-existent keys
- `testExists` - Key existence checking
- `testTTLExpiration` - TTL and expiration behavior
- `testClear` - Clear all keys operation
- `testPing` - Health check functionality
- `testStats` - Statistics tracking
- `testConcurrentAccess` - Thread safety with 10+ goroutines
- `testLargeValues` - Handling of 1MB+ values
- `testEmptyValues` - Empty byte array handling
- `testSpecialCharactersInKeys` - Special characters in key names
**Coverage:** ~95% of interface methods
### 2. Memory Backend Tests (`memory_test.go`)
**Purpose:** Test the in-memory LRU cache backend with comprehensive edge cases.
**Test Cases:**
#### Basic Operations (6 tests)
- `TestMemoryBackend_BasicOperations` - CRUD operations
- SetAndGet
- GetNonExistent
- Delete
- DeleteNonExistent
- Exists
- Clear
#### TTL and Expiration (3 tests)
- `TestMemoryBackend_TTLExpiration`
- ShortTTL (100ms)
- TTLDecrement over time
- CleanupExpiredItems
#### LRU Eviction (2 tests)
- `TestMemoryBackend_LRUEviction` - Verifies LRU algorithm
- `TestMemoryBackend_MemoryLimit` - Memory-based eviction
#### Concurrency (1 test)
- `TestMemoryBackend_ConcurrentAccess` - 20 goroutines, 50 iterations each
#### Edge Cases (6 tests)
- `TestMemoryBackend_UpdateExisting` - Overwriting values
- `TestMemoryBackend_Stats` - Metrics tracking (hits, misses, hit rate)
- `TestMemoryBackend_EmptyValues` - Zero-length byte arrays
- `TestMemoryBackend_LargeValues` - 1MB values
- `TestMemoryBackend_Close` - Proper cleanup
- `TestMemoryBackend_Ping` - Health checks
- `TestMemoryBackend_ValueIsolation` - Returns copies, not references
**Coverage:** ~92% of memory backend code
### 3. Redis Backend Tests (`redis_test.go`)
**Purpose:** Test Redis backend using miniredis (in-memory Redis mock).
**Test Cases:**
#### Basic Operations (4 tests)
- `TestRedisBackend_BasicOperations`
- SetAndGet
- GetNonExistent
- Delete
- Exists
#### Redis-Specific Features (6 tests)
- `TestRedisBackend_KeyPrefixing` - Namespace isolation
- `TestRedisBackend_TTLExpiration` - Redis TTL handling
- `TestRedisBackend_Clear` - Bulk delete with SCAN
- `TestRedisBackend_NoPrefix` - Operation without prefix
#### Error Handling (2 tests)
- `TestRedisBackend_ConnectionFailure` - Connection errors
- `TestRedisBackend_RedisErrors` - Simulated Redis failures
#### Concurrency (1 test)
- `TestRedisBackend_ConcurrentAccess` - 20 goroutines, 50 operations
#### Advanced Features (3 tests)
- `TestRedisBackend_PipelineOperations`
- SetMany (batch writes)
- GetMany (batch reads)
- GetManyWithNonExistent
#### Edge Cases (5 tests)
- `TestRedisBackend_Stats` - Statistics tracking
- `TestRedisBackend_Ping` - Connection health
- `TestRedisBackend_Close` - Resource cleanup
- `TestRedisBackend_UpdateExisting` - Overwrite handling
- `TestRedisBackend_LargeValues` - 1MB values
- `TestRedisBackend_EmptyValues` - Empty arrays
**Coverage:** ~88% of Redis backend code
**Key Testing Tool:** `miniredis` - In-memory Redis mock that supports:
- All basic Redis commands
- TTL and expiration
- Time manipulation (FastForward)
- Error simulation
- No external Redis server required
### 4. Circuit Breaker Tests (`circuit_breaker_test.go`)
**Purpose:** Verify circuit breaker pattern implementation for fault tolerance.
**Test Cases:**
#### State Transitions (5 tests)
- `TestCircuitBreaker_StateTransitions`
- Initial state (Closed)
- Closed → Open (after max failures)
- Open → HalfOpen (after timeout)
- HalfOpen → Closed (after successful requests)
- HalfOpen → Open (on failure)
#### Behavior Tests (5 tests)
- `TestCircuitBreaker_OpenCircuitBlocks` - Blocks requests when open
- `TestCircuitBreaker_HalfOpenMaxRequests` - Limits requests in half-open
- `TestCircuitBreaker_SuccessResetsFailures` - Failure counter reset
- `TestCircuitBreaker_ConcurrentAccess` - Thread safety
- `TestCircuitBreaker_Stats` - Statistics tracking
#### Advanced Tests (7 tests)
- `TestCircuitBreaker_Reset` - Manual reset
- `TestCircuitBreaker_StateChangeCallback` - Notifications
- `TestCircuitBreaker_IsAvailable` - Availability check
- `TestCircuitBreaker_RapidFailures` - Fast consecutive failures
- `TestCircuitBreaker_TimeoutAccuracy` - Timeout precision
- `TestCircuitBreaker_DefaultConfig` - Default configuration
- `TestCircuitBreaker_StateString` - String representation
**Benchmarks:**
- `BenchmarkCircuitBreaker_Execute` - Successful operations
- `BenchmarkCircuitBreaker_ExecuteWithFailures` - Mixed success/failure
**Coverage:** ~95% of circuit breaker code
### 5. Health Check Tests (`health_check_test.go`)
**Purpose:** Validate periodic health checking and status management.
**Test Cases:**
#### Status Transitions (4 tests)
- `TestHealthChecker_StatusTransitions` - Healthy → Degraded → Unhealthy → Healthy
- `TestHealthChecker_InitialState` - Default healthy state
- `TestHealthChecker_ForceCheck` - Manual health check trigger
- `TestHealthChecker_StatusChangeCallback` - Change notifications
#### Behavior Tests (6 tests)
- `TestHealthChecker_Stats` - Statistics tracking
- `TestHealthChecker_Timeout` - Check timeout handling
- `TestHealthChecker_ConcurrentAccess` - Thread safety
- `TestHealthChecker_StopAndStart` - Lifecycle management
- `TestHealthChecker_DegradedState` - Degraded status detection
- `TestHealthChecker_DefaultConfig` - Default settings
#### Advanced Tests (2 tests)
- `TestHealthChecker_StatusString` - String representation
- `TestHealthChecker_RecoveryPattern` - Typical failure/recovery cycle
**Benchmarks:**
- `BenchmarkHealthChecker_ForceCheck` - Check performance
- `BenchmarkHealthChecker_Status` - Status read performance
**Coverage:** ~90% of health checker code
### 6. Integration Tests (`redis_integration_test.go`)
**Purpose:** End-to-end testing of real-world scenarios.
**Test Cases:**
#### Multi-Instance Tests (3 tests)
- `TestRedisIntegration_MultipleInstances`
- ShareTokenBlacklist - JTI sharing across Traefik replicas
- ShareTokenCache - Token cache sharing
- ShareMetadataCache - Provider metadata sharing
#### Replay Detection (2 tests)
- `TestRedisIntegration_JTIReplayDetection`
- PreventReplayAcrossInstances - Block used JTIs
- ConcurrentJTIChecks - Race condition handling
#### Resilience (1 test)
- `TestRedisIntegration_Failover`
- RedisTemporaryFailure - Recovery from temporary failures
#### Performance (1 test)
- `TestRedisIntegration_HighLoad`
- HighConcurrency - 50 goroutines × 100 operations
#### Consistency (2 tests)
- `TestRedisIntegration_TTLConsistency` - TTL accuracy
- `TestRedisIntegration_MemoryUsage` - 10,000 item dataset
- `TestRedisIntegration_Cleanup` - Bulk cleanup operations
**Coverage:** Integration scenarios covering 80%+ of realistic use cases
## Test Helpers and Infrastructure
### Test Helpers (`test_helpers_test.go`)
**Utilities:**
- `TestLogger` - Logging for tests
- `MiniredisServer` - Miniredis setup/teardown
- `TestConfig` - Default test configurations
- `GenerateTestData` - Test data generation
- `GenerateLargeValue` - Large value creation
- `AssertCacheStats` - Statistics validation
- `WaitForCondition` - Async condition waiting
- `AssertEventuallyExpires` - TTL expiration verification
## Running the Tests
### Run All Tests
```bash
go test ./internal/cache/backend/... -v
go test ./internal/cache/resilience/... -v
go test -run TestRedisIntegration -v
```
### Run Specific Test Suites
```bash
# Memory backend only
go test ./internal/cache/backend -run TestMemoryBackend -v
# Redis backend only
go test ./internal/cache/backend -run TestRedisBackend -v
# Circuit breaker only
go test ./internal/cache/resilience -run TestCircuitBreaker -v
# Integration tests only
go test -run TestRedisIntegration -v
```
### Run with Coverage
```bash
go test ./internal/cache/backend/... -coverprofile=coverage.out
go test ./internal/cache/resilience/... -coverprofile=coverage_resilience.out
go tool cover -html=coverage.out
```
### Run Benchmarks
```bash
go test ./internal/cache/backend -bench=. -benchmem
go test ./internal/cache/resilience -bench=. -benchmem
```
### Run with Race Detector
```bash
go test ./internal/cache/... -race -v
```
## Test Patterns Used
### 1. Table-Driven Tests
Used for testing multiple scenarios with similar structure.
### 2. Subtests (t.Run)
Organized test cases into logical groups with clear names.
### 3. Parallel Tests
Tests marked with `t.Parallel()` for faster execution.
### 4. Test Fixtures
Reusable setup functions for common test data.
### 5. Mocking
- `miniredis` for Redis operations
- Mock functions for callbacks and health checks
### 6. Assertion Helpers
Using `testify/assert` and `testify/require` for clear assertions.
## Test Coverage Summary
| Component | Coverage | Tests | Lines of Code |
|-----------|----------|-------|---------------|
| Interface Contract | 95% | 14 | ~200 |
| Memory Backend | 92% | 18 | ~350 |
| Redis Backend | 88% | 21 | ~400 |
| Circuit Breaker | 95% | 17 | ~250 |
| Health Checker | 90% | 12 | ~200 |
| Integration Tests | 80% | 9 | ~300 |
| **Total** | **90%** | **91** | **~1,700** |
## Edge Cases Tested
1. **Empty values** - Zero-length byte arrays
2. **Large values** - 1MB+ data
3. **Special characters** - Keys with :, /, -, _, ., |
4. **Concurrent access** - 10-50 goroutines
5. **TTL edge cases** - Very short (<100ms) and long (24h+) TTLs
6. **Connection failures** - Network errors, timeouts
7. **Redis errors** - Simulated Redis failures
8. **Memory limits** - Eviction under memory pressure
9. **Race conditions** - Concurrent JTI checks
10. **State transitions** - All circuit breaker and health check states
## Performance Benchmarks
Benchmarks included for:
- Cache operations (Set, Get, Delete)
- Circuit breaker execution
- Health check operations
- Concurrent access patterns
- Large datasets (10,000+ items)
## Dependencies
### Testing Libraries
- `github.com/stretchr/testify` - Assertions and test utilities
- `github.com/alicebob/miniredis/v2` - In-memory Redis mock
- `github.com/redis/go-redis/v9` - Redis client
### Why Miniredis?
- **No external dependencies** - No Redis server required
- **Fast** - In-memory, perfect for unit tests
- **Full Redis API** - Supports all operations we need
- **Time manipulation** - FastForward for TTL testing
- **Error simulation** - Test failure scenarios
## Future Enhancements
### Planned Tests
1. Hybrid backend tests (L1/L2 cache)
2. Network partition scenarios
3. Redis cluster support
4. Persistence and recovery tests
5. Metrics and monitoring integration
### Test Infrastructure Improvements
1. Test containers for real Redis integration
2. Performance regression tracking
3. Chaos engineering tests
4. Load testing framework
## Continuous Integration
### Recommended CI Configuration
```yaml
test:
script:
- go test ./internal/cache/... -race -cover -v
- go test -run TestRedisIntegration -v
- go test ./internal/cache/... -bench=. -benchmem
```
## Maintenance Guidelines
1. **Add tests for new features** - Maintain >85% coverage
2. **Update contract tests** - When interface changes
3. **Test edge cases** - Always test error paths
4. **Document test purpose** - Clear comments explaining what each test validates
5. **Keep tests fast** - Use t.Parallel() where possible
6. **Mock external dependencies** - Use miniredis, not real Redis
## Conclusion
This comprehensive test suite provides:
- **High confidence** in cache backend correctness
- **Fast feedback** - Tests run in seconds
- **Good coverage** - 90% overall
- **Clear documentation** - Each test is well-documented
- **Maintainability** - Clear structure and patterns
The test suite ensures that the Redis cache backend feature is production-ready and reliable for multi-replica Traefik deployments with shared caching requirements.
+390
View File
@@ -0,0 +1,390 @@
# Testing Guide
Comprehensive testing infrastructure for traefikoidc.
## Overview
| Metric | Value |
|--------|-------|
| Test files | 99 |
| Lines of test code | ~65,500 |
| Code coverage | 71.0% |
| Race conditions | None (all pass with `-race`) |
## Running Tests
```bash
# Run all tests
go test ./...
# Run with race detection
go test -race ./...
# Run with coverage
go test -cover ./...
# Run specific test suite
go test -v -run "TokenValidationSuite" .
# Run edge case tests
go test -v -run "ClockSkewEdgeCasesSuite|UnicodeClaimsSuite" .
```
## Test Infrastructure
### Directory Structure
```
internal/testutil/
├── compat.go # Re-exports for main package access
├── mocks/
│ ├── interfaces.go # JWKCache, TokenExchanger, TokenVerifier, etc.
│ ├── session.go # SessionManager, SessionData
│ ├── cache.go # Cache, TokenCache, Blacklist
│ └── interfaces_test.go # Mock verification tests
├── fixtures/
│ └── tokens.go # JWT token generation fixtures
└── servers/
├── oidc.go # Mock OIDC server factory
└── oidc_test.go # Server tests
```
### Test Suites
| Suite | File | Description |
|-------|------|-------------|
| TokenValidationSuite | `token_validation_suite_test.go` | Token validation happy path and error cases |
| JWKCacheTestSuite | `token_validation_suite_test.go` | JWK cache behavior tests |
| TokenExchangerTestSuite | `token_validation_suite_test.go` | Token exchange scenarios |
| ClockSkewEdgeCasesSuite | `edge_cases_suite_test.go` | Expiry boundary testing |
| UnicodeClaimsSuite | `edge_cases_suite_test.go` | Unicode/emoji handling in claims |
| LargeClaimsSuite | `edge_cases_suite_test.go` | Large data handling (100s of claims) |
| URLPathEdgeCasesSuite | `edge_cases_suite_test.go` | URL parsing edge cases |
| ConcurrencyEdgeCasesSuite | `edge_cases_suite_test.go` | Concurrent token validation |
| ExampleTestSuite | `testutil_example_test.go` | Example demonstrating patterns |
| AuthFlowBehaviourSuite | `auth_flow_behaviour_test.go` | Authentication flow behavior tests |
| SessionBehaviourSuite | `session_behaviour_test.go` | Session management behavior tests |
| EnhancedMocksSuite | `enhanced_mocks_suite_test.go` | Enhanced mock usage demonstration |
## Mock Types
The project provides two mocking patterns:
### State-Based Mocks (Basic)
Located in `main_test.go`, `mocks_test.go`. Simple mocks that store data in struct fields.
| Mock | Interface | Description |
|------|-----------|-------------|
| `MockJWKCache` | `JWKCacheInterface` | Simple state-based mock with JWKS/Err fields |
| `MockTokenVerifier` | `TokenVerifier` | Function-based mock for token verification |
| `MockTokenExchanger` | `TokenExchanger` | Function-based mock for token exchange |
| `MockOAuthProvider` | `http.Handler` | Full HTTP handler mock for OAuth provider simulation |
| `MockSessionManager` | `SessionManager` | State-based mock for session management |
| `MockHTTPClient` | N/A | Mock HTTP client with customizable responses |
**Usage:**
```go
mock := &MockJWKCache{
JWKS: &JWKSet{Keys: []JWK{jwk}},
Err: nil,
}
tOidc := &TraefikOidc{
jwkCache: mock,
// ...
}
```
### Enhanced State-Based Mocks (with Call Tracking)
Located in `enhanced_mocks_test.go`. State-based mocks with built-in call tracking and assertion helpers.
| Mock | Interface | Description |
|------|-----------|-------------|
| `EnhancedMockJWKCache` | `JWKCacheInterface` | State-based with call tracking |
| `EnhancedMockTokenVerifier` | `TokenVerifier` | State-based with call tracking |
| `EnhancedMockTokenExchanger` | `TokenExchanger` | State-based with call tracking |
| `EnhancedMockCacheInterface` | `CacheInterface` | Functional cache with call tracking |
**Usage:**
```go
mock := &EnhancedMockJWKCache{
JWKS: &JWKSet{Keys: []JWK{jwk}},
}
// Make calls
result, err := mock.GetJWKS(ctx, "https://example.com/jwks", nil)
// Verify calls were made
mock.AssertGetJWKSCalled(t)
mock.AssertGetJWKSCalledWith(t, "https://example.com/jwks")
mock.AssertGetJWKSCallCount(t, 1)
// Access call details
s.Equal(1, mock.GetJWKSCallCount())
```
**Features:**
- Track all calls with parameters and timestamps
- Built-in assertion helpers using testify
- Thread-safe for concurrent tests
- `Reset()` method to clear state between tests
- `LastCall()` to inspect most recent call
### Testify-Based Mocks
Located in `testify_mocks_test.go`. Mocks using testify's `.On()/.Return()` pattern for behavior verification.
| Mock | Interface | Description |
|------|-----------|-------------|
| `TestifyJWKCache` | `JWKCacheInterface` | Testify mock with `.On()/.Return()` |
| `TestifyTokenVerifier` | `TokenVerifier` | Testify mock for token verification |
| `TestifyTokenExchanger` | `TokenExchanger` | Testify mock for token exchange |
| `TestifyCacheInterface` | `CacheInterface` | Testify mock for cache operations |
| `TestifyHTTPClient` | N/A | Testify mock for HTTP client |
| `TestifyRoundTripper` | `http.RoundTripper` | Testify mock for HTTP transport |
**Usage:**
```go
mock := &TestifyJWKCache{}
mock.On("GetJWKS", mock.Anything, "https://example.com/jwks", mock.Anything).
Return(&JWKSet{Keys: []JWK{jwk}}, nil)
// After test
mock.AssertExpectations(t)
```
### Testutil Package Mocks
Located in `internal/testutil/mocks/`. Generic mocks for testing the test infrastructure itself.
```go
import "github.com/lukaszraczylo/traefikoidc/internal/testutil"
mock := testutil.NewJWKCacheMock()
mock.On("GetJWKS", mock.Anything, mock.Anything, mock.Anything).
Return(&mocks.JWKSet{Keys: []mocks.JWK{{Kty: "RSA"}}}, nil)
```
### Choosing the Right Mock
| Use Case | Recommended Mock |
|----------|-----------------|
| Simple return values only | Basic state-based (`MockJWKCache`) |
| Return values + verify calls made | Enhanced state-based (`EnhancedMockJWKCache`) |
| Complex call expectations | Testify-based (`TestifyJWKCache`) |
| Verify call order/sequence | Testify-based |
| HTTP endpoint simulation | `MockOAuthProvider` |
| New testify suite tests | Enhanced or Testify-based |
**Decision Guide:**
1. **Basic State-Based**: Use when you only need to control return values and don't care about verifying interactions.
2. **Enhanced State-Based**: Use when you want to verify calls were made with specific parameters, but prefer simpler setup than testify's `.On()/.Return()` pattern.
3. **Testify-Based**: Use when you need complex behavior like different returns per call, strict call ordering, or detailed expectation matching.
## Token Fixtures
The `testutil.TokenFixture` generates JWT tokens for testing:
```go
fixture, err := testutil.NewTokenFixture()
// Valid token with default claims
token, _ := fixture.ValidToken(nil)
// Token with custom claims
token, _ := fixture.ValidToken(map[string]interface{}{
"email": "test@example.com",
"roles": []string{"admin"},
})
// Expired token
token, _ := fixture.ExpiredToken()
// Token with specific roles/groups
token, _ := fixture.TokenWithRoles([]string{"admin", "user"})
token, _ := fixture.TokenWithGroups([]string{"developers"})
// Token with clock skew
token, _ := fixture.TokenWithSkew(-2 * time.Minute) // expired 2 min ago
token, _ := fixture.TokenWithSkew(5 * time.Minute) // expires in 5 min
// Token missing specific claims
token, _ := fixture.TokenMissingClaim("email", "sub")
// Malformed token
token := fixture.MalformedToken() // "not.a.valid.jwt"
// Get JWKS for verification
jwks := fixture.GetJWKS()
```
## Mock OIDC Server
The `testutil.OIDCServer` provides a fully functional mock OIDC provider:
```go
// Default configuration
server := testutil.NewOIDCServer(nil)
defer server.Close()
// Custom configuration
config := testutil.DefaultServerConfig()
config.Issuer = "https://custom-issuer.com"
config.TokenError = &testutil.OIDCError{
Error: "invalid_grant",
Description: "Authorization code expired",
}
server := testutil.NewOIDCServer(config)
// Provider-specific configurations
googleConfig := testutil.GoogleServerConfig()
azureConfig := testutil.AzureServerConfig()
auth0Config := testutil.Auth0ServerConfig()
keycloakConfig := testutil.KeycloakServerConfig()
// Behavior configurations
slowConfig := testutil.SlowServerConfig(100 * time.Millisecond)
rateLimitedConfig := testutil.RateLimitedServerConfig(5) // Limit after 5 requests
```
### Server Endpoints
| Endpoint | Description |
|----------|-------------|
| `/.well-known/openid-configuration` | OIDC discovery document |
| `/authorize` | Authorization endpoint |
| `/token` | Token exchange endpoint |
| `/jwks` | JSON Web Key Set |
| `/userinfo` | User information endpoint |
| `/introspect` | Token introspection |
| `/revoke` | Token revocation |
| `/logout` | End session endpoint |
### Request Tracking
```go
server := testutil.NewOIDCServer(nil)
// Make requests...
count := server.GetRequestCount()
requests := server.GetRequests()
server.Reset() // Clear tracking
```
## Writing Test Suites
### Basic Suite Structure
```go
type MyTestSuite struct {
suite.Suite
fixture *testutil.TokenFixture
tOidc *TraefikOidc
}
func (s *MyTestSuite) SetupSuite() {
var err error
s.fixture, err = testutil.NewTokenFixture()
s.Require().NoError(err)
}
func (s *MyTestSuite) SetupTest() {
// Per-test setup
s.tOidc = &TraefikOidc{
issuerURL: s.fixture.Issuer,
// ...
}
}
func (s *MyTestSuite) TearDownTest() {
// Per-test cleanup
}
func (s *MyTestSuite) TestSomething() {
token, err := s.fixture.ValidToken(nil)
s.Require().NoError(err)
err = s.tOidc.VerifyToken(token)
s.NoError(err)
}
func TestMyTestSuite(t *testing.T) {
suite.Run(t, new(MyTestSuite))
}
```
### Table-Driven Tests
```go
func (s *MyTestSuite) TestClockSkewEdgeCases() {
testCases := []struct {
name string
skew time.Duration
shouldPass bool
}{
{"valid_token", 5 * time.Minute, true},
{"expired_within_tolerance", -1 * time.Minute, true},
{"expired_beyond_tolerance", -10 * time.Minute, false},
}
for _, tc := range testCases {
s.Run(tc.name, func() {
token, err := s.fixture.TokenWithSkew(tc.skew)
s.Require().NoError(err)
err = s.tOidc.VerifyToken(token)
if tc.shouldPass {
s.NoError(err)
} else {
s.Error(err)
}
})
}
}
```
## Test Categories
### Happy Path Tests
Test the expected successful scenarios:
- Valid token verification
- Successful token exchange
- Session creation and retrieval
- Cache operations
### Error Case Tests
Test failure scenarios:
- Expired tokens
- Invalid signatures
- Wrong issuer/audience
- Network failures
- Rate limiting
### Edge Case Tests
Test boundary conditions:
- Clock skew tolerance boundaries
- Unicode/emoji in claims
- Very large claim values
- Concurrent access
- Special characters in URLs
## Best Practices
1. **Use fixtures for token generation** - Don't manually construct JWTs
2. **Use mock servers for integration tests** - Test against realistic OIDC behavior
3. **Always run with `-race`** - Catch concurrency issues early
4. **Use testify assertions** - Better error messages and cleaner code
5. **Clean up resources** - Use `t.Cleanup()` or `TearDownTest()`
6. **Test edge cases systematically** - Use table-driven tests
-308
View File
@@ -1,308 +0,0 @@
# Test Execution Guide
This guide explains how to run tests efficiently with the new test categorization and optimization system.
## Quick Start
### Fast Development Testing (Default - Target: < 30 seconds)
```bash
# Run quick smoke tests only
go test ./...
# Or explicitly run in short mode
go test ./... -short
```
### Extended Testing (Target: 2-5 minutes)
```bash
# Enable extended tests with more iterations and concurrency
RUN_EXTENDED_TESTS=1 go test ./...
# Or use the flag equivalent (if using test runner that supports it)
go test ./... -extended
```
### Long-Running Performance Tests (Target: 5-15 minutes)
```bash
# Enable comprehensive performance and stress tests
RUN_LONG_TESTS=1 go test ./...
```
### Full Stress Testing (Target: 10-30 minutes)
```bash
# Enable all stress tests with maximum parameters
RUN_STRESS_TESTS=1 go test ./...
```
## Test Categories
### 1. Quick Tests (Default)
- **Purpose**: Fast feedback during development
- **Duration**: < 30 seconds total
- **Features**:
- Basic functionality verification
- Limited iterations (1-3)
- Small data sets
- Minimal concurrency
- Essential memory leak checks
**Configuration**:
- Max Iterations: 3
- Max Concurrency: 5
- Memory Threshold: 2.0 MB
- Cache Size: 50
- Timeout: 10 seconds
### 2. Extended Tests
- **Purpose**: Comprehensive testing before commits
- **Duration**: 2-5 minutes
- **Features**:
- Increased test coverage
- More iterations (5-10)
- Medium concurrency tests
- Enhanced memory leak detection
**Configuration**:
- Max Iterations: 10
- Max Concurrency: 20
- Memory Threshold: 10.0 MB
- Cache Size: 200
- Timeout: 30 seconds
### 3. Long Tests
- **Purpose**: Performance validation and stress testing
- **Duration**: 5-15 minutes
- **Features**:
- High iteration counts (50-100)
- High concurrency scenarios
- Large data sets
- Comprehensive memory testing
**Configuration**:
- Max Iterations: 100
- Max Concurrency: 50
- Memory Threshold: 50.0 MB
- Cache Size: 1000
- Timeout: 60 seconds
### 4. Stress Tests
- **Purpose**: Maximum load testing and edge case validation
- **Duration**: 10-30 minutes
- **Features**:
- Extreme iteration counts (100-500)
- Maximum concurrency (100+)
- Large memory allocations
- Edge case combinations
**Configuration**:
- Max Iterations: 500
- Max Concurrency: 100
- Memory Threshold: 100.0 MB
- Cache Size: 2000
- Timeout: 120 seconds
## Environment Variables
### Test Execution Control
```bash
# Enable specific test types
export RUN_EXTENDED_TESTS=1 # Enable extended tests
export RUN_LONG_TESTS=1 # Enable long-running tests
export RUN_STRESS_TESTS=1 # Enable stress tests
# Disable specific features
export DISABLE_LEAK_DETECTION=1 # Skip memory leak detection
```
### Parameter Customization
```bash
# Customize concurrency limits
export TEST_MAX_CONCURRENCY=10 # Override max concurrent operations
# Customize iteration limits
export TEST_MAX_ITERATIONS=50 # Override max test iterations
# Customize memory thresholds
export TEST_MEMORY_THRESHOLD_MB=25.5 # Override memory growth limit (in MB)
```
## Test-Specific Behavior
### Memory Leak Tests
- **Quick Mode**: 1-3 iterations, small data sets, strict memory limits
- **Extended Mode**: 5-10 iterations, medium data sets, relaxed limits
- **Long Mode**: 50-100 iterations, large data sets, performance focus
- **Stress Mode**: 100-500 iterations, maximum data sets, stress focus
### Concurrency Tests
- **Quick Mode**: 2-5 concurrent operations, basic race detection
- **Extended Mode**: 10-20 concurrent operations, moderate stress
- **Long Mode**: 20-50 concurrent operations, high contention
- **Stress Mode**: 50-100+ concurrent operations, maximum stress
### Cache Tests
- **Quick Mode**: Small caches (50 items), basic operations
- **Extended Mode**: Medium caches (200 items), varied operations
- **Long Mode**: Large caches (1000 items), performance testing
- **Stress Mode**: Very large caches (2000+ items), stress testing
## Integration with CI/CD
### GitHub Actions Example
```yaml
# Quick tests for every push/PR
- name: Quick Tests
run: go test ./... -short
# Extended tests for main branch
- name: Extended Tests
if: github.ref == 'refs/heads/main'
run: RUN_EXTENDED_TESTS=1 go test ./...
# Nightly comprehensive testing
- name: Nightly Stress Tests
if: github.event_name == 'schedule'
run: RUN_STRESS_TESTS=1 go test ./...
```
### Local Development Workflow
```bash
# During active development
go test ./... -short
# Before committing
RUN_EXTENDED_TESTS=1 go test ./...
# Before major releases
RUN_LONG_TESTS=1 go test ./...
# Performance validation
RUN_STRESS_TESTS=1 go test ./...
```
## Performance Optimization Features
### Dynamic Test Scaling
The test system automatically adjusts parameters based on:
- Test mode (quick/extended/long/stress)
- Available resources
- Environment variables
- Previous test performance
### Memory Management
- **Garbage Collection**: Forced GC between test iterations
- **Memory Monitoring**: Real-time memory growth tracking
- **Leak Detection**: Goroutine and memory leak prevention
- **Resource Cleanup**: Automatic cleanup of test resources
### Timeout Management
- **Adaptive Timeouts**: Timeouts scale with test complexity
- **Graceful Degradation**: Tests adapt to slower environments
- **Early Termination**: Failed tests terminate quickly
## Troubleshooting
### Tests Taking Too Long
```bash
# Check if running in extended mode accidentally
echo $RUN_EXTENDED_TESTS $RUN_LONG_TESTS
# Force quick mode
unset RUN_EXTENDED_TESTS RUN_LONG_TESTS RUN_STRESS_TESTS
go test ./... -short
```
### Memory Issues
```bash
# Reduce memory limits for constrained environments
export TEST_MEMORY_THRESHOLD_MB=5.0
export TEST_MAX_CONCURRENCY=2
go test ./...
```
### Concurrency Issues
```bash
# Reduce concurrency for slower systems
export TEST_MAX_CONCURRENCY=5
export TEST_MAX_ITERATIONS=10
go test ./...
```
### Skip Specific Test Types
```bash
# Skip memory leak detection if problematic
export DISABLE_LEAK_DETECTION=1
go test ./...
```
## Benchmarking
### Running Benchmarks
```bash
# Quick benchmarks
go test -bench=. -short
# Extended benchmarks
RUN_EXTENDED_TESTS=1 go test -bench=.
# Memory profiling
go test -bench=. -memprofile=mem.prof
go tool pprof mem.prof
```
### Benchmark Categories
- **Basic Operations**: Set/Get performance
- **Concurrency**: Multi-threaded performance
- **Memory**: Allocation and cleanup performance
- **Cache**: Eviction and cleanup performance
## Best Practices
### For Developers
1. Always run quick tests during development (`go test ./... -short`)
2. Run extended tests before committing (`RUN_EXTENDED_TESTS=1 go test ./...`)
3. Use appropriate test categories for your use case
4. Monitor test execution time and adjust if needed
### For CI/CD
1. Use quick tests for fast feedback on PRs
2. Use extended tests for main branch validation
3. Use long tests for release validation
4. Use stress tests for nightly/weekly validation
### For Performance Testing
1. Use consistent environment variables
2. Run tests multiple times for statistical significance
3. Monitor both execution time and resource usage
4. Use profiling tools for detailed analysis
## Examples
### Daily Development
```bash
# Fast tests while coding
go test ./... -short
# Before git commit
RUN_EXTENDED_TESTS=1 go test ./...
```
### Release Testing
```bash
# Comprehensive validation
RUN_LONG_TESTS=1 go test ./...
# Stress testing
RUN_STRESS_TESTS=1 go test ./...
```
### Custom Configuration
```bash
# Custom limits for specific environment
export TEST_MAX_CONCURRENCY=8
export TEST_MAX_ITERATIONS=25
export TEST_MEMORY_THRESHOLD_MB=15.0
RUN_EXTENDED_TESTS=1 go test ./...
```
This test system provides flexible, scalable test execution that adapts to your development workflow and infrastructure constraints while maintaining comprehensive test coverage.
-163
View File
@@ -1,163 +0,0 @@
# Google OAuth Integration Fix
## Problem Overview
The Traefik OIDC plugin encountered an authentication issue when using Google as an OAuth provider. Authentication would fail with the following error:
```
Some requested scopes were invalid. {valid=[openid, https://www.googleapis.com/auth/userinfo.email, https://www.googleapis.com/auth/userinfo.profile], invalid=[offline_access]}
```
This occurred because Google's OAuth implementation differs from the standard OIDC specification in how it handles refresh tokens and offline access.
## Technical Details of the Issue
### Standard OIDC Provider Behavior
Most OpenID Connect (OIDC) providers follow the standard specification, where:
- To obtain a refresh token, clients include the `offline_access` scope in their authorization request
- This allows authenticated sessions to persist beyond the initial access token expiration
### Google's Non-Standard Approach
Google's OAuth implementation deviates from the standard by:
1. Not supporting the `offline_access` scope, instead rejecting it as an invalid scope
2. Requiring the `access_type=offline` query parameter for requesting refresh tokens
3. Needing the `prompt=consent` parameter to consistently issue refresh tokens (especially for repeat authentications)
This difference caused the plugin to fail when configured for Google OAuth, as it was using a standard approach that didn't work with Google's implementation.
## Solution Implementation
The fix involved modifying the authentication flow to specifically handle Google providers:
1. **Google Provider Detection**: Added code to detect if the OIDC provider is Google based on the issuer URL:
```go
// Check if we're dealing with a Google OIDC provider
isGoogleProvider := strings.Contains(t.issuerURL, "google") ||
strings.Contains(t.issuerURL, "accounts.google.com")
```
2. **Provider-Specific Auth URL Building**: Modified the `buildAuthURL` function to handle Google and non-Google providers differently:
```go
// Handle offline access differently for Google vs other providers
if isGoogleProvider {
// For Google, use access_type=offline parameter instead of offline_access scope
params.Set("access_type", "offline")
t.logger.Debug("Google OIDC provider detected, added access_type=offline for refresh tokens")
// Add prompt=consent for Google to ensure refresh token is issued
params.Set("prompt", "consent")
t.logger.Debug("Google OIDC provider detected, added prompt=consent to ensure refresh tokens")
} else {
// For non-Google providers, use the offline_access scope
hasOfflineAccess := false
for _, scope := range scopes {
if scope == "offline_access" {
hasOfflineAccess = true
break
}
}
if !hasOfflineAccess {
scopes = append(scopes, "offline_access")
}
}
```
3. **Token Refresh Enhancement**: Improved the token refresh logic to better handle Google's behavior, particularly when refresh tokens aren't returned in refresh responses (as Google often uses the same refresh token for multiple requests).
## Why This Approach Works
This solution aligns with Google's OAuth 2.0 documentation which specifies:
1. **Access Type Parameter**: Google's [OAuth 2.0 documentation](https://developers.google.com/identity/protocols/oauth2/web-server#offline) states that to request a refresh token, applications must include `access_type=offline` in the authorization request.
2. **Prompt Parameter**: The [`prompt=consent`](https://developers.google.com/identity/protocols/oauth2/web-server#forceapprovalprompt) parameter forces the consent screen to appear, ensuring a refresh token is issued even if the user has previously granted access.
3. **Scope Validation**: Google strictly validates scopes and rejects non-standard ones like `offline_access`, instead relying on the `access_type` parameter to indicate whether a refresh token should be issued.
By adapting to these Google-specific requirements, the OIDC plugin can now seamlessly work with both standard OIDC providers and Google's OAuth implementation.
## Testing and Verification
Comprehensive tests were implemented to verify the solution:
1. **Provider Detection Test**: Ensures the code correctly identifies Google providers and applies the appropriate parameters.
2. **Auth URL Parameter Tests**: Verifies that:
- For Google providers: `access_type=offline` and `prompt=consent` are included; `offline_access` scope is NOT included
- For non-Google providers: `offline_access` scope IS included; `access_type` parameter is NOT added
3. **Token Refresh Tests**: Validates that Google's token refresh process works correctly, including the preservation of refresh tokens when Google doesn't return a new one.
4. **Integration Test**: Tests the complete authentication flow with a mocked Google provider to ensure all components work together seamlessly.
Sample test case (simplified):
```go
t.Run("Google provider detection adds required parameters", func(t *testing.T) {
// Test buildAuthURL to ensure it adds access_type=offline and prompt=consent for Google
authURL := tOidc.buildAuthURL("https://example.com/callback", "state123", "nonce123", "")
// Check that access_type=offline was added (not offline_access scope for Google)
if !strings.Contains(authURL, "access_type=offline") {
t.Errorf("access_type=offline not added to Google auth URL: %s", authURL)
}
// Verify offline_access scope is NOT included for Google providers
if strings.Contains(authURL, "offline_access") {
t.Errorf("offline_access scope incorrectly added to Google auth URL: %s", authURL)
}
// Check that prompt=consent was added
if !strings.Contains(authURL, "prompt=consent") {
t.Errorf("prompt=consent not added to Google auth URL: %s", authURL)
}
})
```
## Usage Guidance for Developers
When configuring the Traefik OIDC middleware for Google:
1. **Provider URL**: Use `https://accounts.google.com` as the `providerURL` value
2. **Client Configuration**: Create OAuth 2.0 credentials in the Google Cloud Console:
- Configure the authorized redirect URI to match your `callbackURL` setting
- Ensure your OAuth consent screen is properly configured (especially if you want long-lived refresh tokens)
3. **Configuration Example**:
```yaml
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: oidc-google
namespace: traefik
spec:
plugin:
traefikoidc:
providerURL: https://accounts.google.com
clientID: your-google-client-id.apps.googleusercontent.com
clientSecret: your-google-client-secret
sessionEncryptionKey: your-secure-encryption-key-min-32-chars
callbackURL: /oauth2/callback
scopes:
- openid
- email
- profile
# Note: DO NOT manually add offline_access scope for Google
# The middleware handles this automatically and correctly
```
4. **Troubleshooting**: If sessions still expire prematurely with Google (typically after 1 hour):
- Ensure your Google Cloud OAuth consent screen is set to "External" and "Production" mode (not "Testing" mode, which limits refresh token validity)
- Review your application logs with `logLevel: debug` to check for refresh token errors
- Verify you're using a version of the middleware that includes this fix
## Conclusion
This fix ensures that the Traefik OIDC plugin works seamlessly with Google's OAuth implementation without requiring users to make provider-specific configuration changes. The middleware now intelligently adapts to the provider's requirements, making it more robust and user-friendly while maintaining compatibility with the standard OIDC specification for other providers.