package op import ( "context" "fmt" "log/slog" "net/http" "os" "time" "github.com/dexidp/dex/server" dexstorage "github.com/dexidp/dex/storage" "github.com/dexidp/dex/storage/memory" "github.com/mitchellh/mapstructure" "github.com/rs/zerolog/log" "github.com/spf13/viper" "gitlab.com/mobicoop/solidarity/services/mobility-accounts/handlers" maconnector "gitlab.com/mobicoop/solidarity/services/mobility-accounts/oidc-provider/connector" "gitlab.com/mobicoop/solidarity/services/mobility-accounts/storage" ) // OIDCConfig holds the full OIDC provider configuration, decoded from viper. type OIDCConfig struct { Enable bool Port string BaseURL string `mapstructure:"base_url"` Namespaces map[string]OIDCNamespaceConfig } // OIDCNamespaceConfig holds per-namespace OIDC settings. type OIDCNamespaceConfig struct { Namespace string SecretKey string `mapstructure:"secret_key"` TemplatesDir string `mapstructure:"templates_dir"` MatchClaims map[string]string `mapstructure:"match_claims"` Clients []OIDCClient } // OIDCClient represents a static OIDC client. type OIDCClient struct { ID string OIDC bool Secret string RedirectURIs []string `mapstructure:"redirect_uris"` ResponseTypes []string `mapstructure:"response_types"` GrantTypes []string `mapstructure:"grant_types"` Scopes []string Audience []string Public bool //OIDC specific TokenEndpointAuthMethod string `mapstructure:"token_endpoint_auth_method"` } // NewDexServer creates an http.ServeMux that hosts one Dex OIDC server per namespace. // Each namespace is mounted at /{namespaceName}/ and has its own storage, clients, // and connector instance. func NewDexServer(handler *handlers.MobilityAccountsHandler, stor storage.Storage, cfg *viper.Viper) (*http.ServeMux, error) { var oidcConfig OIDCConfig mapstructure.Decode(cfg.Get("services.oidc_provider").(map[string]any), &oidcConfig) baseURL := oidcConfig.BaseURL if baseURL == "" { protocol := "https" if cfg.GetBool("dev_env") { protocol = "http" } baseURL = fmt.Sprintf("%s://0.0.0.0:%s", protocol, cfg.GetString("services.oidc_provider.port")) } mux := http.NewServeMux() logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelInfo})) // Register our custom connector type in Dex's global registry server.ConnectorsConfig["mobilityaccounts"] = func() server.ConnectorConfig { return &maconnector.Config{ Handler: handler, Storage: stor, Namespace: "", // will be set per-namespace below } } for nsName, nsCfg := range oidcConfig.Namespaces { dexServer, err := createNamespaceDexServer(handler, stor, nsCfg, nsName, baseURL, logger) if err != nil { return nil, fmt.Errorf("failed to create Dex server for namespace %q: %w", nsName, err) } prefix := "/" + nsName mux.Handle(prefix+"/", dexServer) } return mux, nil } // createNamespaceDexServer builds a single Dex server.Server for one namespace. func createNamespaceDexServer(handler *handlers.MobilityAccountsHandler, stor storage.Storage, nsCfg OIDCNamespaceConfig, nsName, baseURL string, logger *slog.Logger) (*server.Server, error) { ctx := context.Background() // In-memory Dex storage dexStore := memory.New(logger) // Register static clients clients := buildDexClients(nsCfg.Clients) dexStore = dexstorage.WithStaticClients(dexStore, clients) // Register the connector — we override ConnectorsConfig factory for this namespace // so that Open() will get the right handler/storage/namespace. server.ConnectorsConfig["mobilityaccounts"] = func() server.ConnectorConfig { return &maconnector.Config{ Handler: handler, Storage: stor, Namespace: nsCfg.Namespace, } } conn := dexstorage.Connector{ ID: "mobilityaccounts", Type: "mobilityaccounts", Name: "Mobility Accounts", } dexStore = dexstorage.WithStaticConnectors(dexStore, []dexstorage.Connector{conn}) issuer := fmt.Sprintf("%s/%s", baseURL, nsName) // Determine web config webCfg := server.WebConfig{ Issuer: nsName, } if nsCfg.TemplatesDir != "" { webCfg.Dir = nsCfg.TemplatesDir } // Dex v2.42 manages signing keys internally via storage. dexServer, err := server.NewServer(ctx, server.Config{ Issuer: issuer, Storage: dexStore, SkipApprovalScreen: true, IDTokensValidFor: 30 * time.Hour, Web: webCfg, Logger: logger, Now: time.Now, }) if err != nil { return nil, fmt.Errorf("failed to create Dex server: %w", err) } return dexServer, nil } // buildDexClients converts the config OIDCClient list to Dex storage.Client list. func buildDexClients(clients []OIDCClient) []dexstorage.Client { dexClients := make([]dexstorage.Client, 0, len(clients)) for _, c := range clients { dexClients = append(dexClients, dexstorage.Client{ ID: c.ID, Secret: c.Secret, RedirectURIs: c.RedirectURIs, Public: c.Public, Name: c.ID, }) } return dexClients } // Run starts the OIDC provider HTTP server. Called from main.go as a goroutine. func Run(done chan error, cfg *viper.Viper, handler handlers.MobilityAccountsHandler, stor storage.Storage) { address := "0.0.0.0:" + cfg.GetString("services.oidc_provider.port") log.Info().Str("address", address).Msg("Running Dex OIDC provider") mux, err := NewDexServer(&handler, stor, cfg) if err != nil { log.Error().Err(err).Msg("Failed to create Dex OIDC server") done <- err return } srv := &http.Server{ Handler: mux, Addr: address, WriteTimeout: 15 * time.Second, ReadTimeout: 15 * time.Second, } err = srv.ListenAndServe() if err != nil { log.Error().Err(err).Msg("Dex OIDC server ended") } done <- err }