* Fix bug affecting Azure OIDC authentication ( and most likely others ) * Fixes issue #51 * Ensure that appended roles are unique. Update the documentation. * Improvements targetting possible memory usage spikes. * Additional fixes and cleanup * Refactoring code to fix the issues identified by the users. * Modernize run * Fieldalignment * Multiple changes to improve performance and reduce complexity. - Optimise the errors and recovery. - Deduplicate code in metadata cache. - Remove unused performance monitoring code. - Simplify session management and settings handling. * Fix claims issue. * Add ability to overwrite the default scopes in the settings file * Well.. that escalated quickly. Completely forgot that Traefik uses outdated Yaegi and requires compatibility with 1.20 ( pre-generic Go code ). * Bugfix #51: Ensures that user provided scopes overrides work. * fixup! Bugfix #51: Ensures that user provided scopes overrides work. * fixup! fixup! Bugfix #51: Ensures that user provided scopes overrides work. * Abstract the provider logic into a separate package. * Additional micro fixes and cleanups. * Simplify all the things. * fixup! Simplify all the things. * fixup! fixup! Simplify all the things. * fixup! fixup! fixup! Simplify all the things. * fixup! fixup! fixup! fixup! Simplify all the things. * ... * Cleanup tests. * fixup! Cleanup tests. * fixup! fixup! fixup! Cleanup tests. * fixup! fixup! fixup! fixup! Cleanup tests. * fixup! fixup! fixup! fixup! fixup! Cleanup tests. * Issue #53: Fix CSRF token handling in reverse proxy 1. ✅ HTTPS Detection Fixed (session.go:723) - Now uses X-Forwarded-Proto header instead of r.URL.Scheme - Properly detects HTTPS in reverse proxy environments 2. ✅ SameSite Cookie Attribute Fixed - Removed automatic SameSiteStrictMode for HTTPS (would break OAuth) - Keeps SameSiteLaxMode to allow OAuth callbacks from external domains - Only uses Strict for AJAX requests which don't involve OAuth redirects 3. ✅ Cookie Domain Handling Fixed - Now respects X-Forwarded-Host header for cookie domain - Ensures cookies are set for the public domain, not internal proxy domain 4. ✅ EnhanceSessionSecurity Properly Integrated - Function is now actually called during session save - Applies security enhancements without breaking OAuth flow Why Issue #53 Failed Before: 1. Cookies were not marked Secure in HTTPS environments (browser wouldn't send them back) 2. If they had been Secure with SameSite=Strict, Azure callbacks would still fail 3. Cookie domain might have been wrong (internal vs public domain) Why It Works Now: 1. Cookies are properly marked Secure for HTTPS 2. Uses SameSite=Lax to allow OAuth provider callbacks 3. Cookie domain uses public domain from X-Forwarded-Host 4. CSRF token persists through the entire OAuth flow * Next set of enhancements together with memory usage improvements. * Memory leak fixes and optimisations. * CSRF and Cookie Domain fixes * fixup! CSRF and Cookie Domain fixes * Metadata cache leak fix + profiling * fixup! Metadata cache leak fix + profiling * Memory leaks hunting, part 1337. * Further pursue of perfection. * fixup! Further pursue of perfection. * fixup! fixup! Further pursue of perfection. * fixup! fixup! fixup! Further pursue of perfection. * fixup! fixup! fixup! fixup! Further pursue of perfection. * fixup! fixup! fixup! fixup! fixup! Further pursue of perfection. * fixup! fixup! fixup! fixup! fixup! fixup! Further pursue of perfection. * fixup! fixup! fixup! fixup! fixup! fixup! fixup! Further pursue of perfection. * fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! Further pursue of perfection. * fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! Further pursue of perfection. * Clear race conditions * fixup! Clear race conditions * Weekend fun with memory leaks * Splitting code into multiple files with reasonable testing coverage. ``` ok github.com/lukaszraczylo/traefikoidc 117.017s coverage: 72.6% of statements ok github.com/lukaszraczylo/traefikoidc/auth 0.505s coverage: 87.1% of statements ok github.com/lukaszraczylo/traefikoidc/circuit_breaker 0.283s coverage: 99.0% of statements github.com/lukaszraczylo/traefikoidc/config coverage: 0.0% of statements ok github.com/lukaszraczylo/traefikoidc/handlers 0.349s coverage: 98.2% of statements ok github.com/lukaszraczylo/traefikoidc/internal/providers (cached) coverage: 94.3% of statements ok github.com/lukaszraczylo/traefikoidc/middleware 0.808s coverage: 78.0% of statements ok github.com/lukaszraczylo/traefikoidc/recovery 0.653s coverage: 100.0% of statements ok github.com/lukaszraczylo/traefikoidc/session/chunking (cached) coverage: 87.8% of statements ok github.com/lukaszraczylo/traefikoidc/session/core (cached) coverage: 85.6% of statements ok github.com/lukaszraczylo/traefikoidc/session/crypto (cached) coverage: 81.8% of statements ok github.com/lukaszraczylo/traefikoidc/session/storage (cached) coverage: 93.5% of statements ok github.com/lukaszraczylo/traefikoidc/session/validators (cached) coverage: 98.8% of statements ```` * fixup! Splitting code into multiple files with reasonable testing coverage. * fixup! fixup! Splitting code into multiple files with reasonable testing coverage. * Weekend fun with further optimisations. * fixup! Weekend fun with further optimisations. * fixup! fixup! Weekend fun with further optimisations. * fixup! fixup! fixup! Weekend fun with further optimisations. * fixup! fixup! fixup! fixup! Weekend fun with further optimisations. * fixup! fixup! fixup! fixup! fixup! Weekend fun with further optimisations. * Pre-release cleanup. * Enhance test coverage. * fixup! Enhance test coverage. * fixup! fixup! Enhance test coverage. * fixup! fixup! fixup! Enhance test coverage.
41 KiB
Traefik OIDC Middleware
This middleware replaces the need for forward-auth and oauth2-proxy when using Traefik as a reverse proxy to support OpenID Connect (OIDC) authentication.
Overview
The Traefik OIDC middleware provides a complete OIDC authentication solution with features like:
- Token validation and verification
- Session management with automatic cleanup
- Domain restrictions
- Role-based access control
- Token caching and blacklisting
- Rate limiting
- Excluded paths (public URLs)
- Memory-efficient operation with bounded resource usage
Important Note on Token Validation: This middleware performs authentication and claim extraction based on the ID Token provided by the OIDC provider. It does not primarily use the Access Token for these purposes (though the Access Token is available for templated headers if needed). Therefore, ensure that all necessary claims (e.g., email, roles, custom attributes) are included in the ID Token by your OIDC provider's configuration.
The middleware has been tested with Auth0, Logto, Google and other standard OIDC providers. It includes special handling for Google's OAuth implementation.
Performance and Memory Management
This middleware includes advanced memory management features to ensure stable operation under high load:
- Bounded caches: All internal caches (metadata, sessions, tokens) have configurable size limits with LRU eviction
- Automatic cleanup: Background goroutines periodically clean up expired sessions and tokens
- Memory monitoring: Built-in memory leak detection and prevention
- Graceful degradation: Continues operating safely even under memory pressure
- Zero goroutine leaks: All background tasks are properly managed and terminated on shutdown
Traefik Version Compatibility
This middleware follows closely the current Traefik helm chart versions. If the plugin fails to load, it's time to update to the latest version of the Traefik helm chart.
Installation
As a Traefik Plugin
- Enable the plugin in your Traefik static configuration:
# traefik.yml
experimental:
plugins:
traefikoidc:
moduleName: github.com/lukaszraczylo/traefikoidc
version: v0.2.1 # Use the latest version
- Configure the middleware in your dynamic configuration (see examples below).
Local Development with Docker Compose
For local development or testing, you can use the provided Docker Compose setup:
cd docker
docker-compose up -d
This will start Traefik with the OIDC middleware and two test services.
Configuration Options
The middleware supports the following configuration options:
Required Parameters
| Parameter | Description | Example |
|---|---|---|
providerURL |
The base URL of the OIDC provider | https://accounts.google.com |
clientID |
The OAuth 2.0 client identifier | 1234567890.apps.googleusercontent.com |
clientSecret |
The OAuth 2.0 client secret | your-client-secret |
sessionEncryptionKey |
Key used to encrypt session data (must be at least 32 bytes long) | potato-secret-is-at-least-32-bytes-long |
callbackURL |
The path where the OIDC provider will redirect after authentication | /oauth2/callback |
Optional Parameters
| Parameter | Description | Default | Example |
|---|---|---|---|
logoutURL |
The path for handling logout requests | callbackURL + "/logout" |
/oauth2/logout |
postLogoutRedirectURI |
The URL to redirect to after logout | / |
/logged-out-page |
scopes |
OAuth 2.0 scopes to use for authentication | ["openid", "profile", "email"] (always included by default) |
["roles", "custom_scope"] (appended to defaults) |
overrideScopes |
When true, replaces default scopes with provided scopes instead of appending | false |
true (use only the scopes explicitly provided) |
logLevel |
Sets the logging verbosity | info |
debug, info, error |
forceHTTPS |
Forces the use of HTTPS for all URLs | true |
true, false |
rateLimit |
Sets the maximum number of requests per second | 100 |
500 |
excludedURLs |
Lists paths that bypass authentication | none | ["/health", "/metrics", "/public"] |
allowedUserDomains |
Restricts access to specific email domains | none | ["company.com", "subsidiary.com"] |
allowedUsers |
A list of specific email addresses that are allowed access | none | ["user1@example.com", "user2@another.org"] |
allowedRolesAndGroups |
Restricts access to users with specific roles or groups | none | ["admin", "developer"] |
revocationURL |
The endpoint for revoking tokens | auto-discovered | https://accounts.google.com/revoke |
oidcEndSessionURL |
The provider's end session endpoint | auto-discovered | https://accounts.google.com/logout |
enablePKCE |
Enables PKCE (Proof Key for Code Exchange) for authorization code flow | false |
true, false |
refreshGracePeriodSeconds |
Seconds before token expiry to attempt proactive refresh | 60 |
120 |
cookieDomain |
Explicit domain for session cookies (important for multi-subdomain setups) | auto-detected | .example.com, app.example.com |
headers |
Custom HTTP headers with templates that can access OIDC claims and tokens | none | See "Templated Headers" section |
Scope Configuration
Scope Behavior
The middleware supports two modes for handling OAuth 2.0 scopes, controlled by the overrideScopes parameter:
Default Append Mode (overrideScopes: false)
By default, the middleware uses an append behavior for OAuth 2.0 scopes:
- Default scopes are always included:
["openid", "profile", "email"] - User-provided scopes are appended to the defaults with automatic deduplication
- The final scope list maintains the order: defaults first, then user scopes
Override Mode (overrideScopes: true)
When overrideScopes is set to true, the middleware uses replacement behavior:
- Default scopes are not automatically included
- Only the scopes explicitly provided in the
scopesfield are used - You must include all required scopes explicitly, including
openidif needed
Examples:
Default behavior (no custom scopes):
# No scopes field specified
# Result: ["openid", "profile", "email"]
Default append behavior:
scopes:
- roles
- custom_scope
# Result: ["openid", "profile", "email", "roles", "custom_scope"]
Overlapping scopes with append (automatic deduplication):
scopes:
- openid # Duplicate - will be deduplicated
- roles
- profile # Duplicate - will be deduplicated
- permissions
# Result: ["openid", "profile", "email", "roles", "permissions"]
Using override mode:
overrideScopes: true
scopes:
- openid
- profile
- custom_scope
# Result: ["openid", "profile", "custom_scope"]
Empty scopes list with default behavior:
scopes: []
# Result: ["openid", "profile", "email"]
Empty scopes list with override mode:
overrideScopes: true
scopes: []
# Result: [] (Warning: empty scopes may cause authentication to fail)
The default append behavior ensures essential OIDC scopes are always present, while the override mode gives you complete control over the exact scopes requested from the provider.
Usage Examples
Basic Configuration
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: oidc-basic
namespace: traefik
spec:
plugin:
traefikoidc:
providerURL: https://accounts.google.com
clientID: 1234567890.apps.googleusercontent.com
clientSecret: your-client-secret
sessionEncryptionKey: potato-secret-is-at-least-32-bytes-long
callbackURL: /oauth2/callback
logoutURL: /oauth2/logout
scopes:
- roles # Appended to defaults: ["openid", "profile", "email", "roles"]
With Excluded URLs (Public Access Paths)
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: oidc-with-open-urls
namespace: traefik
spec:
plugin:
traefikoidc:
providerURL: https://accounts.google.com
clientID: 1234567890.apps.googleusercontent.com
clientSecret: your-client-secret
sessionEncryptionKey: potato-secret-is-at-least-32-bytes-long
callbackURL: /oauth2/callback
logoutURL: /oauth2/logout
scopes:
- roles # Appended to defaults: ["openid", "profile", "email", "roles"]
excludedURLs:
- /login # covers /login, /login/me, /login/reminder etc.
- /public-data
- /health
- /metrics
With Email Domain Restrictions
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: oidc-domain-restricted
namespace: traefik
spec:
plugin:
traefikoidc:
providerURL: https://accounts.google.com
clientID: 1234567890.apps.googleusercontent.com
clientSecret: your-client-secret
sessionEncryptionKey: potato-secret-is-at-least-32-bytes-long
callbackURL: /oauth2/callback
logoutURL: /oauth2/logout
scopes:
- roles # Appended to defaults: ["openid", "profile", "email", "roles"]
allowedUserDomains:
- company.com
- subsidiary.com
With Specific User Access
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: oidc-specific-users
namespace: traefik
spec:
plugin:
traefikoidc:
providerURL: https://accounts.google.com
clientID: 1234567890.apps.googleusercontent.com
clientSecret: your-client-secret
sessionEncryptionKey: potato-secret-is-at-least-32-bytes-long
callbackURL: /oauth2/callback
logoutURL: /oauth2/logout
scopes:
- roles # Appended to defaults: ["openid", "profile", "email", "roles"]
allowedUsers:
- user1@example.com
- user2@another.org
With Both Domain and Specific User Access
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: oidc-domain-and-users
namespace: traefik
spec:
plugin:
traefikoidc:
providerURL: https://accounts.google.com
clientID: 1234567890.apps.googleusercontent.com
clientSecret: your-client-secret
sessionEncryptionKey: potato-secret-is-at-least-32-bytes-long
callbackURL: /oauth2/callback
logoutURL: /oauth2/logout
scopes:
- roles # Appended to defaults: ["openid", "profile", "email", "roles"]
allowedUserDomains:
- company.com
allowedUsers:
- special-user@gmail.com
- contractor@external.org
When configuring access control:
- If only
allowedUsersis set, only the specified email addresses will be granted access - If only
allowedUserDomainsis set, only users with email addresses from those domains will be granted access - If both are set, access is granted if the user's email is in
allowedUsersOR their email's domain is inallowedUserDomains - If neither is set, any authenticated user will be granted access
- Email matching is case-insensitive
With Role-Based Access Control
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: oidc-rbac
namespace: traefik
spec:
plugin:
traefikoidc:
providerURL: https://accounts.google.com
clientID: 1234567890.apps.googleusercontent.com
clientSecret: your-client-secret
sessionEncryptionKey: potato-secret-is-at-least-32-bytes-long
callbackURL: /oauth2/callback
logoutURL: /oauth2/logout
scopes:
- roles # Appended to defaults: ["openid", "profile", "email", "roles"]
allowedRolesAndGroups:
- admin
- developer
With Cookie Domain Configuration (Multi-Subdomain Setup)
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: oidc-multi-subdomain
namespace: traefik
spec:
plugin:
traefikoidc:
providerURL: https://accounts.google.com
clientID: 1234567890.apps.googleusercontent.com
clientSecret: your-client-secret
sessionEncryptionKey: potato-secret-is-at-least-32-bytes-long
callbackURL: /oauth2/callback
logoutURL: /oauth2/logout
cookieDomain: .example.com # Allows cookies to be shared across all subdomains
scopes:
- roles # Appended to defaults: ["openid", "profile", "email", "roles"]
Important: The cookieDomain parameter is crucial when running behind a reverse proxy or when your application serves multiple subdomains. Without it, cookies may be created with inconsistent domains, leading to authentication issues like "CSRF token missing in session" errors.
With Custom Logging and Rate Limiting
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: oidc-custom-settings
namespace: traefik
spec:
plugin:
traefikoidc:
providerURL: https://accounts.google.com
clientID: 1234567890.apps.googleusercontent.com
clientSecret: your-client-secret
sessionEncryptionKey: potato-secret-is-at-least-32-bytes-long
callbackURL: /oauth2/callback
logoutURL: /oauth2/logout
logLevel: debug # Options: debug, info, error (default: info)
rateLimit: 500 # Requests per second (default: 100)
forceHTTPS: false # Default is true for security
scopes:
- roles # Appended to defaults: ["openid", "profile", "email", "roles"]
With Custom Post-Logout Redirect
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: oidc-custom-logout
namespace: traefik
spec:
plugin:
traefikoidc:
providerURL: https://accounts.google.com
clientID: 1234567890.apps.googleusercontent.com
clientSecret: your-client-secret
sessionEncryptionKey: potato-secret-is-at-least-32-bytes-long
callbackURL: /oauth2/callback
logoutURL: /oauth2/logout
postLogoutRedirectURI: /logged-out-page # Where to redirect after logout
scopes:
- roles # Appended to defaults: ["openid", "profile", "email", "roles"]
With Templated Headers
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: oidc-with-headers
namespace: traefik
spec:
plugin:
traefikoidc:
providerURL: https://accounts.google.com
clientID: 1234567890.apps.googleusercontent.com
clientSecret: your-client-secret
sessionEncryptionKey: potato-secret-is-at-least-32-bytes-long
callbackURL: /oauth2/callback
logoutURL: /oauth2/logout
scopes:
- roles # Appended to defaults: ["openid", "profile", "email", "roles"]
headers:
# Using double curly braces to escape template expressions
- name: "X-User-Email"
value: "{{{{.Claims.email}}}}"
- name: "X-User-ID"
value: "{{{{.Claims.sub}}}}"
- name: "Authorization"
value: "Bearer {{{{.AccessToken}}}}"
- name: "X-User-Roles"
value: "{{{{range $i, $e := .Claims.roles}}}}{{{{if $i}}}},{{{{end}}}}{{{{$e}}}}{{{{end}}}}"
- name: "X-Is-Admin"
value: "{{{{if eq .Claims.role \"admin\"}}}}true{{{{else}}}}false{{{{end}}}}"
With PKCE Enabled
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: oidc-with-pkce
namespace: traefik
spec:
plugin:
traefikoidc:
providerURL: https://accounts.google.com
clientID: 1234567890.apps.googleusercontent.com
clientSecret: your-client-secret
sessionEncryptionKey: potato-secret-is-at-least-32-bytes-long
callbackURL: /oauth2/callback
logoutURL: /oauth2/logout
enablePKCE: true # Enables PKCE for added security
scopes:
- roles # Appended to defaults: ["openid", "profile", "email", "roles"]
Google OIDC Configuration Example
This example shows a configuration specifically tailored for Google OIDC:
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 # Replace with your Client ID
clientSecret: your-google-client-secret # Replace with your Client Secret
sessionEncryptionKey: your-secure-encryption-key-min-32-chars # Replace with your key
callbackURL: /oauth2/callback # Adjust if needed
logoutURL: /oauth2/logout # Optional: Adjust if needed
scopes:
- roles # Appended to defaults: ["openid", "profile", "email", "roles"]
# Note: DO NOT manually add offline_access scope for Google
# The middleware automatically handles Google-specific requirements
refreshGracePeriodSeconds: 300 # Optional: Start refresh 5 min before expiry (default 60)
# Other optional parameters like allowedUserDomains, etc. can be added here
The middleware automatically detects Google as the provider and applies the necessary adjustments to ensure proper authentication and token refresh. See the Google OAuth Fix section for details.
Keeping Secrets Secret in Kubernetes
For Kubernetes environments, you can reference secrets instead of hardcoding sensitive values:
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: oidc-with-secrets
namespace: traefik
spec:
plugin:
traefikoidc:
providerURL: urn:k8s:secret:traefik-middleware-oidc:ISSUER
clientID: urn:k8s:secret:traefik-middleware-oidc:CLIENT_ID
clientSecret: urn:k8s:secret:traefik-middleware-oidc:SECRET
sessionEncryptionKey: potato-secret-is-at-least-32-bytes-long
callbackURL: /oauth2/callback
logoutURL: /oauth2/logout
scopes:
- roles # Appended to defaults: ["openid", "profile", "email", "roles"]
Don't forget to create the secret:
kubectl create secret generic traefik-middleware-oidc \
--from-literal=ISSUER=https://accounts.google.com \
--from-literal=CLIENT_ID=1234567890.apps.googleusercontent.com \
--from-literal=SECRET=your-client-secret \
-n traefik
Complete Docker Compose Example
Here's a complete example of using the middleware with Docker Compose:
version: "3.7"
services:
traefik:
image: traefik:v3.2.1
command:
- "--experimental.plugins.traefikoidc.modulename=github.com/lukaszraczylo/traefikoidc"
- "--experimental.plugins.traefikoidc.version=v0.2.1"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./traefik-config/traefik.yml:/etc/traefik/traefik.yml
- ./traefik-config/dynamic-configuration.yml:/etc/traefik/dynamic-configuration.yml
labels:
- "traefik.http.routers.dash.rule=Host(`dash.localhost`)"
- "traefik.http.routers.dash.service=api@internal"
ports:
- "80:80"
hello:
image: containous/whoami
labels:
- traefik.enable=true
- traefik.http.routers.hello.entrypoints=http
- traefik.http.routers.hello.rule=Host(`hello.localhost`)
- traefik.http.services.hello.loadbalancer.server.port=80
- traefik.http.routers.hello.middlewares=my-plugin@file
whoami:
image: jwilder/whoami
labels:
- traefik.enable=true
- traefik.http.routers.whoami.entrypoints=http
- traefik.http.routers.whoami.rule=Host(`whoami.localhost`)
- traefik.http.services.whoami.loadbalancer.server.port=8000
- traefik.http.routers.whoami.middlewares=my-plugin@file
traefik-config/traefik.yml:
log:
level: INFO
experimental:
localPlugins:
traefikoidc:
moduleName: github.com/lukaszraczylo/traefikoidc
# API and dashboard configuration
api:
dashboard: true
insecure: true
entryPoints:
http:
address: ":80"
forwardedHeaders:
insecure: true
providers:
docker:
endpoint: "unix:///var/run/docker.sock"
exposedByDefault: false
file:
filename: /etc/traefik/dynamic-configuration.yml
traefik-config/dynamic-configuration.yml:
http:
middlewares:
my-plugin:
plugin:
traefikoidc:
providerURL: https://accounts.google.com
clientID: 1234567890.apps.googleusercontent.com
clientSecret: your-client-secret
callbackURL: /oauth2/callback
logoutURL: /oauth2/logout
postLogoutRedirectURI: /logged-out-page
sessionEncryptionKey: potato-secret-is-at-least-32-bytes-long
scopes:
- roles # Appended to defaults: ["openid", "profile", "email", "roles"]
allowedUserDomains:
- company.com
allowedUsers:
- special-user@gmail.com
- contractor@external.org
allowedRolesAndGroups:
- admin
- developer
forceHTTPS: false
logLevel: debug
rateLimit: 100
excludedURLs:
- /login
- /public
- /health
- /metrics
headers:
# Using YAML literal style to prevent Traefik from pre-evaluating templates
- name: "X-User-Email"
value: |
{{.Claims.email}}
- name: "X-User-ID"
value: |
{{.Claims.sub}}
- name: "Authorization"
value: |
Bearer {{.AccessToken}}
- name: "X-User-Roles"
value: |
{{range $i, $e := .Claims.roles}}{{if $i}},{{end}}{{$e}}{{end}}
Advanced Configuration
Session Management
The middleware uses encrypted cookies to manage user sessions. The sessionEncryptionKey must be at least 32 bytes long and should be kept secret.
PKCE Support
The middleware supports PKCE (Proof Key for Code Exchange), which is an extension to the authorization code flow to prevent authorization code interception attacks. When enabled via the enablePKCE option, the middleware will generate a code verifier for each authentication request and derive a code challenge from it. The code verifier is stored in the user's session and sent during the token exchange process.
PKCE is recommended when:
- Your OIDC provider supports it (most modern providers do)
- You need an additional layer of security for the authorization code flow
- You're concerned about potential authorization code interception attacks
Note that not all OIDC providers support PKCE, so check your provider's documentation before enabling this feature.
Session Duration and Token Refresh
This middleware aims to provide long-lived user sessions, typically up to 24 hours, by utilizing OIDC refresh tokens.
How it works:
- When a user authenticates, the middleware requests an access token and, if available, a refresh token from the OIDC provider.
- The access token usually has a short lifespan (e.g., 1 hour).
- Before the access token expires (controlled by
refreshGracePeriodSeconds), the middleware uses the refresh token to obtain a new access token from the provider without requiring the user to log in again. - This process repeats, allowing the session to remain valid for as long as the refresh token is valid (often 24 hours or more, depending on the provider).
Provider-Specific Considerations (e.g., Google):
- Some providers, like Google, issue short-lived access tokens (e.g., 1 hour) and require specific configurations for long-term sessions.
- To enable session extension beyond the initial token expiry with Google and similar providers, the middleware automatically includes the
offline_accessscope in the authentication request. This scope is necessary to obtain a refresh token. - For Google specifically, the middleware also adds the
prompt=consentparameter to the initial authorization request. This ensures Google issues a refresh token, which is crucial for extending the session. - If a refresh attempt fails (e.g., the refresh token is revoked or expired), the user will be required to re-authenticate. The middleware includes enhanced error handling and logging for these scenarios.
- Ensure your OIDC provider is configured to issue refresh tokens and allows their use for extending sessions. Check your provider's documentation for details on refresh token validity periods.
Google OAuth Compatibility Fix
The middleware includes a specific fix for Google's OAuth implementation, which differs from the standard OIDC specification in how it handles refresh tokens:
-
Issue: Google does not support the standard
offline_accessscope for requesting refresh tokens and instead requires special parameters. -
Automatic Solution: The middleware detects Google as the provider based on the issuer URL and:
- Uses
access_type=offlinequery parameter instead of theoffline_accessscope - Adds
prompt=consentto ensure refresh tokens are consistently issued - Properly handles token refresh with Google's implementation
- Uses
You do not need any special configuration to use Google OAuth - just set providerURL to https://accounts.google.com and the middleware will automatically apply the proper parameters.
For detailed information on the Google OAuth fix, see the dedicated documentation.
Token Caching and Blacklisting
The middleware automatically caches validated tokens to improve performance and maintains a blacklist of revoked tokens.
Templated Headers
The middleware supports setting custom HTTP headers with values templated from OIDC claims and tokens. This allows you to pass authentication information to downstream services in a flexible, customized format.
Templates can access the following variables:
{{.Claims.field}}- Access individual claims from the ID token (e.g.,{{.Claims.email}},{{.Claims.sub}}){{.AccessToken}}- The raw access token string{{.IdToken}}- The raw ID token string (same as AccessToken in most configurations){{.RefreshToken}}- The raw refresh token string
⚠️ Important: Template Escaping
If you encounter the error can't evaluate field AccessToken in type bool when starting Traefik, this indicates that Traefik is attempting to evaluate the template expressions before passing them to the plugin. This is a known issue when using template syntax in Traefik plugin configurations.
Solution: You must escape the template expressions using double curly braces:
headers:
- name: "Authorization"
value: "Bearer {{{{.AccessToken}}}}"
This is the only reliable method that works consistently. Here's why:
-
Double curly braces (
{{{{.AccessToken}}}}) ✅- The YAML parser converts
{{{{→{{and}}}}→}} - Result:
Bearer {{.AccessToken}}reaches the Go template engine correctly
- The YAML parser converts
-
Other methods (YAML literal style, single quotes) do NOT work ❌
- These methods don't prevent Traefik's YAML parser from interpreting the curly braces
- The template syntax gets processed incorrectly before reaching the plugin
Working example configuration:
headers:
- name: "X-User-Email"
value: "{{{{.Claims.email}}}}"
- name: "X-User-ID"
value: "{{{{.Claims.sub}}}}"
- name: "Authorization"
value: "Bearer {{{{.AccessToken}}}}"
- name: "X-User-Name"
value: "{{{{.Claims.given_name}}}} {{{{.Claims.family_name}}}}"
Advanced template examples:
Conditional logic:
headers:
- name: "X-Is-Admin"
value: "{{{{if eq .Claims.role \"admin\"}}}}true{{{{else}}}}false{{{{end}}}}"
Array handling:
headers:
- name: "X-User-Roles"
value: "{{{{range $i, $e := .Claims.roles}}}}{{{{if $i}}}},{{{{end}}}}{{{{$e}}}}{{{{end}}}}"
Notes:
- Variable names are case-sensitive (use
.Claims, not.claims) - Missing claims will result in
<no value>in the header value - The middleware validates templates during startup and logs errors for invalid templates
- Always use double curly braces (
{{{{and}}}}) to escape template expressions in YAML configuration files
Default Headers Set for Downstream Services
When a user is authenticated, the middleware sets the following headers for downstream services:
X-Forwarded-User: The user's email addressX-User-Groups: Comma-separated list of user groups (if available)X-User-Roles: Comma-separated list of user roles (if available)X-Auth-Request-Redirect: The original request URIX-Auth-Request-User: The user's email addressX-Auth-Request-Token: The user's access token
Security Headers
The middleware also sets the following security headers:
X-Frame-Options: DENYX-Content-Type-Options: nosniffX-XSS-Protection: 1; mode=blockReferrer-Policy: strict-origin-when-cross-origin
Provider Configuration Recommendations
Important: ID Token Validation
This Traefik OIDC plugin performs authentication and extracts user claims (like email, roles, groups) exclusively from the ID Token provided by your OIDC provider. It does not primarily use the Access Token for these critical functions. Therefore, it is crucial to ensure that all necessary claims are included in the ID Token itself. A common issue is that some OIDC providers might, by default, place certain claims only in the Access Token or UserInfo endpoint.
This section provides guidance on configuring popular OIDC providers to work optimally with this plugin.
Keycloak
Keycloak is highly configurable, which means you need to ensure your client mappers are set up correctly to include necessary claims in the ID Token.
- Ensure Claims in ID Token:
- Email: Navigate to your Keycloak realm -> Clients -> Your Client ID -> Mappers. Ensure there's a mapper for 'email' (e.g., a "User Property" mapper for the
emailproperty) and that "Add to ID token" is ON. - Roles: For client roles or realm roles, create or edit mappers (e.g., "User Client Role" or "User Realm Role"). Ensure "Add to ID token" is ON. You might want to customize the "Token Claim Name" (e.g., to
rolesorgroups). - Groups: Similarly, for group membership, use a "Group Membership" mapper and ensure "Add to ID token" is ON. Customize the "Token Claim Name" as needed (e.g.,
groups).
- Email: Navigate to your Keycloak realm -> Clients -> Your Client ID -> Mappers. Ensure there's a mapper for 'email' (e.g., a "User Property" mapper for the
- Scopes: Ensure your client requests appropriate scopes that trigger the inclusion of these claims if your mappers are scope-dependent. The default
openid,profile,emailscopes are a good starting point. - Troubleshooting: If claims are missing, double-check the "Mappers" tab for your client in Keycloak. The "Token Claim Name" you define here is what you'll use in the
allowedRolesAndGroupsorheadersconfiguration in this plugin. (See also the Troubleshooting section for Keycloak).
Azure AD (Microsoft Entra ID)
Azure AD generally works well with standard OIDC configurations.
- ID Token Claims: Azure AD typically includes standard claims like
email,name,preferred_username, andoid(Object ID) in the ID Token by default whenopenid profile emailscopes are requested. - Group Claims: To include group claims in the ID Token, you need to configure this in the Azure AD application registration:
- Go to your App Registration -> Token configuration -> Add groups claim.
- You can choose which types of groups (Security groups, Directory roles, All groups) to include.
- Be aware of the "overage" issue: If a user is a member of too many groups, Azure AD will send a link to fetch groups instead of embedding them. This plugin currently expects group claims to be directly in the ID token. For users with many groups, consider alternative role/permission management strategies.
- The claim name for groups is typically
groups.
- Optional Claims: You can add other optional claims via the "Token configuration" section of your App Registration. Ensure these are configured for the ID token.
- Endpoints: The
providerURLshould behttps://login.microsoftonline.com/{your-tenant-id}/v2.0. The plugin will auto-discover the necessary endpoints. - Optimization: Ensure your application manifest in Azure AD is configured for the desired token version (v1.0 or v2.0). This plugin works with v2.0 endpoints.
Google Workspace / Google Cloud Identity
Google's OIDC implementation is well-supported.
- Optimal Configuration: The plugin automatically handles Google-specific requirements, such as using
access_type=offlineandprompt=consentto ensure refresh tokens are issued for long-lived sessions. You do not need to addoffline_accessto scopes. - ID Token Claims: Google includes standard claims like
email,sub,name,given_name,family_name,picturein the ID Token by default withopenid profile emailscopes. - Hosted Domain (hd claim): If you are using Google Workspace and want to restrict access to users within your organization's domain, Google includes an
hd(hosted domain) claim in the ID Token. You can use this with theallowedUserDomainssetting or for custom header logic. - Best Practices:
- Use the
providerURL:https://accounts.google.com. - Ensure your OAuth consent screen in Google Cloud Console is configured correctly and published. For production, it should be "External" and in "Production" status. "Testing" status limits refresh token lifetime.
- Refer to the Google OAuth Compatibility Fix section for more details on how the plugin handles Google's specifics.
- Use the
Auth0
Auth0 is generally OIDC compliant and works well.
- ID Token Claims:
- To add custom claims or standard claims not included by default (like roles or permissions) to the ID Token, you'll need to use Auth0 Rules or Actions.
- Using Actions (Recommended): Create a custom Action that runs after login to add claims to the ID Token. Example:
// Auth0 Action to add email and roles to ID Token exports.onExecutePostLogin = async (event, api) => { const namespace = 'https://your-app.com/'; // Or your custom namespace if (event.authorization) { api.idToken.setCustomClaim(namespace + 'roles', event.authorization.roles); api.idToken.setCustomClaim('email', event.user.email); // Standard claim, ensure it's there // Add other claims as needed } }; - Ensure the claims you add (e.g.,
https://your-app.com/roles) are then used in the plugin'sallowedRolesAndGroupsorheadersconfiguration.
- Scopes: Request appropriate scopes. You might need custom scopes if your Actions/Rules depend on them to add specific claims.
- Endpoints: Your
providerURLwill behttps://your-auth0-domain.auth0.com. - Logout: Ensure
postLogoutRedirectURIis registered in your Auth0 application settings under "Allowed Logout URLs".
Generic OIDC Providers
For other OIDC providers (e.g., Okta, Zitadel, self-hosted solutions):
- ID Token is Key: The primary requirement is that all claims needed for authentication decisions (email, roles, groups, custom attributes for headers) must be included in the ID Token.
- Check Provider Documentation: Consult your OIDC provider's documentation on how to:
- Configure client applications.
- Map user attributes, roles, or group memberships to claims in the ID Token.
- Define custom scopes if they are necessary to include certain claims.
- Standard Endpoints: Ensure your provider exposes a standard OIDC discovery document (
.well-known/openid-configuration) at theproviderURL. The plugin uses this to find authorization, token, JWKS, and end_session endpoints. - Scopes: Always include
openidin your scopes.profileandemailare generally recommended. Add other scopes as required by your provider to release specific claims to the ID Token. - Troubleshooting: If the plugin isn't working as expected (e.g., access denied, claims missing), the first step is to decode the ID Token received from your provider (e.g., using jwt.io) to verify its contents. This will show you exactly what claims the plugin is seeing.
For common issues and general troubleshooting, please refer to the Troubleshooting section.
Troubleshooting
Logging
Set the logLevel to debug to get more detailed logs:
logLevel: debug
Common Issues
-
Token verification failed: Check that your
providerURLis correct and accessible. -
Session encryption key too short: Ensure your
sessionEncryptionKeyis at least 32 bytes long. -
No matching public key found: The JWKS endpoint might be unavailable or the token's key ID (kid) doesn't match any key in the JWKS.
-
Access denied: Your email domain is not allowed: The user's email domain is not in the
allowedUserDomainslist. -
Access denied: You do not have any of the allowed roles or groups: The user doesn't have any of the roles or groups specified in
allowedRolesAndGroups. -
"can't evaluate field AccessToken in type bool" error: This error occurs when Traefik attempts to evaluate template expressions in the headers configuration before passing them to the plugin. To fix this:
- Use double curly braces to escape template expressions:
value: "Bearer {{{{.AccessToken}}}}" - This is the only reliable method that works with Traefik's YAML parsing
- See the Templated Headers section for complete examples
- Use double curly braces to escape template expressions:
-
Google sessions expire after ~1 hour: If using Google as the OIDC provider and sessions expire prematurely (around 1 hour instead of longer), ensure:
- Do NOT manually add the
offline_accessscope. Google rejects this scope as invalid. - The middleware automatically applies the required Google parameters (
access_type=offlineandprompt=consent). - Your Google Cloud OAuth consent screen is set to "External" and "Production" mode. "Testing" mode often limits refresh token validity.
- Verify you're using a version of the middleware that includes the Google OAuth compatibility fix.
- For more details, see the Google OAuth Compatibility Fix section or the detailed documentation.
- Do NOT manually add the
-
Keycloak: Claims Missing from ID Token (e.g., email, roles)
If you are using Keycloak and claims like
email,roles, orgroupsare missing from the ID Token, this plugin may not function as expected (e.g., for domain restrictions or RBAC).- Solution: This plugin validates the ID Token. You must configure Keycloak client mappers to add all necessary claims (email, roles, groups, etc.) to the ID Token.
- For detailed instructions, please see the Keycloak section under Provider Configuration Recommendations.
Recent Improvements
Memory Management (v0.3.0+)
The middleware has undergone significant improvements to memory management and resource utilization:
- Memory Leak Prevention: All background goroutines are properly managed with context cancellation
- Bounded Resource Usage: Session storage, metadata cache, and token cache all have size limits with LRU eviction
- Automatic Cleanup: Expired sessions and tokens are automatically cleaned up by background tasks
- Graceful Shutdown: All resources are properly released when the middleware is stopped
- Performance Monitoring: Built-in monitoring for goroutine leaks and memory growth
These improvements ensure the middleware operates efficiently even under high load and long-running deployments.
Enhanced Test Coverage
- Comprehensive test suite with race condition detection
- Memory leak detection tests
- Goroutine leak prevention tests
- Test coverage increased to 67%+ for main package, 87-99% for subpackages
Architecture and Internal Improvements
Internal Components
The middleware uses several internal components for efficient operation:
- SessionManager: Manages user sessions with automatic cleanup and pool-based allocation
- ChunkManager: Handles large session data by splitting it into manageable chunks
- MetadataCache: Caches OIDC provider metadata with LRU eviction and size limits
- TaskRegistry: Manages background tasks with proper lifecycle management
- MemoryMonitor: Monitors memory usage and detects potential leaks
Key Design Decisions
- Context-based cancellation: All background operations use context for clean shutdown
- Bounded queues and caches: Prevents unbounded memory growth
- LRU eviction policies: Ensures most frequently used data stays in cache
- Atomic operations: Uses atomic counters for statistics to avoid lock contention
- Test-friendly design: Special handling for test environments to ensure clean test execution
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
Development Guidelines
- Memory Management: Ensure all goroutines can be cancelled and resources are bounded
- Testing: Add tests for new features, including memory leak tests where appropriate
- Race Conditions: Run tests with
-raceflag to detect race conditions - Documentation: Update README and .traefik.yml for any new configuration options