mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-12 04:04:26 +00:00
- 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
103 lines
3.4 KiB
Go
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
|
|
}
|