Skip to content

DEP: External Connectors#4578

Open
nabokihms wants to merge 1 commit intodexidp:masterfrom
deckhouse:external-connectors-dep
Open

DEP: External Connectors#4578
nabokihms wants to merge 1 commit intodexidp:masterfrom
deckhouse:external-connectors-dep

Conversation

@nabokihms
Copy link
Copy Markdown
Member

Overview

DEP (Dex Enhancement Proposal) for introducing external connectors support — an official way to build custom Dex distributions with user-provided connectors compiled in, following the OpenTelemetry Collector Builder pattern.

What this PR does / why we need it

This PR adds a proposal document that describes splitting Dex connectors into core (LDAP, OIDC, OAuth2, SAML, AuthProxy) and external (GitHub, GitLab, Google, Microsoft, LinkedIn, Bitbucket, OpenShift, Gitea, Atlassian Crowd, Keystone).

Problem: Dex ships ~15 connectors in-tree. This increases binary size, dependency bloat, and maintenance burden. Community members who want to add new connectors must submit PRs to the core repo, making maintainers responsible for code they don't use.

Solution:

  • Move provider-specific connectors to a monorepo dexidp/dex-connectors (each as an independent Go module)
  • Add a connector.Register() API so connectors self-register via init()
  • Provide a dexbuilder CLI tool that generates a custom Dex binary from a builder.yaml manifest (list of Go modules to include)

Key design decisions:

  • Compile-time inclusion only — WASM, Go plugins, and gRPC plugin approaches were evaluated and not chosen (with security analysis for each)
  • Connector type names are defined in the library itself, not by the user — collisions cause a build-time panic
  • Users can exclude specific core connectors via excludeCoreConnectors
  • The registry map is unexported; access only through Register()/Get()
  • Phased migration: v2.x (registration API, backwards compatible) → v2.x+1 (builder + connector extraction) → v3.0 (external connectors removed from main module)

Special notes for your reviewer

Signed-off-by: maksim.nabokikh <max.nabokih@gmail.com>
@nabokihms nabokihms changed the title feat: add Dex Enhancement Proposal for external connectors architecture DEP: External Connectors Feb 24, 2026
@nabokihms
Copy link
Copy Markdown
Member Author

POC patch
diff --git a/cmd/dex/config.go b/cmd/dex/config.go
index 8861ddef..7971d957 100644
--- a/cmd/dex/config.go
+++ b/cmd/dex/config.go
@@ -12,6 +12,7 @@ import (
 
        "golang.org/x/crypto/bcrypt"
 
+       "github.com/dexidp/dex/connector"
        "github.com/dexidp/dex/pkg/featureflags"
        "github.com/dexidp/dex/server"
        "github.com/dexidp/dex/server/signer"
@@ -465,7 +466,7 @@ func (c *Connector) UnmarshalJSON(b []byte) error {
        if err := json.Unmarshal(b, &conn); err != nil {
                return fmt.Errorf("parse connector: %v", err)
        }
-       f, ok := server.ConnectorsConfig[conn.Type]
+       f, ok := connector.Get(conn.Type)
        if !ok {
                return fmt.Errorf("unknown connector type %q", conn.Type)
        }
diff --git a/cmd/dex/main.go b/cmd/dex/main.go
index 06ab7d0f..bfb853c1 100644
--- a/cmd/dex/main.go
+++ b/cmd/dex/main.go
@@ -1 +1,10 @@
 package main
+
+import (
+       _ "github.com/dexidp/dex/connector/mock"
+       _ "github.com/dexidp/dex/connector/openshift"
+)
+
+func main() {
+       Main()
+}
diff --git a/cmd/dex/run.go b/cmd/dex/run.go
index be334d92..81685173 100644
--- a/cmd/dex/run.go
+++ b/cmd/dex/run.go
@@ -20,7 +20,7 @@ func commandRoot() *cobra.Command {
        return rootCmd
 }
 
-func main() {
+func Main() {
        if err := commandRoot().Execute(); err != nil {
                fmt.Fprintln(os.Stderr, err.Error())
                os.Exit(2)
diff --git a/connector/mock/connectortest.go b/connector/mock/connectortest.go
index be44bfd1..bb21b12b 100644
--- a/connector/mock/connectortest.go
+++ b/connector/mock/connectortest.go
@@ -12,6 +12,15 @@ import (
        "github.com/dexidp/dex/connector"
 )
 
+func init() {
+       connector.Register("mockPassword", func() connector.Config {
+               return new(PasswordConfig)
+       })
+       connector.Register("mockCallback", func() connector.Config {
+               return new(CallbackConfig)
+       })
+}
+
 // NewCallbackConnector returns a mock connector which requires no user interaction. It always returns
 // the same (fake) identity.
 func NewCallbackConnector(logger *slog.Logger) connector.Connector {
diff --git a/connector/openshift/openshift.go b/connector/openshift/openshift.go
index 3d4408c5..822c2d31 100644
--- a/connector/openshift/openshift.go
+++ b/connector/openshift/openshift.go
@@ -22,6 +22,12 @@ const (
        usersURLPath     = "/apis/user.openshift.io/v1/users/~"
 )
 
+func init() {
+       connector.Register("openshift", func() connector.Config {
+               return new(Config)
+       })
+}
+
 // Config holds configuration options for OpenShift login
 type Config struct {
        Issuer       string   `json:"issuer"`
diff --git a/connector/registry.go b/connector/registry.go
index d7cfddd5..e06a6ff1 100644
--- a/connector/registry.go
+++ b/connector/registry.go
@@ -1 +1,41 @@
 package connector
+
+import (
+       "fmt"
+       "log/slog"
+)
+
+// Config is a configuration that can open a connector.
+type Config interface {
+       Open(id string, logger *slog.Logger) (Connector, error)
+}
+
+// registry is the internal map of registered connector types.
+// It is not exported — access is only through Register() and Get().
+var registry = map[string]func() Config{}
+
+// Register registers a connector type that can be used in Dex configuration.
+// It is intended to be called from init() functions of connector packages.
+// Calling Register with an already registered type name will panic.
+func Register(typeName string, factory func() Config) {
+       if _, exists := registry[typeName]; exists {
+               panic(fmt.Sprintf("connector type %q already registered", typeName))
+       }
+       registry[typeName] = factory
+}
+
+// Get returns the config factory for the given connector type name.
+// Returns nil if the type is not registered.
+func Get(typeName string) (func() Config, bool) {
+       f, ok := registry[typeName]
+       return f, ok
+}
+
+// RegisteredTypes returns a list of all registered connector type names.
+func RegisteredTypes() []string {
+       types := make([]string, 0, len(registry))
+       for t := range registry {
+               types = append(types, t)
+       }
+       return types
+}
diff --git a/server/server.go b/server/server.go
index e923e3e0..f1d4b5b5 100644
--- a/server/server.go
+++ b/server/server.go
@@ -699,7 +699,7 @@ var ConnectorsConfig = map[string]func() ConnectorConfig{
 func openConnector(logger *slog.Logger, conn storage.Connector) (connector.Connector, error) {
        var c connector.Connector
 
-       f, ok := ConnectorsConfig[conn.Type]
+       f, ok := connector.Get(conn.Type)
        if !ok {
                return c, fmt.Errorf("unknown connector type %q", conn.Type)
        }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant