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,102 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user