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

103 lines
3.4 KiB
Go

package traefikoidc
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"encoding/json"
"encoding/pem"
"testing"
)
// TestConfigMarshal_RoundTripSurvives guards the R150 fix in
// config_marshalling.go: the hand-maintained redaction list silently
// dropped entire config areas (dynamicClientRegistration, audience,
// client assertion settings) and never redacted their embedded secrets.
// A JSON round-trip must now retain config and redact every secret.
func TestConfigMarshal_RoundTripSurvives(t *testing.T) {
cfg := Config{
ProviderURL: "https://provider.example.com",
ClientID: "client-abc",
ClientSecret: "super-secret-client",
SessionEncryptionKey: "enc-key-here",
Audience: "https://api.example.com",
OverrideScopes: true,
EnablePKCE: true,
AllowOpaqueTokens: true,
ClientAssertionPrivateKey: "-----BEGIN PRIVATE KEY-----\nLEAK-CA-ASSERT\n-----END PRIVATE KEY-----\n",
DynamicClientRegistration: &DynamicClientRegistrationConfig{
Enabled: true,
InitialAccessToken: "dcr-bearer-secret",
StorageBackend: "redis",
},
}
raw, err := json.Marshal(cfg)
if err != nil {
t.Fatalf("MarshalJSON failed: %v", err)
}
s := string(raw)
// Config areas that the legacy list dropped must now be present.
for _, want := range []string{
"\"audience\":\"https://api.example.com\"",
"\"overrideScopes\":true",
"\"enablePKCE\":true",
"\"dynamicClientRegistration\"",
"\"clientAssertionPrivateKey\":\"[REDACTED]\"",
} {
if !containsStr(s, want) {
t.Errorf("marshaled config missing %s in:\n%s", want, s)
}
}
// The DCR initial access token must be redacted, not leaked.
if containsStr(s, "dcr-bearer-secret") {
t.Error("marshaled config leaked DCR initialAccessToken")
}
if !containsStr(s, "initialAccessToken\":\"[REDACTED]\"") {
t.Error("marshaled DCR block must redact initialAccessToken")
}
// Existing secrets still redacted, none of the raw secrets leaked.
for _, secret := range []string{"super-secret-client", "enc-key-here", "LEAK-CA-ASSERT"} {
if containsStr(s, secret) {
t.Errorf("marshaled config leaked secret %q", secret)
}
}
}
// TestClientAssertionSign_EmptyAudienceRejected guards the R150 fix in
// ClientAssertionSigner.Sign: signing with an empty audience previously
// minted an assertion with "aud":"" that the IdP would reject.
func TestClientAssertionSign_EmptyAudienceRejected(t *testing.T) {
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatalf("generate key: %v", err)
}
der, err := x509.MarshalPKCS8PrivateKey(key)
if err != nil {
t.Fatalf("marshal key: %v", err)
}
pemBytes := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: der})
signer, err := NewClientAssertionSigner(pemBytes, "ES256", "test-kid")
if err != nil {
t.Fatalf("new signer: %v", err)
}
if _, err := signer.Sign("", "client-abc"); err == nil {
t.Fatal("Sign with an empty audience must return an error")
}
// A real audience still works.
if tok, err := signer.Sign("https://provider.example.com/token", "client-abc"); err != nil || tok == "" {
t.Fatalf("Sign with a valid audience must succeed, err=%v", err)
}
}
func containsStr(haystack, needle string) bool {
for i := 0; i+len(needle) <= len(haystack); i++ {
if haystack[i:i+len(needle)] == needle {
return true
}
}
return false
}