Files
traefikoidc/types.go
T
Alex SedovandGitHub 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

230 lines
9.8 KiB
Go

// Package traefikoidc provides OIDC authentication middleware for Traefik.
package traefikoidc
import (
"context"
"net/http"
"sync"
"sync/atomic"
"text/template"
"time"
"golang.org/x/time/rate"
)
// CacheInterface defines the common cache operations
type CacheInterface interface {
Set(key string, value any, ttl time.Duration)
Get(key string) (any, bool)
Delete(key string)
SetMaxSize(size int)
Size() int
Clear()
Cleanup()
Close()
GetStats() map[string]any // For testing and monitoring
}
// TokenVerifier interface defines token verification capabilities.
// Implementations should validate token format, signature, and claims.
type TokenVerifier interface {
VerifyToken(token string) error
}
// JWTVerifier interface defines JWT-specific verification capabilities.
// Implementations should validate JWT structure, signature using JWKs, and standard claims.
type JWTVerifier interface {
VerifyJWTSignatureAndClaims(jwt *JWT, token string) error
}
// TokenExchanger interface defines OAuth 2.0 and OpenID Connect token exchange capabilities.
// Implementations should handle authorization code exchange, refresh tokens, and revocation
// according to the OAuth 2.0 and OpenID Connect specifications.
type TokenExchanger interface {
ExchangeCodeForToken(ctx context.Context, grantType string, codeOrToken string, redirectURL string, codeVerifier string) (*TokenResponse, error)
GetNewTokenWithRefreshToken(refreshToken string) (*TokenResponse, error)
RevokeTokenWithProvider(token, tokenType string) error
}
// ProviderMetadata represents OIDC provider configuration data.
// This data is typically retrieved from the provider's .well-known/openid-configuration endpoint
// and contains essential URLs for authentication, token exchange, and key retrieval.
type ProviderMetadata struct {
Issuer string `json:"issuer"`
AuthURL string `json:"authorization_endpoint"`
TokenURL string `json:"token_endpoint"`
JWKSURL string `json:"jwks_uri"`
RevokeURL string `json:"revocation_endpoint"`
EndSessionURL string `json:"end_session_endpoint"`
IntrospectionURL string `json:"introspection_endpoint,omitempty"`
RegistrationURL string `json:"registration_endpoint,omitempty"`
ScopesSupported []string `json:"scopes_supported,omitempty"`
}
// TraefikOidc is the main middleware struct that implements OIDC authentication for Traefik.
// It integrates with various OIDC providers, manages sessions, caches tokens, and handles
// the complete authentication flow. It's designed to work seamlessly with Traefik's
// plugin system and provides flexible configuration options.
// MetadataSnapshot is an immutable bundle of provider-metadata URLs that the
// plugin needs on the hot request path. Published atomically via
// TraefikOidc.metadataSnapshot; readers do exactly one atomic.Value.Load to
// access all fields. Replaces 3 per-request metadataMu.RLock acquisitions
// in middleware.ServeHTTP + token_manager paths, each of which paid
// 1-5ms of Yaegi-dispatch overhead.
//
// The fields are a strict subset of the metadataMu-guarded TraefikOidc
// fields; the legacy fields are still written under metadataMu for
// less-frequent code paths that have not been migrated.
type MetadataSnapshot struct {
IssuerURL string
JWKSURL string
TokenURL string
AuthURL string
RevocationURL string
EndSessionURL string
IntrospectionURL string
RegistrationURL string
}
type TraefikOidc struct {
// metadataSnapshot atomically publishes the read-mostly URL bundle.
// Hot-path readers (middleware.ServeHTTP, token verification) load it
// directly; less-frequent paths still acquire metadataMu.RLock and
// read the individual fields below.
metadataSnapshot atomic.Value
// lastMetadataRetryNano is the UnixNano timestamp of the last metadata
// recovery attempt. Stored atomically so the hot ServeHTTP path can
// throttle retries without acquiring metadataRetryMutex on every request.
lastMetadataRetryNano int64
// firstRequestStarted is 0 until the very first non-health request fires
// the background-task bootstrap; then it flips to 1 via CAS. Replaces the
// firstRequestMutex + firstRequestReceived combo which previously took
// a write lock on every non-health request forever.
firstRequestStarted int32
// metadataRefreshStartedAtomic is the CAS-only variant of the old
// metadataRefreshStarted bool. Both flags live under the same atomic so
// concurrent first-request goroutines race exactly once.
metadataRefreshStartedAtomic int32
jwkCache JWKCacheInterface
jwtVerifier JWTVerifier
ctx context.Context
tokenVerifier TokenVerifier
next http.Handler
tokenExchanger TokenExchanger
tokenBlacklist CacheInterface
tokenTypeCache CacheInterface
introspectionCache CacheInterface
initComplete chan struct{}
limiter *rate.Limiter
headerTemplates map[string]*template.Template
sessionManager *SessionManager
tokenCleanupStopChan chan struct{}
excludedURLs map[string]struct{}
extractClaimsFunc func(tokenString string) (map[string]any, error)
initiateAuthenticationFunc func(rw http.ResponseWriter, req *http.Request, session *SessionData, redirectURL string)
metadataCache *MetadataCache
allowedRolesAndGroups map[string]struct{}
allowedUsers map[string]struct{}
allowedUserDomains map[string]struct{}
tokenCache *TokenCache
httpClient *http.Client
tokenHTTPClient *http.Client
logger *Logger
metadataRefreshStopChan chan struct{}
cancelFunc context.CancelFunc
errorRecoveryManager *ErrorRecoveryManager
tokenResilienceManager *TokenResilienceManager
refreshCoordinator *RefreshCoordinator
goroutineWG *sync.WaitGroup
dcrConfig *DynamicClientRegistrationConfig
dynamicClientRegistrar *DynamicClientRegistrar
scopeFilter *ScopeFilter
securityHeadersApplier func(http.ResponseWriter, *http.Request)
userIdentifierClaim string
revocationURL string
name string
redirURLPath string
logoutURLPath string
tokenURL string
authURL string
endSessionURL string
postLogoutRedirectURI string
jwksURL string
issuerURL string
groupClaimName string
introspectionURL string
providerURL string
roleClaimName string
audience string
clientID string
clientSecret string
clientAuthMethod string
clientAssertion *ClientAssertionSigner
registrationURL string
// configRevocationURL, configEndSessionURL, and configIntrospectionURL
// hold operator-configured endpoint overrides (Config.RevocationURL,
// Config.OIDCEndSessionURL, Config.IntrospectionURL). They are set once at
// construction and never mutated afterwards, so they are read lock-free,
// the same contract as providerURL, which updateMetadataEndpoints already
// reads unlocked. A manual override always wins over discovery; an empty
// string means use the discovered value.
configRevocationURL string
configEndSessionURL string
configIntrospectionURL string
backchannelLogoutPath string
frontchannelLogoutPath string
scopesSupported []string
scopes []string
extraAuthParams map[string]string
// resource holds the RFC 8707 resource indicator from Config.Resource
// (Config.Validate() already guarantees it is either empty or a
// non-empty absolute URI). Sent as the `resource` param on the
// authorization request and on both the authorization_code and
// refresh_token grants.
resource string
// explicitAudience mirrors Config.Audience verbatim, with no
// resource- or clientID-based defaulting applied. audience (below) does
// carry that defaulting, for token-validation purposes; explicitAudience
// exists so buildAuthURL can decide whether to send the non-standard
// `audience` request parameter without that default leaking onto the
// wire.
explicitAudience string
refreshGracePeriod time.Duration
maxRefreshTokenAge time.Duration
metadataMu sync.RWMutex
shutdownOnce sync.Once
sessionInvalidationCache CacheInterface
refreshResultCache CacheInterface
minimalHeaders bool
stripAuthCookies bool
enableBackchannelLogout bool
enableFrontchannelLogout bool
requireTokenIntrospection bool
allowPrivateIPAddresses bool
// allowLoopbackHosts permits loopback/localhost hosts in outbound URL
// validation (validateHost). Derived at construction time from a loopback
// providerURL (local development), never operator-set directly. Mirrors
// the allowLoopback derivation in updateMetadataEndpoints.
allowLoopbackHosts bool
disableReplayDetection bool
allowOpaqueTokens bool
strictAudienceValidation bool
overrideScopes bool
enablePKCE bool
forceHTTPS bool
suppressDiagnosticLogs bool
// Bearer-auth runtime state (populated only when EnableBearerAuth=true).
bearerIdentifierClaim string
bearerFailureTracker *bearerFailureTracker
maxTokenAge time.Duration
maxIdentifierLength int
bearerFailureThreshold int
bearerFailureWindow time.Duration
bearerFailurePenalty time.Duration
enableBearerAuth bool
stripAuthorizationHeader bool
bearerEmitWWWAuthenticate bool
bearerOverridesCookie bool
}