Traefik OIDC plugin middleware
Find a file
Alex Sedov d3012d5169
feat: add RFC 8707 resource indicator via resource config (#161)
* feat: add RFC 8707 resource indicator via resource config

`Config.Resource` is an alternative to `audience` for IdPs that implement
RFC 8707. `buildAuthURL` sends it as the `resource` parameter on the
authorization request, before `extraAuthParams` so an operator-configured
`resource` key there cannot override or duplicate it. `exchangeTokens`
repeats it on both the `authorization_code` and `refresh_token` grants
(RFC 8707 §2.2), so a refreshed token keeps the audience binding of the
original grant.

Only one resource is accepted. RFC 8707 permits a list, but no provider
that supports the parameter handles a list usefully: one rejects any
request naming more than one resource with HTTP 400 `invalid_target`;
node-oidc-provider accepts several and issues an opaque token bound to
none of them.

`Config.Validate()` requires an absolute URI of at most 256 characters
with no fragment, wildcard, or control characters. `http`/`https` values
go through `isValidSecureURL`, so HTTPS is required outside loopback;
other schemes (`urn:`) pass as-is.

When `resource` is set and `audience` is not, `audience` defaults to the
resource for access-token validation; `strictAudienceValidation` applies
the same way whichever of the two is set. That default is validation-only.
A new `TraefikOidc.explicitAudience` mirrors `Config.Audience` verbatim,
and `buildAuthURL` keys the non-standard `audience` request parameter (the
Auth0 custom-API pattern) on it rather than on `audience`, so a
resource-derived default is never sent as an unsolicited `audience`
parameter alongside `resource` — a combination that misbehaves against
providers such as Ory Hydra, which give `audience` a different meaning.
`audience_test.go` and `url_helpers_ultra_test.go` set `audience` on the
struct directly, bypassing `New()`, and now set `explicitAudience` too.

docs/CONFIGURATION.md carries only the reference-table row and a short
pointer; the new docs/RFC8707.md holds the request behaviour, the
single-resource rationale, the `resource`/`audience` relationship, and a
provider-support table from interop testing: node-oidc-provider supports
it; Zitadel accepts and silently ignores it (`oidc.AuthRequest` has no
`resource` field, confirmed against a live instance); Authentik returns
`invalid_target`; Keycloak has no support through 26.6; Ory Hydra/Fosite
uses its own `audience` parameter; Azure AD/Entra ID v2 is reported to
reject `resource` combined with v2-style `scope`; Auth0 and AWS Cognito
claims are unverified. README.md, docs/index.html, and .traefik.yml each
point at docs/RFC8707.md instead of repeating it, and the comments on
`Config.Resource`, in main.go, and in url_helpers.go defer to it as well.

rfc8707_resource_test.go covers the authorization-URL parameter (set,
unset, query-bearing value percent-encoded rather than merged,
`extraAuthParams` unable to override), both grants sending `resource` in
the form body, the `Config.Validate()` rules table, audience defaulting
through `New()`, and a regression pair: a resource-derived audience
default is not sent as `audience`, while an explicitly configured one
still is.

* fix: accept Resource in place of Audience for bearer-auth validation

`NewWithContext` rejected `EnableBearerAuth=true` whenever
`config.Audience` was empty, even though `t.audience` already falls back
to `config.Resource` in that case. The check now fails only when both
`Audience` and `Resource` are empty, and the error names both fields.
`TestStartupValidation_BearerAcceptsResourceInPlaceOfAudience` covers the
`Resource`-only configuration.

`docs/RFC8707.md` links the `audience` parameter note to
`docs/AUTH0_AUDIENCE_GUIDE.md` instead of restating it, and drops the
provider table's GitHub row and the interop-testing paragraph, which are
covered elsewhere.
2026-09-10 14:49:53 +01:00
.github security: remediate audit findings (ranks 1–16 + 22 Lows) + yaegi load validation (#144) 2026-05-30 14:10:32 +01:00
cmd/yaegicheck security: remediate audit findings (ranks 1–16 + 22 Lows) + yaegi load validation (#144) 2026-05-30 14:10:32 +01:00
docs feat: add RFC 8707 resource indicator via resource config (#161) 2026-09-10 14:49:53 +01:00
examples docs: pin documented plugin version to v1.0.29 (#153) 2026-07-31 10:04:47 +01:00
integration Smarter approach to the cookies (#103) 2025-12-12 18:35:06 +00:00
internal security: remediate audit findings (ranks 1–16 + 22 Lows) + yaegi load validation (#144) 2026-05-30 14:10:32 +01:00
regression fix(settings): repair #149 + AST-based header template validation + allowedClaims (#150) 2026-07-21 13:04:39 +01:00
session/chunking Smarter approach to the cookies (#103) 2025-12-12 18:35:06 +00:00
vendor fix: resolve cache eviction lock-up and migrate telemetry [patch-release] 2026-05-30 13:22:03 +01:00
.gitguardian.yaml Complete rebuild of the plugin 2025-09-18 11:01:30 +01:00
.gitignore fix issue with logout url (#112) 2026-01-04 01:59:50 +00:00
.golangci.yml feat(core): refactor linters config and improve code quality (#119) 2026-01-15 10:40:49 +00:00
.goreleaser.yaml Add signing of the plugin on release. 2025-12-15 00:38:35 +00:00
.traefik.yml feat: add RFC 8707 resource indicator via resource config (#161) 2026-09-10 14:49:53 +01:00
audience_test.go feat: add RFC 8707 resource indicator via resource config (#161) 2026-09-10 14:49:53 +01:00
auth_flow.go security: remediate audit findings (ranks 1–16 + 22 Lows) + yaegi load validation (#144) 2026-05-30 14:10:32 +01:00
auth_flow_behaviour_test.go fix(refresh): honor userIdentifierClaim in token refresh path (#132) 2026-05-07 09:21:41 +01:00
auth_flow_pkce_test.go 0.7.10 (#80) 2025-10-16 10:56:28 +01:00
autocleanup.go feat(core): refactor linters config and improve code quality (#119) 2026-01-15 10:40:49 +00:00
autocleanup_additional_test.go Release 0.7.5 (#70) 2025-10-01 12:13:10 +01:00
azure_oidc_test.go refactor: delete dead non-RS validators; tests use RS variants 2026-05-23 13:04:26 +01:00
background_tasks_ultra_test.go review fixes apr 2026 (#130) 2026-04-19 10:12:00 +01:00
bearer_auth.go security: remediate audit findings (ranks 1–16 + 22 Lows) + yaegi load validation (#144) 2026-05-30 14:10:32 +01:00
bearer_auth_test.go feat: add RFC 8707 resource indicator via resource config (#161) 2026-09-10 14:49:53 +01:00
ca_cert_test.go review fixes apr 2026 (#130) 2026-04-19 10:12:00 +01:00
cache_bench_test.go Cleanup [dec2025] (#101) 2025-12-09 01:38:02 +00:00
cache_compat.go Smarter approach to the cookies (#103) 2025-12-12 18:35:06 +00:00
cache_manager.go security: remediate audit findings (ranks 1–16 + 22 Lows) + yaegi load validation (#144) 2026-05-30 14:10:32 +01:00
cache_test.go LRU + cache conflicts prevention. (#104) 2025-12-24 18:54:39 +00:00
client_assertion.go feat(auth): support private_key_jwt and client_secret_basic (#137) 2026-05-09 18:02:41 +01:00
config_marshalling.go feat(core): refactor linters config and improve code quality (#119) 2026-01-15 10:40:49 +00:00
coverage_boost_final_test.go fix(settings): repair #149 + AST-based header template validation + allowedClaims (#150) 2026-07-21 13:04:39 +01:00
csrf_session_test.go fix(refresh): honor userIdentifierClaim in token refresh path (#132) 2026-05-07 09:21:41 +01:00
custom_claims_test.go Add redis support for distributed caching (#83) 2025-11-30 02:18:46 +00:00
dcr_storage_compat.go feat(dcr): Add Redis storage support for multi-replica deployments (#109) 2025-12-31 12:52:39 +00:00
dcr_storage_test.go feat(dcr): Add Redis storage support for multi-replica deployments (#109) 2025-12-31 12:52:39 +00:00
dynamic_client_registration.go security: remediate audit findings (ranks 1–16 + 22 Lows) + yaegi load validation (#144) 2026-05-30 14:10:32 +01:00
dynamic_client_registration_test.go security: remediate audit findings (ranks 1–16 + 22 Lows) + yaegi load validation (#144) 2026-05-30 14:10:32 +01:00
edge_cases_suite_test.go Cleanup [dec2025] (#101) 2025-12-09 01:38:02 +00:00
enhanced_mocks_suite_test.go Cleanup [dec2025] (#101) 2025-12-09 01:38:02 +00:00
enhanced_mocks_test.go perf(jwk,cache): cache parsed public keys + RLock token cache reads 2026-04-30 10:14:10 +01:00
error_recovery.go security: remediate audit findings (ranks 1–16 + 22 Lows) + yaegi load validation (#144) 2026-05-30 14:10:32 +01:00
error_recovery_bench_test.go Cleanup [dec2025] (#101) 2025-12-09 01:38:02 +00:00
error_recovery_test.go Smarter approach to the cookies (#103) 2025-12-12 18:35:06 +00:00
go.mod fix: resolve cache eviction lock-up and migrate telemetry [patch-release] 2026-05-30 13:22:03 +01:00
go.sum fix: resolve cache eviction lock-up and migrate telemetry [patch-release] 2026-05-30 13:22:03 +01:00
goroutine_manager.go Smarter approach to the cookies (#103) 2025-12-12 18:35:06 +00:00
goroutine_manager_test.go 0.7.10 (#80) 2025-10-16 10:56:28 +01:00
helpers.go feat: add RFC 8707 resource indicator via resource config (#161) 2026-09-10 14:49:53 +01:00
helpers_test.go review fixes apr 2026 (#130) 2026-04-19 10:12:00 +01:00
http_client_factory.go review fixes apr 2026 (#130) 2026-04-19 10:12:00 +01:00
http_client_factory_unit_test.go Smarter approach to the cookies (#103) 2025-12-12 18:35:06 +00:00
http_client_pool.go security: remediate audit findings (ranks 1–16 + 22 Lows) + yaegi load validation (#144) 2026-05-30 14:10:32 +01:00
http_client_pool_test.go 0.7.10 (#80) 2025-10-16 10:56:28 +01:00
input_validation.go feat(core): refactor linters config and improve code quality (#119) 2026-01-15 10:40:49 +00:00
input_validation_test.go Smarter approach to the cookies (#103) 2025-12-12 18:35:06 +00:00
issue67_regression_test.go fix: eliminate per-request global mutexes in Yaegi hot paths 2026-05-23 10:47:21 +01:00
issue132_regression_test.go fix(refresh): honor userIdentifierClaim in token refresh path (#132) 2026-05-07 09:21:41 +01:00
issue134_followup_graph_test.go refactor: delete dead non-RS validators; tests use RS variants 2026-05-23 13:04:26 +01:00
issue134_regression_test.go fix(jwk): keep parsed JWKS in local cache only (#134) (#136) 2026-05-08 13:35:23 +01:00
issue135_regression_test.go feat(auth): support private_key_jwt and client_secret_basic (#137) 2026-05-09 18:02:41 +01:00
issue149_regression_test.go fix(settings): repair #149 + AST-based header template validation + allowedClaims (#150) 2026-07-21 13:04:39 +01:00
issue151_regression_test.go fix(catalog): analyzer-valid testData and explicit New() error return 2026-07-22 10:22:35 +01:00
issue152_regression_test.go fix(config): wire introspectionURL override and repair dead endpoint overrides (#152) 2026-08-09 01:59:47 +01:00
issue154_regression_test.go fix(security): allow loopback hosts for loopback providerURL (#154) 2026-08-09 00:47:25 +01:00
jwk.go security: remediate audit findings (ranks 1–16 + 22 Lows) + yaegi load validation (#144) 2026-05-30 14:10:32 +01:00
jwk_caching_test.go Smarter approach to the cookies (#103) 2025-12-12 18:35:06 +00:00
jwt.go perf(jwk,cache): cache parsed public keys + RLock token cache reads 2026-04-30 10:14:10 +01:00
LICENSE Create LICENSE 2025-04-10 01:39:57 +01:00
logger_singleton.go traefik plugin 0.7.7 (#73) 2025-10-08 11:44:00 +01:00
logout.go security: remediate audit findings (ranks 1–16 + 22 Lows) + yaegi load validation (#144) 2026-05-30 14:10:32 +01:00
logout_test.go security: remediate audit findings (ranks 1–16 + 22 Lows) + yaegi load validation (#144) 2026-05-30 14:10:32 +01:00
main.go feat: add RFC 8707 resource indicator via resource config (#161) 2026-09-10 14:49:53 +01:00
main_bench_test.go Complete rebuild of the plugin 2025-09-18 11:01:30 +01:00
main_coverage_boost2_test.go Add redis support for distributed caching (#83) 2025-11-30 02:18:46 +00:00
main_coverage_boost_test.go feat(core): refactor linters config and improve code quality (#119) 2026-01-15 10:40:49 +00:00
main_exchange_test.go Smarter approach to the cookies (#103) 2025-12-12 18:35:06 +00:00
main_goroutine_leak_test.go security: remediate audit findings (ranks 1–16 + 22 Lows) + yaegi load validation (#144) 2026-05-30 14:10:32 +01:00
main_initialization_test.go security: remediate audit findings (ranks 1–16 + 22 Lows) + yaegi load validation (#144) 2026-05-30 14:10:32 +01:00
main_refresh_test.go Smarter approach to the cookies (#103) 2025-12-12 18:35:06 +00:00
main_servehttp_test.go fix: eliminate per-request global mutexes in Yaegi hot paths 2026-05-23 10:47:21 +01:00
main_simple_test.go Smarter approach to the cookies (#103) 2025-12-12 18:35:06 +00:00
main_test.go security: remediate audit findings (ranks 1–16 + 22 Lows) + yaegi load validation (#144) 2026-05-30 14:10:32 +01:00
Makefile security: remediate audit findings (ranks 1–16 + 22 Lows) + yaegi load validation (#144) 2026-05-30 14:10:32 +01:00
memory_leak_bench_test.go Cleanup [dec2025] (#101) 2025-12-09 01:38:02 +00:00
memory_leak_fixes.go review fixes apr 2026 (#130) 2026-04-19 10:12:00 +01:00
memory_leak_test.go security: remediate audit findings (ranks 1–16 + 22 Lows) + yaegi load validation (#144) 2026-05-30 14:10:32 +01:00
memory_monitor.go review fixes apr 2026 (#130) 2026-04-19 10:12:00 +01:00
memory_optimizations.go 0.7.10 (#80) 2025-10-16 10:56:28 +01:00
metadata_cache.go security: remediate audit findings (ranks 1–16 + 22 Lows) + yaegi load validation (#144) 2026-05-30 14:10:32 +01:00
middleware.go fix(settings): repair #149 + AST-based header template validation + allowedClaims (#150) 2026-07-21 13:04:39 +01:00
middleware_edge_cases_test.go fix: eliminate per-request global mutexes in Yaegi hot paths 2026-05-23 10:47:21 +01:00
mocks_test.go Smarter approach to the cookies (#103) 2025-12-12 18:35:06 +00:00
pkce_flow_test.go 0.7.10 (#80) 2025-10-16 10:56:28 +01:00
principal.go feat: opt-in M2M bearer-token authentication (supersedes #93) (#140) 2026-05-18 17:35:37 +01:00
profiling.go Release 0.7.5 (#70) 2025-10-01 12:13:10 +01:00
profiling_test.go Add redis support for distributed caching (#83) 2025-11-30 02:18:46 +00:00
README.md feat: add RFC 8707 resource indicator via resource config (#161) 2026-09-10 14:49:53 +01:00
redis_integration_test.go Add redis support for distributed caching (#83) 2025-11-30 02:18:46 +00:00
refresh_coordinator.go fix: remove write-lock convoy in getLocal + fix mutateState CAS bug 2026-05-25 00:06:47 +01:00
refresh_coordinator_test.go fix(refresh-coordinator): trim per-request mutex/map ops 2026-05-23 11:23:16 +01:00
refresh_coordinator_wireup_test.go fix(refresh): coalesce refresh-token grants + bound goroutines + cache hot path (target v0.8.27) (#131) 2026-04-30 18:52:39 +01:00
refresh_distributed_test.go fix(refresh): coalesce refresh-token grants + bound goroutines + cache hot path (target v0.8.27) (#131) 2026-04-30 18:52:39 +01:00
refresh_race_test.go release 0.7.2 (#66) 2025-09-25 12:52:53 +01:00
refresh_token_expiry_test.go fix(refresh): coalesce refresh-token grants + bound goroutines + cache hot path (target v0.8.27) (#131) 2026-04-30 18:52:39 +01:00
requeststate.go feat(middleware): per-request context object (requestState) 2026-05-23 12:22:51 +01:00
rfc8707_resource_test.go feat: add RFC 8707 resource indicator via resource config (#161) 2026-09-10 14:49:53 +01:00
scope_filter.go traefik plugin 0.7.7 (#73) 2025-10-08 11:44:00 +01:00
scope_filter_test.go traefik plugin 0.7.7 (#73) 2025-10-08 11:44:00 +01:00
security_audit_fixes_test.go security: remediate audit findings (ranks 1–16 + 22 Lows) + yaegi load validation (#144) 2026-05-30 14:10:32 +01:00
security_edge_cases_test.go fix(refresh): honor userIdentifierClaim in token refresh path (#132) 2026-05-07 09:21:41 +01:00
SECURITY_FIX.md fix 116 (#118) 2026-01-08 22:50:46 +00:00
semver.yaml Multiple improvements. 2025-02-01 12:16:50 +00:00
session.go feat: add cookiePath config to scope session cookies to subpath (#143) 2026-08-19 17:49:52 +01:00
session_behaviour_test.go fix(refresh): honor userIdentifierClaim in token refresh path (#132) 2026-05-07 09:21:41 +01:00
session_bench_test.go Cleanup [dec2025] (#101) 2025-12-09 01:38:02 +00:00
session_chunk_cleanup.go 0.7.10 (#80) 2025-10-16 10:56:28 +01:00
session_chunk_manager.go feat(core): refactor linters config and improve code quality (#119) 2026-01-15 10:40:49 +00:00
session_test.go fix(refresh): honor userIdentifierClaim in token refresh path (#132) 2026-05-07 09:21:41 +01:00
settings.go feat: add RFC 8707 resource indicator via resource config (#161) 2026-09-10 14:49:53 +01:00
sharded_cache.go Smarter approach to the cookies (#103) 2025-12-12 18:35:06 +00:00
singleton_resources.go security: remediate audit findings (ranks 1–16 + 22 Lows) + yaegi load validation (#144) 2026-05-30 14:10:32 +01:00
singleton_resources_test.go security: remediate audit findings (ranks 1–16 + 22 Lows) + yaegi load validation (#144) 2026-05-30 14:10:32 +01:00
template_validation.go fix(settings): repair #149 + AST-based header template validation + allowedClaims (#150) 2026-07-21 13:04:39 +01:00
test_config.go Smarter approach to the cookies (#103) 2025-12-12 18:35:06 +00:00
test_framework_test.go fix(refresh): honor userIdentifierClaim in token refresh path (#132) 2026-05-07 09:21:41 +01:00
test_helpers_adapter_test.go feat(core): refactor linters config and improve code quality (#119) 2026-01-15 10:40:49 +00:00
test_infrastructure.go Smarter approach to the cookies (#103) 2025-12-12 18:35:06 +00:00
test_main_test.go Complete rebuild of the plugin 2025-09-18 11:01:30 +01:00
test_utils_test.go Smarter approach to the cookies (#103) 2025-12-12 18:35:06 +00:00
testify_mocks_test.go Cleanup [dec2025] (#101) 2025-12-09 01:38:02 +00:00
testutil_example_test.go Cleanup [dec2025] (#101) 2025-12-09 01:38:02 +00:00
token_bench_test.go Cleanup [dec2025] (#101) 2025-12-09 01:38:02 +00:00
token_introspection.go security: remediate audit findings (ranks 1–16 + 22 Lows) + yaegi load validation (#144) 2026-05-30 14:10:32 +01:00
token_manager.go security: remediate audit findings (ranks 1–16 + 22 Lows) + yaegi load validation (#144) 2026-05-30 14:10:32 +01:00
token_resilience.go Smarter approach to the cookies (#103) 2025-12-12 18:35:06 +00:00
token_test.go security: remediate audit findings (ranks 1–16 + 22 Lows) + yaegi load validation (#144) 2026-05-30 14:10:32 +01:00
token_validation_rs.go security: remediate audit findings (ranks 1–16 + 22 Lows) + yaegi load validation (#144) 2026-05-30 14:10:32 +01:00
token_validation_suite_test.go Cleanup [dec2025] (#101) 2025-12-09 01:38:02 +00:00
types.go feat: add RFC 8707 resource indicator via resource config (#161) 2026-09-10 14:49:53 +01:00
universal_cache.go security: remediate audit findings (ranks 1–16 + 22 Lows) + yaegi load validation (#144) 2026-05-30 14:10:32 +01:00
universal_cache_orphan_test.go fix: resolve cache eviction lock-up and migrate telemetry [patch-release] 2026-05-30 13:22:03 +01:00
universal_cache_serialization_test.go Fix cache serialisation (#117) 2026-01-08 22:06:19 +00:00
universal_cache_singleton.go fix(cache/redis): honor enableTLS for Redis backend (#133) 2026-05-07 12:24:13 +01:00
url_helpers.go feat: add RFC 8707 resource indicator via resource config (#161) 2026-09-10 14:49:53 +01:00
url_helpers_ultra_test.go feat: add RFC 8707 resource indicator via resource config (#161) 2026-09-10 14:49:53 +01:00
utilities.go fix: workaround for yaegi type checking using reflection (#159) 2026-09-02 16:42:47 +01:00
verify_token_hotpath_test.go fix(refresh): coalesce refresh-token grants + bound goroutines + cache hot path (target v0.8.27) (#131) 2026-04-30 18:52:39 +01:00
version.go fix: resolve cache eviction lock-up and migrate telemetry [patch-release] 2026-05-30 13:22:03 +01:00
workflow-prepare.sh fix: resolve cache eviction lock-up and migrate telemetry [patch-release] 2026-05-30 13:22:03 +01:00

Traefik OIDC Middleware

OpenID Connect authentication middleware for Traefik. Replaces forward-auth + oauth2-proxy. Auto-detects all major OIDC providers, validates ID tokens, manages sessions, and forwards user identity to downstream services.

Documentation

Provider support

Provider OIDC Refresh Auto-detected by
Google Full Yes accounts.google.com
Azure AD Full Yes login.microsoftonline.com, sts.windows.net
Auth0 Full Yes *.auth0.com
Okta Full Yes *.okta.com, *.oktapreview.com, *.okta-emea.com
Keycloak Full Yes host containing keycloak, or /realms/ in path (covers KC <17 /auth/realms/ and 17+ /realms/)
AWS Cognito Full Yes cognito-idp.*.amazonaws.com
GitLab Full Yes gitlab.com
GitHub OAuth 2.0 only — no ID token, no refresh No github.com
Generic Full Yes any RFC-compliant .well-known/openid-configuration

Authentication and claim extraction use the ID token. Ensure your provider includes required claims (email, roles, groups) in the ID token, not just the access token or UserInfo endpoint.

Install

Enable the plugin in Traefik's static configuration:

# traefik.yml
experimental:
  plugins:
    traefikoidc:
      moduleName: github.com/lukaszraczylo/traefikoidc
      version: v1.0.29

Then attach the middleware in your dynamic configuration (see Quickstart below).

This middleware tracks the current Traefik helm chart release. If it fails to load, update Traefik first.

Verify release signatures

Release checksums are signed with cosign keyless signing:

cosign verify-blob \
  --certificate-identity-regexp "https://github.com/lukaszraczylo/traefikoidc/.*" \
  --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
  --bundle "traefikoidc_v<version>_checksums.txt.sigstore.json" \
  traefikoidc_v<version>_checksums.txt

Quickstart

apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
  name: oidc-auth
  namespace: traefik
spec:
  plugin:
    traefikoidc:
      providerURL: https://accounts.google.com
      clientID: 1234567890.apps.googleusercontent.com
      clientSecret: urn:k8s:secret:traefik-oidc:CLIENT_SECRET
      sessionEncryptionKey: urn:k8s:secret:traefik-oidc:SESSION_KEY
      callbackURL: /oauth2/callback
      logoutURL: /oauth2/logout
      postLogoutRedirectURI: /
      # forceHTTPS defaults to true (secure-by-default). Only set false if you
      # serve OIDC over plaintext HTTP for local dev.
      allowedUserDomains: [company.com]
      allowedRolesAndGroups: [admin, developer]
      excludedURLs: [/health, /metrics]

More example configs in examples/.

Required parameters

Parameter Description
providerURL Issuer URL (used for OIDC discovery).
clientID OAuth 2.0 client ID.
clientSecret OAuth 2.0 client secret. Supports urn:k8s:secret:ns:name:key. Required when clientAuthMethod is unset, client_secret_post, or client_secret_basic; optional with private_key_jwt.
sessionEncryptionKey Cookie encryption key, min 32 bytes.
callbackURL Callback path, e.g. /oauth2/callback.

Common optional parameters

Full reference in docs/CONFIGURATION.md.

Parameter Default Purpose
forceHTTPS true Forces https:// in redirect URIs. Leave at default behind any TLS-terminating LB (AWS ALB, GCP LB, Azure App Gateway). Set false only for plaintext HTTP local dev.
logoutURL callbackURL + "/logout" RP-initiated logout path.
postLogoutRedirectURI / Where to send users after logout.
scopes appended to openid profile email Extra OAuth scopes. Set overrideScopes: true to replace defaults.
extraAuthParams none Map of extra query parameters appended to the authorization request (e.g. screen_hint: signup, login_hint, ui_locales, prompt). Plugin-managed params (client_id, state, nonce, redirect_uri, code_challenge, scope, response_type, …) cannot be overridden.
excludedURLs none Paths that bypass auth, matched at a path-segment or file-extension boundary (e.g. /public matches /public, /public/sub and /public.json, but not /publicsecret).
allowedUserDomains none Restrict to email domains.
allowedUsers none Restrict to specific addresses (or claim values when userIdentifierClaim != email).
allowedRolesAndGroups none Require any of these roles/groups from ID-token claims.
roleClaimName / groupClaimName roles / groups For namespaced claims (Auth0).
userIdentifierClaim email Use sub, oid, upn, or preferred_username for users without email.
enablePKCE false PKCE on the auth code flow.
cookieDomain auto Set explicitly for multi-subdomain setups (.example.com).
cookiePrefix _oidc_raczylo_ Unique prefix per middleware instance to isolate sessions.
cookiePath / Restrict cookies to a path prefix. Set to the middleware's path (e.g. /app) to prevent the browser from sending OIDC cookies to unprotected paths, avoiding 431 "Request Header Or Cookie Too Large" errors on mixed-use domains.
sessionMaxAge 86400 Session lifetime in seconds.
refreshGracePeriodSeconds 60 Proactively refresh tokens this many seconds before expiry.
maxRefreshTokenAgeSeconds 21600 Heuristic max stored refresh-token lifetime (6h). Past this, the plugin treats the RT as expired without contacting the IdP — returns 401 to AJAX, full re-auth on navigations. Set 0 to disable. Tune to match your IdP's RT TTL.
rateLimit 100 Requests/sec. Min 10.
logLevel info debug, info, error.
audience clientID, or resource if set Custom access-token audience (Auth0 custom APIs).
resource none RFC 8707 resource indicator, alternative to audience for supporting IdPs (see docs/RFC8707.md).
strictAudienceValidation false Reject mismatched audiences. Set true in production.
allowOpaqueTokens / requireTokenIntrospection false Accept opaque access tokens via RFC 7662.
disableReplayDetection false Disable JTI cache. Use Redis instead for multi-replica.
allowPrivateIPAddresses false Permit private-IP providerURL (internal Keycloak, etc.).
minimalHeaders false Reduce forwarded headers (mitigates HTTP 431).
stripAuthCookies false Strip OIDC cookies from backend hop (mitigates HTTP 431).
caCertPath / caCertPEM none Trust an internal CA for the provider's TLS.
insecureSkipVerify false Local dev only. Disables TLS verification, logs a security warning.
clientAuthMethod client_secret_post Client auth method. Set private_key_jwt for RFC 7523 JWT assertions (Entra ID, Okta, Auth0, Keycloak). See Client authentication via private key JWT.
clientAssertionPrivateKey none Inline PEM private key for private_key_jwt. Mutually exclusive with clientAssertionKeyPath.
clientAssertionKeyPath none File path to PEM private key for private_key_jwt.
clientAssertionKeyID none JWS kid header. Required when clientAuthMethod=private_key_jwt; must match the public key registered with the IdP.
clientAssertionAlg RS256 JWS alg for private_key_jwt. Supported: RS256/384/512, PS256/384/512, ES256/384/512.
enableBackchannelLogout / backchannelLogoutURL false / none OIDC Back-Channel Logout (server-to-server).
enableFrontchannelLogout / frontchannelLogoutURL false / none OIDC Front-Channel Logout (iframe).
redis disabled See docs/REDIS.md.
dynamicClientRegistration disabled See docs/DCR.md.

Production gotchas

Upgrading from an earlier release

  • Sessions are re-issued once. Session cookies are now AES-256 encrypted (previously signed only) and their cryptographic lifetime tracks sessionMaxAge (previously a fixed 30 days). Existing cookies become invalid on upgrade, so users re-authenticate one time.
  • Invalid configuration now fails closed at startup instead of being silently accepted: a sessionEncryptionKey shorter than 32 bytes, a rateLimit below 10, a missing callbackURL, or a non-HTTPS remote providerURL are rejected. Plaintext HTTP is permitted only for loopback hosts (local development).

TLS termination at a load balancer

forceHTTPS defaults to true, so redirect URIs always use https://. This is the right default behind AWS ALB, GCP LB, Azure App Gateway, or any LB that terminates TLS — X-Forwarded-Proto is unreliable (ALB may overwrite it).

Only set forceHTTPS: false when you actually serve OIDC over plaintext HTTP (local dev). See issue #82.

Multi-replica deployments

Each replica keeps its own in-memory JTI cache → false positive "token replay detected" when the same token hits different replicas. Two options:

  1. Set disableReplayDetection: true (loses replay protection).
  2. Enable Redis for shared state (recommended) — see docs/REDIS.md.

For IdP-initiated logout (back/front-channel) in multi-replica setups, Redis is required so a logout on one instance invalidates sessions on the others. Front-channel logout requests must include a matching iss query parameter; requests that omit it are rejected with 400.

Multiple middleware instances on the same host

Each instance must use a unique cookiePrefix and sessionEncryptionKey, otherwise a session minted by one instance can grant access through another. See issue #87.

Bearer-token (M2M) authentication

Opt-in path for API clients that present Authorization: Bearer <jwt> instead of logging in via the browser flow. Default off. When enabled, the middleware validates the bearer JWT against the configured OIDC provider (signature, issuer, audience, expiry) and forwards the request downstream with the principal headers — no cookie session is created.

enableBearerAuth: true
audience: https://api.example.com   # REQUIRED when bearer is enabled
# optional, defaults shown:
bearerIdentifierClaim: sub          # claim used as X-Forwarded-User
stripAuthorizationHeader: true      # drop the raw token before forwarding
bearerEmitWWWAuthenticate: true     # RFC 6750 hint on 401s
bearerOverridesCookie: false        # cookie wins when both are present (safer)
maxTokenAgeSeconds: 86400           # 24h cap on iat
bearerFailureThreshold: 20          # consecutive 401s/IP before 429 throttle

Hardening built in by default:

  • Audience required. Startup fails if enableBearerAuth=true and audience is unset. Eliminates the "token issued for service B accepted by A" confusion vector.
  • ID tokens explicitly rejected. Bearer is access-token-only. ID tokens (detected via nonce, typ: at+jwt, token_use, scope, or audience shape) return 401.
  • alg and kid pinned at the entrypoint. Asymmetric-only allowlist (RS256/384/512, PS256/384/512, ES256/384/512); kid length and charset capped — both checked before any JWKS fetch so attacker noise can't amplify into upstream calls.
  • Identifier sanitised. Default identifier source is sub; email is rejected unless explicitly opted in (which the middleware still refuses to avoid the unverified-email spoofing footgun). Control characters, bidi- override codepoints, and the delimiters , ; = are all rejected before the value reaches X-Forwarded-User.
  • Multi-audience tokens require azp. When aud is an array of more than one element, the token must carry azp == clientID.
  • iat upper-age bound. Tokens older than maxTokenAgeSeconds are rejected even if exp is far in the future.
  • Per-IP 401 throttle. After bearerFailureThreshold consecutive 401s from one source IP, further bearer requests from that IP are rejected with 429 Too Many Requests + Retry-After.
  • Cookie-wins by default. When both a session cookie and an Authorization: Bearer header arrive on the same request, the cookie path runs (safer against browser/extension/proxy bearer injection). Set bearerOverridesCookie: true for the AWS/GCP/Kubernetes convention.
  • Replay protection preserved. The bearer path skips the JTI Set (so the same token can be reused) but the Get stays active — RevokeToken still terminates a bearer token immediately.
  • Excluded URLs strip Authorization. When enableBearerAuth=true, excluded paths (e.g. /health, /metrics) get the Authorization header removed before forwarding so the token can't leak into public endpoint logs.
  • Optional real-time revocation. Set requireTokenIntrospection: true to call RFC 7662 introspection on every cache miss; revoked tokens fail immediately. Introspection endpoint failures return 503 (distinguishes infra outage from credential rejection).

Obtaining bearer tokens — minting is the IdP's job, not the middleware's. The canonical M2M flow is OAuth 2.0 client_credentials (RFC 6749 §4.4); Google requires JWT bearer assertion (RFC 7523) instead. Minimal Auth0-shape request:

curl -s -X POST https://issuer.example.com/oauth/token \
  -H 'Content-Type: application/json' \
  -d '{
    "grant_type":    "client_credentials",
    "client_id":     "your-m2m-client-id",
    "client_secret": "your-m2m-client-secret",
    "audience":      "https://api.example.com",
    "scope":         "api:read api:write"
  }'

The audience you request from the IdP must match the audience you configured on the middleware. Per-provider endpoints, parameter names, and gotchas (Entra v2 endpoint, Cognito Resource Servers, Keycloak audience mappers, Google's opaque-token quirk) are documented in docs/BEARER_AUTH.md.

Full threat model, configuration matrix, and follow-up gaps in docs/BEARER_AUTH.md.

SSE and WebSocket endpoints

Browser clients cannot follow an OIDC 302 redirect on an SSE stream or a WebSocket upgrade. The middleware handles this automatically:

  • SSE (Accept: text/event-stream) and WebSocket (Upgrade: websocket) requests skip the OIDC redirect.
  • They are not unauthenticated — a valid encrypted session cookie is required, otherwise the request is rejected. The session must already exist (i.e. the user logged in via a normal HTTP page first).
  • X-Forwarded-User is forwarded from the session.
  • Validation is cookie-only (no JWK fetch), so streaming keeps working during brief IdP outages.

No configuration needed — this is implicit behavior.

HTTP 431 from backends

Either the ID token or the chunked OIDC cookies overflow your backend's header buffer. Combine these as needed:

minimalHeaders: true     # drop X-Auth-Request-Token et al.
stripAuthCookies: true   # strip _oidc_raczylo_* cookies on the backend hop

Cookies remain in the browser; only the Traefik→backend hop is affected. See #64, #122.

Internal CA for the provider

If the provider's TLS cert is signed by a private CA (self-hosted GitLab, internal Keycloak, ADFS):

caCertPath: /etc/ssl/certs/internal-ca.pem
# or, inline:
caCertPEM: |
  -----BEGIN CERTIFICATE-----
  ...
  -----END CERTIFICATE-----

Both can be combined. An unparseable bundle fails the plugin at startup. See #125.

Client authentication via private key JWT

Use when your IdP enforces short-lived secrets or pushes secretless client auth — Microsoft Entra ID / Azure AD, Okta, Auth0, Keycloak. Instead of sending a static clientSecret, the plugin signs a short-lived JWT and submits it as client_assertion per RFC 7523.

Minimal config:

clientAuthMethod: private_key_jwt
clientAssertionKeyPath: /etc/traefik/oidc/client-key.pem
clientAssertionKeyID: my-key-2026
# clientAssertionAlg: RS256   # default; or PS256/384/512, ES256/384/512

Or inline:

clientAuthMethod: private_key_jwt
clientAssertionPrivateKey: |
  -----BEGIN PRIVATE KEY-----
  ...
  -----END PRIVATE KEY-----
clientAssertionKeyID: my-key-2026

Accepted PEM forms: PKCS#8 (PRIVATE KEY), PKCS#1 (RSA PRIVATE KEY), SEC1 (EC PRIVATE KEY). The assertion uses iss=sub=clientID, aud=tokenURL, 60s lifetime, random hex jti per request. Sent on /token (auth-code + refresh) and /revoke. The kid must match the public key registered with the IdP.

clientSecret becomes optional with private_key_jwt. Existing client_secret_post setups are unaffected. Keys are parsed once at startup — rotation requires a Traefik reload.

See issue #135.

Environment variable names containing API

Traefik reserves TRAEFIK_API_*. User vars whose name contains API (e.g. OIDC_ENCRYPTION_SECRET_API) make the plugin fail with invalid handler type: <nil>. Rename to anything without the literal API substring. See #98.

Templated headers

Forward identity to backends via Go templates over ID-token claims and tokens:

headers:
  - name: X-User-Email
    value: "{{.Claims.email}}"
  - name: Authorization
    value: "Bearer {{.AccessToken}}"
  - name: X-User-Roles
    value: "{{range $i, $e := .Claims.roles}}{{if $i}},{{end}}{{$e}}{{end}}"

Available bindings: .Claims.<field>, .AccessToken, .IdToken (or .IDToken), .RefreshToken. Names are case-sensitive (.Claims, not .claims).

Header templates are validated at startup (a failing template stops the middleware from loading). Only a fixed set of claim fields may be emitted — standard OIDC claims plus common provider claims (email, name, given_name, family_name, preferred_username, sub, groups, roles, realm_access, resource_access, oid, tid, upn, hd, picture, locale, email_verified, and a few more; see safeClaimsFields in template_validation.go). To emit a claim not on that list, add it to allowedClaims:

allowedClaims:
  - employee_id
headers:
  - name: X-Employee-Id
    value: "{{.Claims.employee_id}}"

Rendering the whole context ({{.}}, {{$}}), the whole claims map ({{.Claims}}), or any non-listed claim is rejected — this prevents a template from accidentally forwarding raw tokens or unlisted claims. range/with must target a specific listed claim (e.g. {{range .Claims.groups}}); get/default are the only functions allowed.

File-provider users: escape the braces. Traefik's file provider runs every dynamic configuration file through Go templating before the plugin sees it. Plain {{.AccessToken}} then fails with can't evaluate field AccessToken in type bool. Wrap the expression in a raw string so the file provider emits it literally: value: "{{`{{.Claims.email}}`}}". All other providers (Kubernetes CRD, Docker labels, Consul, ...) pass the value through untouched — use the plain form there. Quadruple braces ({{{{ }}}}) do not work anywhere: the file provider fails to parse them, and every other path hands them to the plugin verbatim, where template validation rejects them (issues #149, #151).

Default downstream headers

When a request is authenticated, the middleware sets:

Header Notes
X-Forwarded-User User's email (always).
X-User-Groups Comma-separated.
X-User-Roles Comma-separated.
X-Auth-Request-User User's email.
X-Auth-Request-Redirect Original request URI.
X-Auth-Request-Token Full ID token — the largest header; suppressed by minimalHeaders.

Plus security headers (CSP, HSTS, X-Frame-Options, X-Content-Type-Options, X-XSS-Protection, Referrer-Policy) controlled by the securityHeaders section — see docs/CONFIGURATION.md.

Common errors

Symptom Cause
Token verification failed Wrong/unreachable providerURL, or clock skew.
Session encryption key too short sessionEncryptionKey is < 32 bytes.
No matching public key found JWKS endpoint down, or kid mismatch.
Access denied: Your email domain is not allowed User's domain not in allowedUserDomains.
Access denied: You do not have any of the allowed roles or groups Claims missing or not in allowedRolesAndGroups.
can't evaluate field AccessToken in type bool File provider templated your header value — escape it: "{{`{{.AccessToken}}`}}" (see "Templated headers").
tls: failed to verify certificate: x509: certificate signed by unknown authority Internal CA — set caCertPath / caCertPEM.
invalid handler type: <nil> Env var name contains API — rename it.
false positive replay detected Multi-replica without Redis — see Multi-replica deployments.
Google sessions expire after ~1h Consent screen still in "Testing" mode. Do not add offline_access — Google rejects it; the middleware sets access_type=offline automatically.

Provider-specific issues (Keycloak mappers, Azure AD group overage, Auth0 namespaced claims, Cognito regions, GitLab self-hosted) live in docs/PROVIDERS.md.

Set logLevel: debug to surface detail.

Telemetry

On first plugin instantiation this middleware sends a single anonymous adoption ping — project name, version, timestamp; no identifiers, no request data, no token contents. Fire-and-forget with a 2-second timeout; cannot block plugin load or panic.

Local source: telemetry.go. Disclosure mirrors oss-telemetry — Disabling telemetry.

Quick opt-out: set any of DO_NOT_TRACK=1, OSS_TELEMETRY_DISABLED=1, or TRAEFIKOIDC_DISABLE_TELEMETRY=1.

License

See LICENSE.