mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-21 04:57:48 +00:00
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
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user