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
121 lines
4.4 KiB
Go
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)
|
|
}
|
|
}
|