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,129 @@
|
||||
package traefikoidc
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/sessions"
|
||||
)
|
||||
|
||||
func r148BaseConfig() *Config {
|
||||
c := CreateConfig()
|
||||
c.ProviderURL = "https://provider.example.com"
|
||||
c.ClientID = "test-client"
|
||||
c.ClientSecret = "test-secret"
|
||||
c.SessionEncryptionKey = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
|
||||
c.CallbackURL = "/callback"
|
||||
return c
|
||||
}
|
||||
|
||||
// TestConfigValidates_ScopesRequireOpenID guards the R148 fix in
|
||||
// settings.go Validate: scopes flow verbatim into the OIDC authorize
|
||||
// request. An empty scope element (trailing comma in a Traefik label)
|
||||
// and an override that drops the mandatory "openid" scope are rejected
|
||||
// by strict OIDC providers at runtime (OIDC Core 3.1.2.1) — every
|
||||
// login fails. Validate now rejects both at startup.
|
||||
func TestConfigValidates_ScopesRequireOpenID(t *testing.T) {
|
||||
empty := r148BaseConfig()
|
||||
empty.Scopes = []string{"profile", ""}
|
||||
if err := empty.Validate(); err == nil {
|
||||
t.Fatal("empty scope element must be rejected by Validate")
|
||||
}
|
||||
|
||||
overrideNoOpenID := r148BaseConfig()
|
||||
overrideNoOpenID.OverrideScopes = true
|
||||
overrideNoOpenID.Scopes = []string{"profile"}
|
||||
if err := overrideNoOpenID.Validate(); err == nil {
|
||||
t.Fatal("overrideScopes without mandatory openid scope must be rejected by Validate")
|
||||
}
|
||||
|
||||
ok := r148BaseConfig()
|
||||
ok.Scopes = []string{"openid", "profile"}
|
||||
if err := ok.Validate(); err != nil {
|
||||
t.Fatalf("valid scopes must pass Validate, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestConfigValidates_SecurityHeaderCRLF guards the R148 fix:
|
||||
// SecurityHeader CORS lists and CustomHeaders are emitted verbatim into
|
||||
// response headers; a CR/LF would split/corrupt the header. Validate
|
||||
// now rejects CR/LF/NUL in these values.
|
||||
func TestConfigValidates_SecurityHeaderCRLF(t *testing.T) {
|
||||
cors := r148BaseConfig()
|
||||
cors.SecurityHeaders = &SecurityHeadersConfig{}
|
||||
cors.SecurityHeaders.CORSAllowedHeaders = []string{"X-Auth\r\nInjected: 1"}
|
||||
if err := cors.Validate(); err == nil {
|
||||
t.Fatal("CORSAllowedHeaders entry with CR must be rejected by Validate")
|
||||
}
|
||||
|
||||
custom := r148BaseConfig()
|
||||
custom.SecurityHeaders = &SecurityHeadersConfig{}
|
||||
custom.SecurityHeaders.CustomHeaders = map[string]string{"X-Custom": "a\nb"}
|
||||
if err := custom.Validate(); err == nil {
|
||||
t.Fatal("CustomHeaders value with LF must be rejected by Validate")
|
||||
}
|
||||
|
||||
ok := r148BaseConfig()
|
||||
ok.SecurityHeaders = &SecurityHeadersConfig{}
|
||||
ok.SecurityHeaders.CORSAllowedHeaders = []string{"X-Auth"}
|
||||
if err := ok.Validate(); err != nil {
|
||||
t.Fatalf("clean security headers must pass Validate, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestConfigValidates_HeaderName guard the R148 fix: a header Name
|
||||
// with characters illegal in an HTTP field name would be written
|
||||
// verbatim into every response line by Go's server, corrupting it.
|
||||
func TestConfigValidates_HeaderName(t *testing.T) {
|
||||
bad := r148BaseConfig()
|
||||
bad.Headers = []TemplatedHeader{{Name: "X User Email", Value: "[email protected]"}}
|
||||
if err := bad.Validate(); err == nil {
|
||||
t.Fatal("header name with space must be rejected by Validate")
|
||||
}
|
||||
|
||||
ok := r148BaseConfig()
|
||||
ok.Headers = []TemplatedHeader{{Name: "X-User-Email", Value: "[email protected]"}}
|
||||
if err := ok.Validate(); err != nil {
|
||||
t.Fatalf("valid header name must pass Validate, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestVerify_JTIBlacklistPiercesCache guard the R148 fix in
|
||||
// verifyTokenWithOpts: the cached fast-return must still reject a token
|
||||
// whose JTI has been blacklisted. A same-JTI rotated token that
|
||||
// happens to be cached would otherwise keep returning "valid" until the
|
||||
// cache TTL even though its JTI was revoked under a different raw
|
||||
// value.
|
||||
func TestVerify_JTIBlacklistPiercesCache(t *testing.T) {
|
||||
oidc := makeBearerOIDC(t, nil)
|
||||
// makeBearerOIDC leaves tokenBlacklist nil; wire an isolated one.
|
||||
oidc.tokenBlacklist = newMapCache()
|
||||
|
||||
token := "aaa.bbb.ccc" // not the test-key prefix; 2 dots, len >= 10
|
||||
seedVerified(t, oidc, token, map[string]interface{}{"sub": "u", "jti": "shared-jti"})
|
||||
oidc.tokenBlacklist.Set("shared-jti", time.Now().Unix(), time.Minute)
|
||||
|
||||
if err := oidc.verifyTokenWithOpts(token, verifyOpts{}); err == nil {
|
||||
t.Fatal("cached token whose JTI is blacklisted must be rejected (fast path must pierce jti)")
|
||||
}
|
||||
}
|
||||
|
||||
// TestChunk_NoIndexZeroNoPanic guards the R148 fix in
|
||||
// processChunkedToken: a sparse chunk set without index 0 previously
|
||||
// nil-dereferenced chunks[0].Now it must return a clean missing-chunk
|
||||
// error instead of panicking.
|
||||
func TestChunk_NoIndexZeroNoPanic(t *testing.T) {
|
||||
cm := NewChunkManager(newNoOpLogger())
|
||||
chunks := map[int]*sessions.Session{
|
||||
1: {Values: map[interface{}]interface{}{"token_chunk": "part1", "token_total": 1}},
|
||||
}
|
||||
|
||||
res := cm.processChunkedToken(chunks, RefreshTokenConfig)
|
||||
if res.Error == nil {
|
||||
t.Fatal("sparse chunk set missing index 0 must return an error, not a token")
|
||||
}
|
||||
if !strings.Contains(res.Error.Error(), "missing") {
|
||||
t.Fatalf("expected missing-chunk error, got: %v", res.Error)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user