fix(config): wire introspectionURL override and repair dead endpoint overrides (#152)

Issue #152 asks for a manual introspection endpoint because some IdPs
omit introspection_endpoint from discovery. Analysis found the two
documented overrides revocationURL and oidcEndSessionURL were dead
code: the 1b49e13 rebuild dropped their constructor wiring.

Add the IntrospectionURL config field and wire all three overrides.
The overrides apply inside updateMetadataEndpoints, so they survive
every metadata refresh. A manual value always wins over discovery.
The constructor also seeds the live fields, so a failed discovery
does not lose operator configuration.

Manual values skip the poisoned-discovery defences on purpose. They
sit at the same trust tier as providerURL and isValidSecureURL gates
them at config time.

Add issue152_regression_test.go: 8 tests pin precedence, refresh
survival, cold-start seeding, end-to-end introspection over HTTP,
and the validation contract.
This commit is contained in:
2026-08-09 01:59:47 +01:00
parent 78b6f193b3
commit f728f61777
7 changed files with 651 additions and 27 deletions
+29
View File
@@ -271,6 +271,21 @@ func NewWithContext(ctx context.Context, config *Config, next http.Handler, name
allowOpaqueTokens: config.AllowOpaqueTokens,
requireTokenIntrospection: config.RequireTokenIntrospection,
disableReplayDetection: config.DisableReplayDetection,
// Operator-configured endpoint overrides (issue #152). Applied inside
// updateMetadataEndpoints so the override survives every metadata
// refresh, not just construction.
configRevocationURL: config.RevocationURL,
configEndSessionURL: config.OIDCEndSessionURL,
configIntrospectionURL: config.IntrospectionURL,
// Cold-start seed: discovery failure must not lose operator-configured
// endpoints; updateMetadataEndpoints re-applies these on every refresh.
// Seeding introspectionURL here does not populate metadataSnapshot
// (only updateMetadataEndpoints does); all three consumers
// (token_introspection.go, token_manager.go, helpers.go) read these
// metadataMu-guarded fields directly, not the snapshot.
revocationURL: config.RevocationURL,
endSessionURL: config.OIDCEndSessionURL,
introspectionURL: config.IntrospectionURL,
scopes: func() []string {
userProvidedScopes := deduplicateScopes(config.Scopes)
@@ -602,6 +617,20 @@ func (t *TraefikOidc) updateMetadataEndpoints(metadata *ProviderMetadata) {
t.logger.Errorf("Ignoring discovered issuer %q: host does not match configured providerURL", discoveredIssuer)
discoveredIssuer = ""
}
// Operator-configured endpoint overrides take precedence over discovered
// values. They deliberately skip validateDiscoveredEndpoint and the
// sameHost pin: those defend against a poisoned discovery document, while
// operator config sits at the same trust tier as providerURL itself and is
// already gated by isValidSecureURL at config time.
if t.configRevocationURL != "" {
metadata.RevokeURL = t.configRevocationURL
}
if t.configEndSessionURL != "" {
metadata.EndSessionURL = t.configEndSessionURL
}
if t.configIntrospectionURL != "" {
metadata.IntrospectionURL = t.configIntrospectionURL
}
t.metadataMu.Lock()