diff --git a/go.mod b/go.mod index 1be695715..ac23843e5 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ toolchain go1.26.8 require ( github.com/IGLOU-EU/go-wildcard/v2 v2.1.1 github.com/containerd/errdefs v1.0.0 - github.com/coreos/go-oidc/v3 v3.20.0 + github.com/coreos/go-oidc/v3 v3.21.0 github.com/crewjam/saml v0.5.1 github.com/fsnotify/fsnotify v1.10.1 github.com/go-pdf/fpdf v0.9.0 diff --git a/go.sum b/go.sum index 1322cfa40..321187371 100644 --- a/go.sum +++ b/go.sum @@ -12,8 +12,8 @@ github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= -github.com/coreos/go-oidc/v3 v3.20.0 h1:EtE0WIBHk03N+DqGkY4+UONzzZHk7amKt6IyNd7OsZE= -github.com/coreos/go-oidc/v3 v3.20.0/go.mod h1:DYCf24+ncYi+XkIH97GY1+dqoRlbaSI26KVTCI9SrY4= +github.com/coreos/go-oidc/v3 v3.21.0 h1:wZo4Q9Pum8dYEj0eMUPrqR+kvuGkeUplbLpNCkBqoWM= +github.com/coreos/go-oidc/v3 v3.21.0/go.mod h1:DYCf24+ncYi+XkIH97GY1+dqoRlbaSI26KVTCI9SrY4= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/crewjam/saml v0.5.1 h1:g+mfp0CrLuLRZCK793PgJcZeg5dS/0CDwoeAX2zcwNI= diff --git a/internal/api/oidc_service_test.go b/internal/api/oidc_service_test.go index 01d48c8de..467853dee 100644 --- a/internal/api/oidc_service_test.go +++ b/internal/api/oidc_service_test.go @@ -1,17 +1,109 @@ package api import ( + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" "crypto/x509" + "encoding/base64" "encoding/pem" "errors" + "fmt" "net/http" "net/http/httptest" "os" "strings" "testing" "time" + + "github.com/rcourtman/pulse-go-rewrite/internal/config" ) +func TestOIDCServiceVerifiesTokenWhenProviderHasUnsupportedKey(t *testing.T) { + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatalf("generate RSA key: %v", err) + } + + modulus := base64.RawURLEncoding.EncodeToString(privateKey.PublicKey.N.Bytes()) + jwks := fmt.Sprintf(`{ + "keys": [ + { + "kty": "OKP", + "crv": "Ed448", + "kid": "unsupported", + "use": "sig", + "x": "gH1eRK-6hW6ZoAy2k11U4L5uaIaMaZTMCf1cAbsxsYLvTqV2-TQG1PNyLOrhZkMyzUJulMc1wAfH" + }, + { + "kty": "RSA", + "kid": "supported", + "use": "sig", + "alg": "RS256", + "n": %q, + "e": "AQAB" + } + ] + }`, modulus) + + server := newIPv4HTTPServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + baseURL := "http://" + r.Host + w.Header().Set("Content-Type", "application/json") + switch r.URL.Path { + case "/.well-known/openid-configuration": + _, _ = fmt.Fprintf(w, `{ + "issuer": %q, + "authorization_endpoint": %q, + "token_endpoint": %q, + "jwks_uri": %q + }`, baseURL, baseURL+"/authorize", baseURL+"/token", baseURL+"/jwks") + case "/jwks": + _, _ = w.Write([]byte(jwks)) + default: + http.NotFound(w, r) + } + })) + defer server.Close() + + service, err := NewOIDCService(t.Context(), &config.OIDCConfig{ + Enabled: true, + IssuerURL: server.URL, + ClientID: "pulse-client", + RedirectURL: "http://pulse.example/api/oidc/callback", + Scopes: []string{"openid"}, + }) + if err != nil { + t.Fatalf("initialize Pulse OIDC service: %v", err) + } + defer service.stateStore.Stop() + + header := base64.RawURLEncoding.EncodeToString([]byte(`{"alg":"RS256","kid":"supported"}`)) + payload := []byte(fmt.Sprintf(`{ + "iss": %q, + "sub": "pulse-user", + "aud": "pulse-client", + "iat": %d, + "exp": %d + }`, server.URL, time.Now().Add(-time.Minute).Unix(), time.Now().Add(time.Hour).Unix())) + encodedPayload := base64.RawURLEncoding.EncodeToString(payload) + signingInput := header + "." + encodedPayload + digest := sha256.Sum256([]byte(signingInput)) + signature, err := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA256, digest[:]) + if err != nil { + t.Fatalf("sign JWT: %v", err) + } + token := signingInput + "." + base64.RawURLEncoding.EncodeToString(signature) + + idToken, err := service.verifier.Verify(t.Context(), token) + if err != nil { + t.Fatalf("verify ID token against mixed provider key set: %v", err) + } + if idToken.Subject != "pulse-user" { + t.Fatalf("verified subject = %q, want pulse-user", idToken.Subject) + } +} + func TestNewOIDCHTTPClient_WithCustomCABundle(t *testing.T) { // Self-signed TLS server should be rejected by default client server := newIPv4TLSServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {