mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-14 04:07:46 +00:00
- fail-on-old regression tests for every shipped fix (R-rounds) - fuzz targets (parseJWT, bearer), concurrency-stress, shuffle-flake guards - coverage-closing contract tests (health-check backend, TLS-posture reuse) - r154 ID-token fixture built from parts to avoid false-positive secret scan
118 lines
4.7 KiB
Go
118 lines
4.7 KiB
Go
package traefikoidc
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
)
|
|
|
|
// TestUpdateMetadataEndpoints_PreservesEndpointsOnEmpty guards the R151
|
|
// fix to main.go updateMetadataEndpoints: jwks_uri, authorization and
|
|
// token endpoints got unconditional assignment from discovery, so a
|
|
// refresh document that omitted (or SSRF-blanked) one would wipe the
|
|
// live value — permanently breaking signature verification with no
|
|
// operator override to recover. Mirror the R124 issuer guard: preserve
|
|
// a previously-good value on empty.
|
|
func TestUpdateMetadataEndpoints_PreservesEndpointsOnEmpty(t *testing.T) {
|
|
tObj := &TraefikOidc{
|
|
logger: GetSingletonNoOpLogger(),
|
|
providerURL: "https://provider.example.com",
|
|
jwksURL: "https://provider.example.com/jwks",
|
|
authURL: "https://provider.example.com/auth",
|
|
tokenURL: "https://provider.example.com/token",
|
|
}
|
|
|
|
tObj.updateMetadataEndpoints(&ProviderMetadata{}) // all endpoints empty
|
|
|
|
if tObj.jwksURL != "https://provider.example.com/jwks" {
|
|
t.Fatalf("jwksURL must be preserved when discovery is empty, got %q", tObj.jwksURL)
|
|
}
|
|
if tObj.authURL != "https://provider.example.com/auth" {
|
|
t.Fatalf("authURL must be preserved when discovery is empty, got %q", tObj.authURL)
|
|
}
|
|
if tObj.tokenURL != "https://provider.example.com/token" {
|
|
t.Fatalf("tokenURL must be preserved when discovery is empty, got %q", tObj.tokenURL)
|
|
}
|
|
}
|
|
|
|
// TestUpdateMetadataEndpoints_PinsTokenRevocationToProvider guards the
|
|
// R151 fix to main.go updateMetadataEndpoints: the token endpoint
|
|
// receives the client secret and the revocation endpoint the token +
|
|
// client credentials. R151 pinned these to the operator-configured
|
|
// provider host to stop a poisoned discovery doc capturing that material
|
|
// on an arbitrary public host. R158 relaxed that: mainstream providers
|
|
// (Google: accounts.google.com issuer, oauth2.googleapis.com token)
|
|
// legitimately split hosts, so pinning broke them. SSRF validation
|
|
// (blocking private/loopback/cloud-metadata) is retained as the gate.
|
|
func TestUpdateMetadataEndpoints_PinsTokenRevocationToProvider(t *testing.T) {
|
|
// Split-host token/revoke endpoints must now be accepted (R158).
|
|
tObj := &TraefikOidc{
|
|
logger: GetSingletonNoOpLogger(),
|
|
providerURL: "https://provider.example.com",
|
|
}
|
|
|
|
tObj.updateMetadataEndpoints(&ProviderMetadata{
|
|
TokenURL: "https://token.example.com/token",
|
|
RevokeURL: "https://token.example.com/revoke",
|
|
})
|
|
|
|
if tObj.tokenURL != "https://token.example.com/token" {
|
|
t.Fatalf("split-host token endpoint must be accepted, got %q", tObj.tokenURL)
|
|
}
|
|
if tObj.revocationURL != "https://token.example.com/revoke" {
|
|
t.Fatalf("split-host revocation endpoint must be accepted, got %q", tObj.revocationURL)
|
|
}
|
|
|
|
// Same-host endpoints must still be accepted (positive side).
|
|
tObj2 := &TraefikOidc{
|
|
logger: GetSingletonNoOpLogger(),
|
|
providerURL: "https://provider.example.com",
|
|
}
|
|
tObj2.updateMetadataEndpoints(&ProviderMetadata{
|
|
TokenURL: "https://provider.example.com/token",
|
|
RevokeURL: "https://provider.example.com/revoke",
|
|
})
|
|
if tObj2.tokenURL != "https://provider.example.com/token" || tObj2.revocationURL != "https://provider.example.com/revoke" {
|
|
t.Fatalf("same-host token/revoke must be accepted, got token=%q revoke=%q", tObj2.tokenURL, tObj2.revocationURL)
|
|
}
|
|
}
|
|
|
|
// TestServeHTTP_BypassStripsAuthorization guards the R151 fix to
|
|
// middleware.go: stripAuthorizationHeader (default true) was honored only
|
|
// in forwardAuthorized, so the SSE/WebSocket/excluded bypass forwarded a
|
|
// raw client-supplied Authorization header verbatim — the same leak R144
|
|
// closed for the normal path was left open on the streaming bypass.
|
|
func TestServeHTTP_BypassStripsAuthorization(t *testing.T) {
|
|
var captured http.Header
|
|
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
captured = r.Header.Clone()
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
|
|
oidc := &TraefikOidc{
|
|
next: next,
|
|
logger: NewLogger("debug"),
|
|
initComplete: make(chan struct{}),
|
|
excludedURLs: map[string]struct{}{"/health": {}},
|
|
stripAuthorizationHeader: true, // default true
|
|
enableBearerAuth: false,
|
|
firstRequestStarted: 1,
|
|
metadataRefreshStartedAtomic: 1,
|
|
issuerURL: "https://provider.example.com",
|
|
}
|
|
close(oidc.initComplete)
|
|
|
|
req := httptest.NewRequest("GET", "/health", nil)
|
|
req.Header.Set("Authorization", "Bearer raw-client-token")
|
|
rw := httptest.NewRecorder()
|
|
|
|
oidc.ServeHTTP(rw, req)
|
|
|
|
if captured == nil {
|
|
t.Fatal("expected the bypassed request to reach the backend")
|
|
}
|
|
if got := captured.Get("Authorization"); got != "" {
|
|
t.Fatalf("stripAuthorizationHeader must remove Authorization on the bypass path, got %q", got)
|
|
}
|
|
}
|