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,114 @@
|
||||
package traefikoidc
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestR163_StrictAudienceRejectsClientIdOnlyAccessToken guards the
|
||||
// strictAudienceValidation audience selection in VerifyJWTSignatureAndClaims
|
||||
// (token_manager.go). detectTokenType classifies any token whose aud
|
||||
// contains clientID as an ID token, which previously swapped the
|
||||
// expected audience to clientID — so an access token carrying only
|
||||
// clientID (never the configured API audience) always passed the
|
||||
// audience check and strict mode never rejected it. The fix enforces the
|
||||
// configured audience for such tokens when strict mode is on.
|
||||
// Fail-on-old: an access token with aud=clientID only is accepted.
|
||||
func TestR163_StrictAudienceRejectsClientIdOnlyAccessToken(t *testing.T) {
|
||||
ts := NewTestSuite(t)
|
||||
ts.Setup()
|
||||
|
||||
ts.tOidc.strictAudienceValidation = true
|
||||
ts.tOidc.audience = "https://my-api.example.com" // distinct from clientID
|
||||
|
||||
tok, err := createTestJWT(ts.rsaPrivateKey, "RS256", "test-key-id", map[string]interface{}{
|
||||
"iss": "https://test-issuer.com",
|
||||
"aud": "test-client-id", // clientID only; not the configured audience
|
||||
"exp": float64(time.Now().Add(time.Hour).Unix()),
|
||||
"iat": float64(time.Now().Unix()),
|
||||
"sub": "test-user",
|
||||
"jti": "r163-aud-jti",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("createTestJWT: %v", err)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("GET", "https://example.com/test", nil)
|
||||
session, err := ts.tOidc.sessionManager.GetSession(req)
|
||||
if err != nil {
|
||||
t.Fatalf("GetSession: %v", err)
|
||||
}
|
||||
session.SetAuthenticated(true)
|
||||
session.SetAccessToken(tok)
|
||||
|
||||
rs := (&requestState{}).captureSession(session)
|
||||
authenticated, _, _ := ts.tOidc.validateStandardTokensRS(rs)
|
||||
if authenticated {
|
||||
t.Error("strict mode must reject an access token whose aud is only the client id, not the configured audience")
|
||||
}
|
||||
}
|
||||
|
||||
// TestR163_ChunkOverflowExpiresStaleCombined guards the combined→legacy
|
||||
// fallback in saveCombined (session.go). When the combined payload
|
||||
// exceeds maxCombinedChunks, Save falls back to the legacy per-token
|
||||
// chunk format but previously left the OLD combined chunk cookies in
|
||||
// place; GetSession loads combined-first, so it kept serving the stale
|
||||
// combined payload and shadowing the freshly written legacy tokens. The
|
||||
// fix expires the old combined chunks during the fallback.
|
||||
// Fail-on-old: after the fallback the combined chunk cookie is not
|
||||
// expired (still live).
|
||||
func TestR163_ChunkOverflowExpiresStaleCombined(t *testing.T) {
|
||||
sm := createTestSessionManager(t)
|
||||
chunkName := sm.combinedChunkCookieName(0)
|
||||
|
||||
// First write a small combined session so a combined chunk cookie exists.
|
||||
reqA := httptest.NewRequest("GET", "/", nil)
|
||||
sA, err := sm.GetSession(reqA)
|
||||
if err != nil {
|
||||
t.Fatalf("create small session: %v", err)
|
||||
}
|
||||
sA.useCombinedStorage = true
|
||||
sA.SetRefreshToken("small-token")
|
||||
recA := httptest.NewRecorder()
|
||||
if err := sA.Save(reqA, recA); err != nil {
|
||||
t.Fatalf("save small combined session: %v", err)
|
||||
}
|
||||
|
||||
// Overwrite with a large incompressible token that overflows the
|
||||
// combined envelope and triggers the legacy fallback.
|
||||
big := make([]byte, 17*1024)
|
||||
if _, err := rand.Read(big); err != nil {
|
||||
t.Fatalf("rand: %v", err)
|
||||
}
|
||||
bigToken := base64.RawURLEncoding.EncodeToString(big)
|
||||
|
||||
reqB := httptest.NewRequest("GET", "/", nil)
|
||||
for _, c := range recA.Result().Cookies() {
|
||||
reqB.AddCookie(c)
|
||||
}
|
||||
sB, err := sm.GetSession(reqB)
|
||||
if err != nil {
|
||||
t.Fatalf("load session for overflow: %v", err)
|
||||
}
|
||||
sB.useCombinedStorage = true
|
||||
sB.SetRefreshToken(bigToken)
|
||||
recB := httptest.NewRecorder()
|
||||
if err := sB.Save(reqB, recB); err != nil {
|
||||
t.Fatalf("overflow Save must fall back to legacy, got: %v", err)
|
||||
}
|
||||
|
||||
// The stale combined chunk cookie must be expired by the fallback.
|
||||
chunkExpired := false
|
||||
for _, c := range recB.Result().Cookies() {
|
||||
if c.Name == chunkName && c.MaxAge <= 0 {
|
||||
chunkExpired = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !chunkExpired {
|
||||
t.Fatalf("overflow fallback must expire the stale combined chunk cookie %q; without it GetSession keeps serving the old combined payload", chunkName)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user