Files
traefikoidc/review_r142_grace_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

88 lines
3.1 KiB
Go

package traefikoidc
import (
"encoding/base64"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
)
// TestGraceSkipDoesNotForwardUnauthenticated verifies that the grace-period
// refresh-skip only bare-forwards a session that is genuinely
// authenticated. skipRefreshForTokens only parseJWT's the token and reads
// the raw exp claim — it does not verify the signature or identity, so an
// unauthenticated session carrying a still-valid (future-exp) token must not
// be forwarded past the auth gate by the grace fast-path (R142).
//
// Setup: an UNAUTHENTICATED session (SetAuthenticated left at its default
// false) holding a refresh token and a future-exp ID token whose exp is
// outside the grace window. shouldSkipGraceRefresh is true, but because
// authenticated == false the new gate falls through to the refresh path
// instead of bare-forwarding.
func TestGraceSkipDoesNotForwardUnauthenticated(t *testing.T) {
sessionManager, err := NewSessionManager(
"test-encryption-key-32-bytes-long!!",
false, "", "", 0,
newNoOpLogger(),
)
if err != nil {
t.Fatalf("session manager: %v", err)
}
defer sessionManager.Shutdown()
nextCalled := false
oidc := &TraefikOidc{
next: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { nextCalled = true }),
logger: newNoOpLogger(),
initComplete: make(chan struct{}),
sessionManager: sessionManager,
firstRequestStarted: 1,
metadataRefreshStartedAtomic: 1,
issuerURL: "https://provider.example.com",
extractClaimsFunc: extractClaims,
audience: "test-client",
}
close(oidc.initComplete)
now := time.Now()
// Well-formed 3-segment JWT with future exp and proper claims. It is
// NOT signature-verifiable (junk signature), but parseJWT succeeds and
// forwardAuthorized only decodes claims, so it is enough for the skip
// decision and for the forwarding pipeline.
payload, _ := json.Marshal(map[string]any{
"exp": float64(now.Add(time.Hour).Unix()),
"iat": float64(now.Add(-time.Minute).Unix()),
"sub": "[email protected]",
})
junkSig := base64.RawURLEncoding.EncodeToString([]byte("this-is-not-a-valid-signature-ful"))
idToken := "eyJhbGciOiJSUzI1NiJ9." + base64.RawURLEncoding.EncodeToString(payload) + "." + junkSig
req0 := httptest.NewRequest(http.MethodGet, "/protected", nil)
rw0 := httptest.NewRecorder()
sess, err := sessionManager.GetSession(req0)
if err != nil {
t.Fatalf("get session: %v", err)
}
sess.SetUserIdentifier("[email protected]")
sess.SetIDToken(idToken)
sess.SetRefreshToken("refresh-token")
sess.MarkDirty()
if err := sess.Save(req0, rw0); err != nil {
t.Fatalf("save session: %v", err)
}
sess.returnToPoolSafely()
req := httptest.NewRequest(http.MethodGet, "/protected", nil)
for _, c := range rw0.Result().Cookies() {
req.AddCookie(c)
}
rw := httptest.NewRecorder()
oidc.ServeHTTP(rw, req)
if nextCalled {
t.Fatal("unauthenticated session with a future-exp token must not be bare-forwarded by the grace skip (auth gate bypass)")
}
}