Files
mobility-accounts/oidc-provider/oidc-provider.go
2026-03-03 11:16:12 +01:00

188 lines
5.7 KiB
Go
Executable File

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{
Dir: "/web",
Issuer: nsName,
}
// 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
}