Files
traefikoidc/review_r97_test.go
T
lukaszraczylo 6d96e8869b test: add R-round regression and security suite
- 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
2026-08-14 16:22:39 +01:00

121 lines
4.4 KiB
Go

package traefikoidc
import (
"crypto/rand"
"encoding/base64"
"net/http"
"net/http/httptest"
"testing"
)
// R97 regression (Session): the refresh token's issued_at (used by the
// maxRefreshTokenAge staleness heuristic) must survive a combined-cookie
// save/load round-trip. Previously combined storage dropped it, so after
// reload GetRefreshTokenIssuedAt() returned zero and the heuristic was
// silently disabled.
func TestCombinedRoundTripPreservesRefreshIssuedAt(t *testing.T) {
sm := createTestSessionManager(t)
req0 := httptest.NewRequest(http.MethodGet, "/", nil)
s, err := sm.GetSession(req0)
if err != nil {
t.Fatalf("create session: %v", err)
}
s.useCombinedStorage = true // exercise the combined (not legacy) path
s.SetRefreshToken("test-refresh-token-123")
issuedAt := s.GetRefreshTokenIssuedAt()
if issuedAt.IsZero() {
t.Fatalf("precondition: refresh token should have a non-zero issued_at")
}
rec := httptest.NewRecorder()
if err := s.Save(req0, rec); err != nil {
t.Fatalf("save combined session: %v", err)
}
req := httptest.NewRequest(http.MethodGet, "/", nil)
for _, c := range rec.Result().Cookies() {
req.AddCookie(c)
}
s2, err := sm.GetSession(req)
if err != nil {
t.Fatalf("load session: %v", err)
}
defer s2.returnToPoolSafely()
if got := s2.GetRefreshTokenIssuedAt(); got.Unix() != issuedAt.Unix() {
t.Fatalf("refresh issued_at lost after combined round-trip: want %d, got %d", issuedAt.Unix(), got.Unix())
}
if s2.GetRefreshToken() != "test-refresh-token-123" {
t.Fatalf("refresh token value not preserved in combined round-trip")
}
}
// R97 regression (Session): when the combined-cookie payload is too
// large (>10 chunks), saveCombined must fall back to the legacy
// per-token chunk format (which supports ~100KB) instead of returning
// an error and dropping the session. Previously a large (incompressible)
// refresh token made Save error and forced the user to re-authenticate.
func TestSaveCombined_OverflowFallsBackToLegacy(t *testing.T) {
sm := createTestSessionManager(t)
// A ~17KB random JWT payload → ~23KB token: under SetRefreshToken's 50KB
// cap (so it is stored) and under the JWT read guard (payload ≤ 40KB,
// sig ≤ 2KB, so legacy can read it back), but incompressible so the
// combined payload (14KB limit) overflows maxCombinedChunks. Old code
// errored here and dropped the session; new code falls back to legacy.
payload := make([]byte, 17*1024)
if _, err := rand.Read(payload); err != nil {
t.Fatalf("rand: %v", err)
}
bigToken := base64.RawURLEncoding.EncodeToString([]byte(`{"alg":"none"}`)) + "." +
base64.RawURLEncoding.EncodeToString(payload) + "." +
base64.RawURLEncoding.EncodeToString([]byte("0123456789abcdef0123")) // 26-char sig (>=10 required)
if len(bigToken) > 50*1024 {
t.Fatalf("precondition: token must be under the 50KB SetRefreshToken cap, got %d", len(bigToken))
}
req0 := httptest.NewRequest(http.MethodGet, "/", nil)
s, err := sm.GetSession(req0)
if err != nil {
t.Fatalf("create session: %v", err)
}
s.useCombinedStorage = true // force the combined path
s.SetRefreshToken(bigToken)
if s.GetRefreshToken() != bigToken {
t.Fatalf("precondition: SetRefreshToken must have stored the large token")
}
rec := httptest.NewRecorder()
if err := s.Save(req0, rec); err != nil {
t.Fatalf("Save with oversized combined payload must fall back to legacy, got error: %v", err)
}
if len(rec.Result().Cookies()) == 0 {
t.Fatalf("Save should still persist cookies (legacy fallback), got none")
}
}
// R97 regression (Discovery): name-based discovery endpoints (localhost,
// cloud metadata hostnames, internal hostnames) must be SSRF-guarded just
// like IP-literal endpoints. Previously validateDiscoveredEndpoint only
// checked net.ParseIP-literal hosts, so a discovery doc returning e.g.
// metadata.google.internal or localhost bypassed the SSRF defense.
func TestValidateDiscoveredEndpoint_RejectsDangerousHostnames(t *testing.T) {
oidc := &TraefikOidc{}
bad := []string{
"https://metadata.google.internal/jwks",
"http://localhost:3000/jwks",
}
for _, url := range bad {
if err := oidc.validateDiscoveredEndpoint(url, false); err == nil {
t.Errorf("expected SSRF rejection for %q, got nil", url)
}
}
// A normal public hostname must still be accepted.
if err := oidc.validateDiscoveredEndpoint("https://login.example.com/oauth2/token", false); err != nil {
t.Fatalf("expected public hostname to be accepted, got %v", err)
}
}