mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-12 04:04:26 +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
145 lines
4.9 KiB
Go
145 lines
4.9 KiB
Go
package traefikoidc
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func r145BaseConfig() *Config {
|
|
c := CreateConfig()
|
|
c.ProviderURL = "https://provider.example.com"
|
|
c.ClientID = "test-client"
|
|
c.ClientSecret = "test-secret"
|
|
c.SessionEncryptionKey = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
|
|
c.CallbackURL = "/callback"
|
|
return c
|
|
}
|
|
|
|
// TestConfigValidates_RefreshDurationOverflow guards the R145 fix in
|
|
// settings.go Validate: RefreshGracePeriodSeconds and
|
|
// MaxRefreshTokenAgeSeconds are converted to time.Duration as
|
|
// time.Duration(x) * time.Second at startup. Values above
|
|
// ~9.22e9 seconds overflow int64 nanoseconds and wrap negative — a
|
|
// wrapped-negative grace pulls the refresh threshold into the past
|
|
// (perpetual refresh), and a wrapped-negative max-age marks every
|
|
// session refresh-token-expired (mass re-auth). This is the same bug
|
|
// class already guarded for SessionMaxAge (R141), now extended here.
|
|
func TestConfigValidates_RefreshDurationOverflow(t *testing.T) {
|
|
for _, field := range []string{"refreshGracePeriodSeconds", "maxRefreshTokenAgeSeconds"} {
|
|
t.Run(field, func(t *testing.T) {
|
|
c := r145BaseConfig()
|
|
switch field {
|
|
case "refreshGracePeriodSeconds":
|
|
c.RefreshGracePeriodSeconds = 10_000_000_000
|
|
case "maxRefreshTokenAgeSeconds":
|
|
c.MaxRefreshTokenAgeSeconds = 10_000_000_000
|
|
}
|
|
if err := c.Validate(); err == nil {
|
|
t.Fatalf("out-of-range %s (would overflow time.Duration at startup) must be rejected by Validate", field)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestConfigValidates_CookieDomain guards the R145 fix in settings.go
|
|
// Validate: cookieDomain is applied verbatim to the session cookie's
|
|
// Domain option. An invalid value (a full URL, or chars illegal in a
|
|
// cookie domain) makes every browser reject the Set-Cookie, surfacing
|
|
// only as a login loop at runtime; it must fail at startup instead.
|
|
func TestConfigValidates_CookieDomain(t *testing.T) {
|
|
urlDomain := r145BaseConfig()
|
|
urlDomain.CookieDomain = "https://example.com"
|
|
if err := urlDomain.Validate(); err == nil {
|
|
t.Fatal("cookieDomain as a full URL must be rejected by Validate")
|
|
}
|
|
|
|
badChars := r145BaseConfig()
|
|
badChars.CookieDomain = "exa mple.com;user"
|
|
if err := badChars.Validate(); err == nil {
|
|
t.Fatal("cookieDomain with illegal characters must be rejected by Validate")
|
|
}
|
|
|
|
valid := r145BaseConfig()
|
|
valid.CookieDomain = ".example.com"
|
|
if err := valid.Validate(); err != nil {
|
|
t.Fatalf("a valid bare cookie domain must pass Validate, got: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestCleanupOldCookies_DeleteCookieSecureCaseInsensitive guards the R145
|
|
// fix in session.go CleanupOldCookies: the delete-cookie Secure
|
|
// attribute compared X-Forwarded-Proto case-sensitively (an exact
|
|
// residual of the R143 goto for session.Save). A proxy emitting a
|
|
// non-canonical "HTTPS" value downgraded even the deletion cookie to
|
|
// non-Secure. The check must be case-insensitive.
|
|
func TestCleanupOldCookies_DeleteCookieSecureCaseInsensitive(t *testing.T) {
|
|
sm, err := NewSessionManager(
|
|
"test-encryption-key-32-bytes-long!!",
|
|
false, "app.example.com", "testpref", 0,
|
|
newNoOpLogger(),
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("session manager: %v", err)
|
|
}
|
|
defer sm.Shutdown()
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
req.Host = "app.example.com"
|
|
req.Header.Set("X-Forwarded-Proto", "HTTPS")
|
|
req.AddCookie(&http.Cookie{Name: "testpref_sess", Value: "x"})
|
|
rw := httptest.NewRecorder()
|
|
|
|
sm.CleanupOldCookies(rw, req)
|
|
|
|
anySecure := false
|
|
for _, c := range rw.Result().Cookies() {
|
|
if c.Secure {
|
|
anySecure = true
|
|
break
|
|
}
|
|
}
|
|
if !anySecure {
|
|
t.Fatal("a non-canonical (uppercase) X-Forwarded-Proto https value must still yield a Secure delete cookie")
|
|
}
|
|
}
|
|
|
|
// TestCallback_ErrorDescriptionSanitized guards the R145 fix in
|
|
// auth_flow.go handleCallback: the provider error / error_description
|
|
// query params are attacker-controlled and were concatenated verbatim
|
|
// into the log line and echoed into the error response. An embedded
|
|
// newline could inject a forged line into structured logs; unbounded
|
|
// length would bloat the log and response. Values must be
|
|
// newline-sanitized and length-capped.
|
|
func TestCallback_ErrorDescriptionSanitized(t *testing.T) {
|
|
sm, err := NewSessionManager(
|
|
"test-encryption-key-32-bytes-long!!",
|
|
false, "", "", 0,
|
|
newNoOpLogger(),
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("session manager: %v", err)
|
|
}
|
|
defer sm.Shutdown()
|
|
|
|
oidc := &TraefikOidc{
|
|
logger: newNoOpLogger(),
|
|
sessionManager: sm,
|
|
}
|
|
|
|
desc := "line1%0Aline2"
|
|
req := httptest.NewRequest(http.MethodGet, "/callback?error=access_denied&error_description="+desc, nil)
|
|
rw := httptest.NewRecorder()
|
|
|
|
oidc.handleCallback(rw, req, "/")
|
|
|
|
body := rw.Body.String()
|
|
if strings.Contains(body, "line1\nline2") {
|
|
t.Fatalf("callback error body must not contain a raw newline from error_description (log/response injection), got %q", body)
|
|
}
|
|
if !strings.Contains(body, "line1 line2") {
|
|
t.Fatalf("callback error body must contain the newline-sanitized error_description")
|
|
}
|
|
}
|