mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-20 04:57:02 +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,125 @@
|
||||
package traefikoidc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/base64"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestConfigValidate_SessionMaxAgeUpperBound verifies that an oversized
|
||||
// sessionMaxAge (which overflows the time.Duration (ns) conversion and
|
||||
// wraps negative, locking every session out) is rejected at Validate time
|
||||
// rather than failing at runtime (R141).
|
||||
func TestConfigValidate_SessionMaxAgeUpperBound(t *testing.T) {
|
||||
base := func() *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
|
||||
}
|
||||
|
||||
if err := base().Validate(); err != nil {
|
||||
t.Fatalf("baseline config should validate: %v", err)
|
||||
}
|
||||
|
||||
// ~317 years in seconds: overflows int64 nanoseconds -> would wrap
|
||||
// negative without the upper bound.
|
||||
c := base()
|
||||
c.SessionMaxAge = 10_000_000_000
|
||||
err := c.Validate()
|
||||
if err == nil {
|
||||
t.Fatal("oversized sessionMaxAge must be rejected by Validate")
|
||||
}
|
||||
}
|
||||
|
||||
// TestUniversalCache_UpdateAtMaxSizeDoesNotEvictLiveEntry verifies that an
|
||||
// in-place update of an existing key at MaxSize does NOT evict an unrelated
|
||||
// live LRU entry (there is no net growth), mirroring the shard backend fix
|
||||
// (R141).
|
||||
func TestUniversalCache_UpdateAtMaxSizeDoesNotEvictLiveEntry(t *testing.T) {
|
||||
c := NewUniversalCache(UniversalCacheConfig{
|
||||
Type: CacheTypeToken,
|
||||
MaxSize: 2,
|
||||
MaxMemoryBytes: 1 << 20,
|
||||
})
|
||||
defer c.Close()
|
||||
|
||||
if err := c.SetLocal("B", "v2", time.Minute); err != nil {
|
||||
t.Fatalf("set B: %v", err)
|
||||
}
|
||||
if err := c.SetLocal("A", "v1", time.Minute); err != nil {
|
||||
t.Fatalf("set A: %v", err)
|
||||
}
|
||||
// Cache is full, and A is the most-recently-used entry (front of the
|
||||
// LRU). Updating A is a pure in-place update (no net size change); it
|
||||
// must not evict the least-recently-used live entry B.
|
||||
if err := c.SetLocal("A", "v2", time.Minute); err != nil {
|
||||
t.Fatalf("update A: %v", err)
|
||||
}
|
||||
if got := c.Size(); got != 2 {
|
||||
t.Fatalf("Size()=%d, want 2 (in-place update evicted a live entry)", got)
|
||||
}
|
||||
if _, ok := c.GetLocal("B"); !ok {
|
||||
t.Fatal("B was evicted by the in-place update of A")
|
||||
}
|
||||
}
|
||||
|
||||
// TestDCR_BuildRegistrationRequest_ValidatesURIMetadata verifies that the
|
||||
// optional URI metadata fields (logo_uri, client_uri, policy_uri,
|
||||
// tos_uri, jwks_uri) are validated as absolute http(s) URLs like
|
||||
// redirect_uris, so a malformed jwks_uri cannot reach the IdP (R141).
|
||||
func TestDCR_BuildRegistrationRequest_ValidatesURIMetadata(t *testing.T) {
|
||||
base := func() *DynamicClientRegistrationConfig {
|
||||
return &DynamicClientRegistrationConfig{
|
||||
ClientMetadata: &ClientRegistrationMetadata{
|
||||
RedirectURIs: []string{"https://app.example.com/callback"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// A relative jwks_uri must be rejected.
|
||||
cfg := base()
|
||||
cfg.ClientMetadata.JWKSURI = "example.com/keys"
|
||||
if _, err := NewDynamicClientRegistrar(nil, nil, cfg, "").buildRegistrationRequest(); err == nil {
|
||||
t.Fatal("relative jwks_uri must be rejected as a non-absolute URL")
|
||||
}
|
||||
|
||||
// A javascript: client_uri must be rejected.
|
||||
cfg = base()
|
||||
cfg.ClientMetadata.ClientURI = "javascript:alert(1)"
|
||||
if _, err := NewDynamicClientRegistrar(nil, nil, cfg, "").buildRegistrationRequest(); err == nil {
|
||||
t.Fatal("javascript: client_uri must be rejected")
|
||||
}
|
||||
|
||||
// A valid full set must be accepted unchanged.
|
||||
cfg = base()
|
||||
cfg.ClientMetadata.LogoURI = "https://app.example.com/logo.png"
|
||||
cfg.ClientMetadata.TOSURI = "https://app.example.com/tos"
|
||||
if _, err := NewDynamicClientRegistrar(nil, nil, cfg, "").buildRegistrationRequest(); err != nil {
|
||||
t.Fatalf("valid URI metadata should be accepted: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDecompressTokenInternal_RoundTrip guards the decompression change
|
||||
// (drop the single-Read shortcut for the pooled buffer) by confirming a
|
||||
// gzip round-trip still returns the exact original bytes (R141).
|
||||
func TestDecompressTokenInternal_RoundTrip(t *testing.T) {
|
||||
original := "header.payload.signature-part-with-extra-length"
|
||||
var buf bytes.Buffer
|
||||
gz := gzip.NewWriter(&buf)
|
||||
if _, err := gz.Write([]byte(original)); err != nil {
|
||||
t.Fatalf("gzip write: %v", err)
|
||||
}
|
||||
if err := gz.Close(); err != nil {
|
||||
t.Fatalf("gzip close: %v", err)
|
||||
}
|
||||
compressed := base64.StdEncoding.EncodeToString(buf.Bytes())
|
||||
if got := decompressToken(compressed); got != original {
|
||||
t.Fatalf("decompress round-trip mismatch:\n got %q\nwant %q", got, original)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user