     1→// Package dex provides an embedded Dex OIDC identity provider.
     2→package dex
     3→
     4→import (
     5→	"context"
     6→	"encoding/json"
     7→	"errors"
     8→	"fmt"
     9→	"strings"
    10→
    11→	"github.com/dexidp/dex/storage"
    12→)
    13→
    14→// ConnectorConfig represents the configuration for an identity provider connector
    15→type ConnectorConfig struct {
    16→	// ID is the unique identifier for the connector
    17→	ID string
    18→	// Name is a human-readable name for the connector
    19→	Name string
    20→	// Type is the connector type (oidc, google, microsoft)
    21→	Type string
    22→	// Issuer is the OIDC issuer URL (for OIDC-based connectors)
    23→	Issuer string
    24→	// ClientID is the OAuth2 client ID
    25→	ClientID string
    26→	// ClientSecret is the OAuth2 client secret
    27→	ClientSecret string
    28→	// RedirectURI is the OAuth2 redirect URI
    29→	RedirectURI string
    30→}
    31→
    32→// CreateConnector creates a new connector in Dex storage.
    33→// It maps the connector config to the appropriate Dex connector type and configuration.
    34→func (p *Provider) CreateConnector(ctx context.Context, cfg *ConnectorConfig) (*ConnectorConfig, error) {
    35→	// Fill in the redirect URI if not provided
    36→	if cfg.RedirectURI == "" {
    37→		cfg.RedirectURI = p.GetRedirectURI()
    38→	}
    39→
    40→	storageConn, err := p.buildStorageConnector(cfg)
    41→	if err != nil {
    42→		return nil, fmt.Errorf("failed to build connector: %w", err)
    43→	}
    44→
    45→	if err := p.storage.CreateConnector(ctx, storageConn); err != nil {
    46→		return nil, fmt.Errorf("failed to create connector: %w", err)
    47→	}
    48→
    49→	p.logger.Info("connector created", "id", cfg.ID, "type", cfg.Type)
    50→	return cfg, nil
    51→}
    52→
    53→// GetConnector retrieves a connector by ID from Dex storage.
    54→func (p *Provider) GetConnector(ctx context.Context, id string) (*ConnectorConfig, error) {
    55→	conn, err := p.storage.GetConnector(ctx, id)
    56→	if err != nil {
    57→		if err == storage.ErrNotFound {
    58→			return nil, err
    59→		}
    60→		return nil, fmt.Errorf("failed to get connector: %w", err)
    61→	}
    62→
    63→	return p.parseStorageConnector(conn)
    64→}
    65→
    66→// ListConnectors returns all connectors from Dex storage (excluding the local connector).
    67→func (p *Provider) ListConnectors(ctx context.Context) ([]*ConnectorConfig, error) {
    68→	connectors, err := p.storage.ListConnectors(ctx)
    69→	if err != nil {
    70→		return nil, fmt.Errorf("failed to list connectors: %w", err)
    71→	}
    72→
    73→	result := make([]*ConnectorConfig, 0, len(connectors))
    74→	for _, conn := range connectors {
    75→		// Skip the local password connector
    76→		if conn.ID == "local" && conn.Type == "local" {
    77→			continue
    78→		}
    79→
    80→		cfg, err := p.parseStorageConnector(conn)
    81→		if err != nil {
    82→			p.logger.Warn("failed to parse connector", "id", conn.ID, "error", err)
    83→			continue
    84→		}
    85→		result = append(result, cfg)
    86→	}
    87→
    88→	return result, nil
    89→}
    90→
    91→// UpdateConnector updates an existing connector in Dex storage.
    92→// It merges incoming updates with existing values to prevent data loss on partial updates.
    93→func (p *Provider) UpdateConnector(ctx context.Context, cfg *ConnectorConfig) error {
    94→	if err := p.storage.UpdateConnector(ctx, cfg.ID, func(old storage.Connector) (storage.Connector, error) {
    95→		oldCfg, err := p.parseStorageConnector(old)
    96→		if err != nil {
    97→			return storage.Connector{}, fmt.Errorf("failed to parse existing connector: %w", err)
    98→		}
    99→
   100→		mergeConnectorConfig(cfg, oldCfg)
   101→
   102→		storageConn, err := p.buildStorageConnector(cfg)
   103→		if err != nil {
   104→			return storage.Connector{}, fmt.Errorf("failed to build connector: %w", err)
   105→		}
   106→		return storageConn, nil
   107→	}); err != nil {
   108→		return fmt.Errorf("failed to update connector: %w", err)
   109→	}
   110→
   111→	p.logger.Info("connector updated", "id", cfg.ID, "type", cfg.Type)
   112→	return nil
   113→}
   114→
   115→// mergeConnectorConfig preserves existing values for empty fields in the update.
   116→func mergeConnectorConfig(cfg, oldCfg *ConnectorConfig) {
   117→	if cfg.ClientSecret == "" {
   118→		cfg.ClientSecret = oldCfg.ClientSecret
   119→	}
   120→	if cfg.RedirectURI == "" {
   121→		cfg.RedirectURI = oldCfg.RedirectURI
   122→	}
   123→	if cfg.Issuer == "" && cfg.Type == oldCfg.Type {
   124→		cfg.Issuer = oldCfg.Issuer
   125→	}
   126→	if cfg.ClientID == "" {
   127→		cfg.ClientID = oldCfg.ClientID
   128→	}
   129→	if cfg.Name == "" {
   130→		cfg.Name = oldCfg.Name
   131→	}
   132→}
   133→
   134→// DeleteConnector removes a connector from Dex storage.
   135→func (p *Provider) DeleteConnector(ctx context.Context, id string) error {
   136→	// Prevent deletion of the local connector
   137→	if id == "local" {
   138→		return fmt.Errorf("cannot delete the local password connector")
   139→	}
   140→
   141→	if err := p.storage.DeleteConnector(ctx, id); err != nil {
   142→		return fmt.Errorf("failed to delete connector: %w", err)
   143→	}
   144→
   145→	p.logger.Info("connector deleted", "id", id)
   146→	return nil
   147→}
   148→
   149→// GetRedirectURI returns the default redirect URI for connectors.
   150→func (p *Provider) GetRedirectURI() string {
   151→	if p.config == nil {
   152→		return ""
   153→	}
   154→	issuer := strings.TrimSuffix(p.config.Issuer, "/")
   155→	if !strings.HasSuffix(issuer, "/oauth2") {
   156→		issuer += "/oauth2"
   157→	}
   158→	return issuer + "/callback"
   159→}
   160→
   161→// buildStorageConnector creates a storage.Connector from ConnectorConfig.
   162→// It handles the type-specific configuration for each connector type.
   163→func (p *Provider) buildStorageConnector(cfg *ConnectorConfig) (storage.Connector, error) {
   164→	redirectURI := p.resolveRedirectURI(cfg.RedirectURI)
   165→
   166→	var dexType string
   167→	var configData []byte
   168→	var err error
   169→
   170→	switch cfg.Type {
   171→	case "oidc", "zitadel", "entra", "okta", "pocketid", "authentik", "keycloak":
   172→		dexType = "oidc"
   173→		configData, err = buildOIDCConnectorConfig(cfg, redirectURI)
   174→	case "google":
   175→		dexType = "google"
   176→		configData, err = buildOAuth2ConnectorConfig(cfg, redirectURI)
   177→	case "microsoft":
   178→		dexType = "microsoft"
   179→		configData, err = buildOAuth2ConnectorConfig(cfg, redirectURI)
   180→	default:
   181→		return storage.Connector{}, fmt.Errorf("unsupported connector type: %s", cfg.Type)
   182→	}
   183→	if err != nil {
   184→		return storage.Connector{}, err
   185→	}
   186→
   187→	return storage.Connector{ID: cfg.ID, Type: dexType, Name: cfg.Name, Config: configData}, nil
   188→}
   189→
   190→// resolveRedirectURI returns the redirect URI, using a default if not provided
   191→func (p *Provider) resolveRedirectURI(redirectURI string) string {
   192→	if redirectURI != "" || p.config == nil {
   193→		return redirectURI
   194→	}
   195→	issuer := strings.TrimSuffix(p.config.Issuer, "/")
   196→	if !strings.HasSuffix(issuer, "/oauth2") {
   197→		issuer += "/oauth2"
   198→	}
   199→	return issuer + "/callback"
   200→}
   201→
   202→// buildOIDCConnectorConfig creates config for OIDC-based connectors
   203→func buildOIDCConnectorConfig(cfg *ConnectorConfig, redirectURI string) ([]byte, error) {
   204→	oidcConfig := map[string]interface{}{
   205→		"issuer":               cfg.Issuer,
   206→		"clientID":             cfg.ClientID,
   207→		"clientSecret":         cfg.ClientSecret,
   208→		"redirectURI":          redirectURI,
   209→		"scopes":               []string{"openid", "profile", "email"},
   210→		"insecureEnableGroups": true,
   211→		//some providers don't return email verified, so we need to skip it if not present (e.g., Entra, Okta, Duo)
   212→		"insecureSkipEmailVerified": true,
   213→	}
   214→	switch cfg.Type {
   215→	case "zitadel":
   216→		oidcConfig["getUserInfo"] = true
   217→	case "entra":
   218→		oidcConfig["claimMapping"] = map[string]string{"email": "preferred_username"}
   219→	case "okta":
   220→		oidcConfig["scopes"] = []string{"openid", "profile", "email", "groups"}
   221→	case "pocketid":
   222→		oidcConfig["scopes"] = []string{"openid", "profile", "email", "groups"}
   223→	}
   224→	return encodeConnectorConfig(oidcConfig)
   225→}
   226→
   227→// buildOAuth2ConnectorConfig creates config for OAuth2 connectors (google, microsoft)
   228→func buildOAuth2ConnectorConfig(cfg *ConnectorConfig, redirectURI string) ([]byte, error) {
   229→	return encodeConnectorConfig(map[string]interface{}{
   230→		"clientID":     cfg.ClientID,
   231→		"clientSecret": cfg.ClientSecret,
   232→		"redirectURI":  redirectURI,
   233→	})
   234→}
   235→
   236→// parseStorageConnector converts a storage.Connector back to ConnectorConfig.
   237→// It infers the original identity provider type from the Dex connector type and ID.
   238→func (p *Provider) parseStorageConnector(conn storage.Connector) (*ConnectorConfig, error) {
   239→	cfg := &ConnectorConfig{
   240→		ID:   conn.ID,
   241→		Name: conn.Name,
   242→	}
   243→
   244→	if len(conn.Config) == 0 {
   245→		cfg.Type = conn.Type
   246→		return cfg, nil
   247→	}
   248→
   249→	var configMap map[string]interface{}
   250→	if err := decodeConnectorConfig(conn.Config, &configMap); err != nil {
   251→		return nil, fmt.Errorf("failed to parse connector config: %w", err)
   252→	}
   253→
   254→	// Extract common fields
   255→	if v, ok := configMap["clientID"].(string); ok {
   256→		cfg.ClientID = v
   257→	}
   258→	if v, ok := configMap["clientSecret"].(string); ok {
   259→		cfg.ClientSecret = v
   260→	}
   261→	if v, ok := configMap["redirectURI"].(string); ok {
   262→		cfg.RedirectURI = v
   263→	}
   264→	if v, ok := configMap["issuer"].(string); ok {
   265→		cfg.Issuer = v
   266→	}
   267→
   268→	// Infer the original identity provider type from Dex connector type and ID
   269→	cfg.Type = inferIdentityProviderType(conn.Type, conn.ID, configMap)
   270→
   271→	return cfg, nil
   272→}
   273→
   274→// inferIdentityProviderType determines the original identity provider type
   275→// based on the Dex connector type, connector ID, and configuration.
   276→func inferIdentityProviderType(dexType, connectorID string, _ map[string]interface{}) string {
   277→	if dexType != "oidc" {
   278→		return dexType
   279→	}
   280→	return inferOIDCProviderType(connectorID)
   281→}
   282→
   283→// inferOIDCProviderType infers the specific OIDC provider from connector ID
   284→func inferOIDCProviderType(connectorID string) string {
   285→	connectorIDLower := strings.ToLower(connectorID)
   286→	for _, provider := range []string{"pocketid", "zitadel", "entra", "okta", "authentik", "keycloak"} {
   287→		if strings.Contains(connectorIDLower, provider) {
   288→			return provider
   289→		}
   290→	}
   291→	return "oidc"
   292→}
   293→
   294→// encodeConnectorConfig serializes connector config to JSON bytes.
   295→func encodeConnectorConfig(config map[string]interface{}) ([]byte, error) {
   296→	return json.Marshal(config)
   297→}
   298→
   299→// decodeConnectorConfig deserializes connector config from JSON bytes.
   300→func decodeConnectorConfig(data []byte, v interface{}) error {
   301→	return json.Unmarshal(data, v)
   302→}
   303→
   304→// ensureLocalConnector creates a local (password) connector if it doesn't exist
   305→func ensureLocalConnector(ctx context.Context, stor storage.Storage) error {
   306→	// Check specifically for the local connector
   307→	_, err := stor.GetConnector(ctx, "local")
   308→	if err == nil {
   309→		// Local connector already exists
   310→		return nil
   311→	}
   312→	if !errors.Is(err, storage.ErrNotFound) {
   313→		return fmt.Errorf("failed to get local connector: %w", err)
   314→	}
   315→
   316→	// Create a local connector for password authentication
   317→	localConnector := storage.Connector{
   318→		ID:   "local",
   319→		Type: "local",
   320→		Name: "Email",
   321→	}
   322→
   323→	if err := stor.CreateConnector(ctx, localConnector); err != nil {
   324→		return fmt.Errorf("failed to create local connector: %w", err)
   325→	}
   326→
   327→	return nil
   328→}
   329→
   330→// HasNonLocalConnectors checks if there are any connectors other than the local connector.
   331→func (p *Provider) HasNonLocalConnectors(ctx context.Context) (bool, error) {
   332→	connectors, err := p.storage.ListConnectors(ctx)
   333→	if err != nil {
   334→		return false, fmt.Errorf("failed to list connectors: %w", err)
   335→	}
   336→
   337→	p.logger.Info("checking for non-local connectors", "total_connectors", len(connectors))
   338→	for _, conn := range connectors {
   339→		p.logger.Info("found connector in storage", "id", conn.ID, "type", conn.Type, "name", conn.Name)
   340→		if conn.ID != "local" || conn.Type != "local" {
   341→			p.logger.Info("found non-local connector", "id", conn.ID)
   342→			return true, nil
   343→		}
   344→	}
   345→	p.logger.Info("no non-local connectors found")
   346→	return false, nil
   347→}
   348→
   349→// DisableLocalAuth removes the local (password) connector.
   350→// Returns an error if no other connectors are configured.
   351→func (p *Provider) DisableLocalAuth(ctx context.Context) error {
   352→	hasOthers, err := p.HasNonLocalConnectors(ctx)
   353→	if err != nil {
   354→		return err
   355→	}
   356→	if !hasOthers {
   357→		return fmt.Errorf("cannot disable local authentication: no other identity providers configured")
   358→	}
   359→
   360→	// Check if local connector exists
   361→	_, err = p.storage.GetConnector(ctx, "local")
   362→	if errors.Is(err, storage.ErrNotFound) {
   363→		// Already disabled
   364→		return nil
   365→	}
   366→	if err != nil {
   367→		return fmt.Errorf("failed to check local connector: %w", err)
   368→	}
   369→
   370→	// Delete the local connector
   371→	if err := p.storage.DeleteConnector(ctx, "local"); err != nil {
   372→		return fmt.Errorf("failed to delete local connector: %w", err)
   373→	}
   374→
   375→	p.logger.Info("local authentication disabled")
   376→	return nil
   377→}
   378→
   379→// EnableLocalAuth creates the local (password) connector if it doesn't exist.
   380→func (p *Provider) EnableLocalAuth(ctx context.Context) error {
   381→	return ensureLocalConnector(ctx, p.storage)
   382→}
   383→
   384→// ensureStaticConnectors creates or updates static connectors in storage
   385→func ensureStaticConnectors(ctx context.Context, stor storage.Storage, connectors []Connector) error {
   386→	for _, conn := range connectors {
   387→		storConn, err := conn.ToStorageConnector()
   388→		if err != nil {
   389→			return fmt.Errorf("failed to convert connector %s: %w", conn.ID, err)
   390→		}
   391→		_, err = stor.GetConnector(ctx, conn.ID)
   392→		if err == storage.ErrNotFound {
   393→			if err := stor.CreateConnector(ctx, storConn); err != nil {
   394→				return fmt.Errorf("failed to create connector %s: %w", conn.ID, err)
   395→			}
   396→			continue
   397→		}
   398→		if err != nil {
   399→			return fmt.Errorf("failed to get connector %s: %w", conn.ID, err)
   400→		}
   401→		if err := stor.UpdateConnector(ctx, conn.ID, func(old storage.Connector) (storage.Connector, error) {
   402→			old.Name = storConn.Name
   403→			old.Config = storConn.Config
   404→			return old, nil
   405→		}); err != nil {
   406→			return fmt.Errorf("failed to update connector %s: %w", conn.ID, err)
   407→		}
   408→	}
   409→	return nil
   410→}
   411→

// Package dex provides an embedded Dex OIDC identity provider.
package dex

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"strings"

	"github.com/dexidp/dex/storage"
)

// ConnectorConfig represents the configuration for an identity provider connector
type ConnectorConfig struct {
	// ID is the unique identifier for the connector
	ID string
	// Name is a human-readable name for the connector
	Name string
	// Type is the connector type (oidc, google, microsoft)
	Type string
	// Issuer is the OIDC issuer URL (for OIDC-based connectors)
	Issuer string
	// ClientID is the OAuth2 client ID
	ClientID string
	// ClientSecret is the OAuth2 client secret
	ClientSecret string
	// RedirectURI is the OAuth2 redirect URI
	RedirectURI string
}

// CreateConnector creates a new connector in Dex storage.
// It maps the connector config to the appropriate Dex connector type and configuration.
func (p *Provider) CreateConnector(ctx context.Context, cfg *ConnectorConfig) (*ConnectorConfig, error) {
	// Fill in the redirect URI if not provided
	if cfg.RedirectURI == "" {
		cfg.RedirectURI = p.GetRedirectURI()
	}

	storageConn, err := p.buildStorageConnector(cfg)
	if err != nil {
		return nil, fmt.Errorf("failed to build connector: %w", err)
	}

	if err := p.storage.CreateConnector(ctx, storageConn); err != nil {
		return nil, fmt.Errorf("failed to create connector: %w", err)
	}

	p.logger.Info("connector created", "id", cfg.ID, "type", cfg.Type)
	return cfg, nil
}

// GetConnector retrieves a connector by ID from Dex storage.
func (p *Provider) GetConnector(ctx context.Context, id string) (*ConnectorConfig, error) {
	conn, err := p.storage.GetConnector(ctx, id)
	if err != nil {
		if err == storage.ErrNotFound {
			return nil, err
		}
		return nil, fmt.Errorf("failed to get connector: %w", err)
	}

	return p.parseStorageConnector(conn)
}

// ListConnectors returns all connectors from Dex storage (excluding the local connector).
func (p *Provider) ListConnectors(ctx context.Context) ([]*ConnectorConfig, error) {
	connectors, err := p.storage.ListConnectors(ctx)
	if err != nil {
		return nil, fmt.Errorf("failed to list connectors: %w", err)
	}

	result := make([]*ConnectorConfig, 0, len(connectors))
	for _, conn := range connectors {
		// Skip the local password connector
		if conn.ID == "local" && conn.Type == "local" {
			continue
		}

		cfg, err := p.parseStorageConnector(conn)
		if err != nil {
			p.logger.Warn("failed to parse connector", "id", conn.ID, "error", err)
			continue
		}
		result = append(result, cfg)
	}

	return result, nil
}

// UpdateConnector updates an existing connector in Dex storage.
// It merges incoming updates with existing values to prevent data loss on partial updates.
func (p *Provider) UpdateConnector(ctx context.Context, cfg *ConnectorConfig) error {
	if err := p.storage.UpdateConnector(ctx, cfg.ID, func(old storage.Connector) (storage.Connector, error) {
		oldCfg, err := p.parseStorageConnector(old)
		if err != nil {
			return storage.Connector{}, fmt.Errorf("failed to parse existing connector: %w", err)
		}

		mergeConnectorConfig(cfg, oldCfg)

		storageConn, err := p.buildStorageConnector(cfg)
		if err != nil {
			return storage.Connector{}, fmt.Errorf("failed to build connector: %w", err)
		}
		return storageConn, nil
	}); err != nil {
		return fmt.Errorf("failed to update connector: %w", err)
	}

	p.logger.Info("connector updated", "id", cfg.ID, "type", cfg.Type)
	return nil
}

// mergeConnectorConfig preserves existing values for empty fields in the update.
func mergeConnectorConfig(cfg, oldCfg *ConnectorConfig) {
	if cfg.ClientSecret == "" {
		cfg.ClientSecret = oldCfg.ClientSecret
	}
	if cfg.RedirectURI == "" {
		cfg.RedirectURI = oldCfg.RedirectURI
	}
	if cfg.Issuer == "" && cfg.Type == oldCfg.Type {
		cfg.Issuer = oldCfg.Issuer
	}
	if cfg.ClientID == "" {
		cfg.ClientID = oldCfg.ClientID
	}
	if cfg.Name == "" {
		cfg.Name = oldCfg.Name
	}
}

// DeleteConnector removes a connector from Dex storage.
func (p *Provider) DeleteConnector(ctx context.Context, id string) error {
	// Prevent deletion of the local connector
	if id == "local" {
		return fmt.Errorf("cannot delete the local password connector")
	}

	if err := p.storage.DeleteConnector(ctx, id); err != nil {
		return fmt.Errorf("failed to delete connector: %w", err)
	}

	p.logger.Info("connector deleted", "id", id)
	return nil
}

// GetRedirectURI returns the default redirect URI for connectors.
func (p *Provider) GetRedirectURI() string {
	if p.config == nil {
		return ""
	}
	issuer := strings.TrimSuffix(p.config.Issuer, "/")
	if !strings.HasSuffix(issuer, "/oauth2") {
		issuer += "/oauth2"
	}
	return issuer + "/callback"
}

// buildStorageConnector creates a storage.Connector from ConnectorConfig.
// It handles the type-specific configuration for each connector type.
func (p *Provider) buildStorageConnector(cfg *ConnectorConfig) (storage.Connector, error) {
	redirectURI := p.resolveRedirectURI(cfg.RedirectURI)

	var dexType string
	var configData []byte
	var err error

	switch cfg.Type {
	case "oidc", "zitadel", "entra", "okta", "pocketid", "authentik", "keycloak":
		dexType = "oidc"
		configData, err = buildOIDCConnectorConfig(cfg, redirectURI)
	case "google":
		dexType = "google"
		configData, err = buildOAuth2ConnectorConfig(cfg, redirectURI)
	case "microsoft":
		dexType = "microsoft"
		configData, err = buildOAuth2ConnectorConfig(cfg, redirectURI)
	default:
		return storage.Connector{}, fmt.Errorf("unsupported connector type: %s", cfg.Type)
	}
	if err != nil {
		return storage.Connector{}, err
	}

	return storage.Connector{ID: cfg.ID, Type: dexType, Name: cfg.Name, Config: configData}, nil
}

// resolveRedirectURI returns the redirect URI, using a default if not provided
func (p *Provider) resolveRedirectURI(redirectURI string) string {
	if redirectURI != "" || p.config == nil {
		return redirectURI
	}
	issuer := strings.TrimSuffix(p.config.Issuer, "/")
	if !strings.HasSuffix(issuer, "/oauth2") {
		issuer += "/oauth2"
	}
	return issuer + "/callback"
}

// buildOIDCConnectorConfig creates config for OIDC-based connectors
func buildOIDCConnectorConfig(cfg *ConnectorConfig, redirectURI string) ([]byte, error) {
	oidcConfig := map[string]interface{}{
		"issuer":               cfg.Issuer,
		"clientID":             cfg.ClientID,
		"clientSecret":         cfg.ClientSecret,
		"redirectURI":          redirectURI,
		"scopes":               []string{"openid", "profile", "email"},
		"insecureEnableGroups": true,
		//some providers don't return email verified, so we need to skip it if not present (e.g., Entra, Okta, Duo)
		"insecureSkipEmailVerified": true,
	}
	switch cfg.Type {
	case "zitadel":
		oidcConfig["getUserInfo"] = true
	case "entra":
		oidcConfig["claimMapping"] = map[string]string{"email": "preferred_username"}
	case "okta":
		oidcConfig["scopes"] = []string{"openid", "profile", "email", "groups"}
	case "pocketid":
		oidcConfig["scopes"] = []string{"openid", "profile", "email", "groups"}
	}
	return encodeConnectorConfig(oidcConfig)
}

// buildOAuth2ConnectorConfig creates config for OAuth2 connectors (google, microsoft)
func buildOAuth2ConnectorConfig(cfg *ConnectorConfig, redirectURI string) ([]byte, error) {
	return encodeConnectorConfig(map[string]interface{}{
		"clientID":     cfg.ClientID,
		"clientSecret": cfg.ClientSecret,
		"redirectURI":  redirectURI,
	})
}

// parseStorageConnector converts a storage.Connector back to ConnectorConfig.
// It infers the original identity provider type from the Dex connector type and ID.
func (p *Provider) parseStorageConnector(conn storage.Connector) (*ConnectorConfig, error) {
	cfg := &ConnectorConfig{
		ID:   conn.ID,
		Name: conn.Name,
	}

	if len(conn.Config) == 0 {
		cfg.Type = conn.Type
		return cfg, nil
	}

	var configMap map[string]interface{}
	if err := decodeConnectorConfig(conn.Config, &configMap); err != nil {
		return nil, fmt.Errorf("failed to parse connector config: %w", err)
	}

	// Extract common fields
	if v, ok := configMap["clientID"].(string); ok {
		cfg.ClientID = v
	}
	if v, ok := configMap["clientSecret"].(string); ok {
		cfg.ClientSecret = v
	}
	if v, ok := configMap["redirectURI"].(string); ok {
		cfg.RedirectURI = v
	}
	if v, ok := configMap["issuer"].(string); ok {
		cfg.Issuer = v
	}

	// Infer the original identity provider type from Dex connector type and ID
	cfg.Type = inferIdentityProviderType(conn.Type, conn.ID, configMap)

	return cfg, nil
}

// inferIdentityProviderType determines the original identity provider type
// based on the Dex connector type, connector ID, and configuration.
func inferIdentityProviderType(dexType, connectorID string, _ map[string]interface{}) string {
	if dexType != "oidc" {
		return dexType
	}
	return inferOIDCProviderType(connectorID)
}

// inferOIDCProviderType infers the specific OIDC provider from connector ID
func inferOIDCProviderType(connectorID string) string {
	connectorIDLower := strings.ToLower(connectorID)
	for _, provider := range []string{"pocketid", "zitadel", "entra", "okta", "authentik", "keycloak"} {
		if strings.Contains(connectorIDLower, provider) {
			return provider
		}
	}
	return "oidc"
}

// encodeConnectorConfig serializes connector config to JSON bytes.
func encodeConnectorConfig(config map[string]interface{}) ([]byte, error) {
	return json.Marshal(config)
}

// decodeConnectorConfig deserializes connector config from JSON bytes.
func decodeConnectorConfig(data []byte, v interface{}) error {
	return json.Unmarshal(data, v)
}

// ensureLocalConnector creates a local (password) connector if it doesn't exist
func ensureLocalConnector(ctx context.Context, stor storage.Storage) error {
	// Check specifically for the local connector
	_, err := stor.GetConnector(ctx, "local")
	if err == nil {
		// Local connector already exists
		return nil
	}
	if !errors.Is(err, storage.ErrNotFound) {
		return fmt.Errorf("failed to get local connector: %w", err)
	}

	// Create a local connector for password authentication
	localConnector := storage.Connector{
		ID:   "local",
		Type: "local",
		Name: "Email",
	}

	if err := stor.CreateConnector(ctx, localConnector); err != nil {
		return fmt.Errorf("failed to create local connector: %w", err)
	}

	return nil
}

// HasNonLocalConnectors checks if there are any connectors other than the local connector.
func (p *Provider) HasNonLocalConnectors(ctx context.Context) (bool, error) {
	connectors, err := p.storage.ListConnectors(ctx)
	if err != nil {
		return false, fmt.Errorf("failed to list connectors: %w", err)
	}

	p.logger.Info("checking for non-local connectors", "total_connectors", len(connectors))
	for _, conn := range connectors {
		p.logger.Info("found connector in storage", "id", conn.ID, "type", conn.Type, "name", conn.Name)
		if conn.ID != "local" || conn.Type != "local" {
			p.logger.Info("found non-local connector", "id", conn.ID)
			return true, nil
		}
	}
	p.logger.Info("no non-local connectors found")
	return false, nil
}

// DisableLocalAuth removes the local (password) connector.
// Returns an error if no other connectors are configured.
func (p *Provider) DisableLocalAuth(ctx context.Context) error {
	hasOthers, err := p.HasNonLocalConnectors(ctx)
	if err != nil {
		return err
	}
	if !hasOthers {
		return fmt.Errorf("cannot disable local authentication: no other identity providers configured")
	}

	// Check if local connector exists
	_, err = p.storage.GetConnector(ctx, "local")
	if errors.Is(err, storage.ErrNotFound) {
		// Already disabled
		return nil
	}
	if err != nil {
		return fmt.Errorf("failed to check local connector: %w", err)
	}

	// Delete the local connector
	if err := p.storage.DeleteConnector(ctx, "local"); err != nil {
		return fmt.Errorf("failed to delete local connector: %w", err)
	}

	p.logger.Info("local authentication disabled")
	return nil
}

// EnableLocalAuth creates the local (password) connector if it doesn't exist.
func (p *Provider) EnableLocalAuth(ctx context.Context) error {
	return ensureLocalConnector(ctx, p.storage)
}

// ensureStaticConnectors creates or updates static connectors in storage
func ensureStaticConnectors(ctx context.Context, stor storage.Storage, connectors []Connector) error {
	for _, conn := range connectors {
		storConn, err := conn.ToStorageConnector()
		if err != nil {
			return fmt.Errorf("failed to convert connector %s: %w", conn.ID, err)
		}
		_, err = stor.GetConnector(ctx, conn.ID)
		if err == storage.ErrNotFound {
			if err := stor.CreateConnector(ctx, storConn); err != nil {
				return fmt.Errorf("failed to create connector %s: %w", conn.ID, err)
			}
			continue
		}
		if err != nil {
			return fmt.Errorf("failed to get connector %s: %w", conn.ID, err)
		}
		if err := stor.UpdateConnector(ctx, conn.ID, func(old storage.Connector) (storage.Connector, error) {
			old.Name = storConn.Name
			old.Config = storConn.Config
			return old, nil
		}); err != nil {
			return fmt.Errorf("failed to update connector %s: %w", conn.ID, err)
		}
	}
	return nil
}


No files found


     1→// Package dex provides an embedded Dex OIDC identity provider.
     2→package dex
     3→
     4→import (
     5→	"context"
     6→	"encoding/base64"
     7→	"errors"
     8→	"fmt"
     9→	"log/slog"
    10→	"net"
    11→	"net/http"
    12→	"os"
    13→	"path/filepath"
    14→	"strings"
    15→	"sync"
    16→	"time"
    17→
    18→	dexapi "github.com/dexidp/dex/api/v2"
    19→	"github.com/dexidp/dex/server"
    20→	"github.com/dexidp/dex/storage"
    21→	"github.com/dexidp/dex/storage/sql"
    22→	"github.com/google/uuid"
    23→	"github.com/prometheus/client_golang/prometheus"
    24→	"golang.org/x/crypto/bcrypt"
    25→	"google.golang.org/grpc"
    26→)
    27→
    28→// Config matches what management/internals/server/server.go expects
    29→type Config struct {
    30→	Issuer  string
    31→	Port    int
    32→	DataDir string
    33→	DevMode bool
    34→
    35→	// GRPCAddr is the address for the gRPC API (e.g., ":5557"). Empty disables gRPC.
    36→	GRPCAddr string
    37→}
    38→
    39→// Provider wraps a Dex server
    40→type Provider struct {
    41→	config       *Config
    42→	yamlConfig   *YAMLConfig
    43→	dexServer    *server.Server
    44→	httpServer   *http.Server
    45→	listener     net.Listener
    46→	grpcServer   *grpc.Server
    47→	grpcListener net.Listener
    48→	storage      storage.Storage
    49→	logger       *slog.Logger
    50→	mu           sync.Mutex
    51→	running      bool
    52→}
    53→
    54→// NewProvider creates and initializes the Dex server
    55→func NewProvider(ctx context.Context, config *Config) (*Provider, error) {
    56→	if config.Issuer == "" {
    57→		return nil, fmt.Errorf("issuer is required")
    58→	}
    59→	if config.Port <= 0 {
    60→		return nil, fmt.Errorf("invalid port")
    61→	}
    62→	if config.DataDir == "" {
    63→		return nil, fmt.Errorf("data directory is required")
    64→	}
    65→
    66→	logger := slog.New(slog.NewTextHandler(os.Stderr, nil))
    67→
    68→	// Ensure data directory exists
    69→	if err := os.MkdirAll(config.DataDir, 0700); err != nil {
    70→		return nil, fmt.Errorf("failed to create data directory: %w", err)
    71→	}
    72→
    73→	// Initialize SQLite storage
    74→	dbPath := filepath.Join(config.DataDir, "oidc.db")
    75→	sqliteConfig := &sql.SQLite3{File: dbPath}
    76→	stor, err := sqliteConfig.Open(logger)
    77→	if err != nil {
    78→		return nil, fmt.Errorf("failed to open storage: %w", err)
    79→	}
    80→
    81→	// Ensure a local connector exists (for password authentication)
    82→	if err := ensureLocalConnector(ctx, stor); err != nil {
    83→		stor.Close()
    84→		return nil, fmt.Errorf("failed to ensure local connector: %w", err)
    85→	}
    86→
    87→	// Ensure issuer ends with /oauth2 for proper path mounting
    88→	issuer := strings.TrimSuffix(config.Issuer, "/")
    89→	if !strings.HasSuffix(issuer, "/oauth2") {
    90→		issuer += "/oauth2"
    91→	}
    92→
    93→	// Build refresh token policy (required to avoid nil pointer panics)
    94→	refreshPolicy, err := server.NewRefreshTokenPolicy(logger, false, "", "", "")
    95→	if err != nil {
    96→		stor.Close()
    97→		return nil, fmt.Errorf("failed to create refresh token policy: %w", err)
    98→	}
    99→
   100→	// Build Dex server config - use Dex's types directly
   101→	dexConfig := server.Config{
   102→		Issuer:                 issuer,
   103→		Storage:                stor,
   104→		SkipApprovalScreen:     true,
   105→		SupportedResponseTypes: []string{"code"},
   106→		Logger:                 logger,
   107→		PrometheusRegistry:     prometheus.NewRegistry(),
   108→		RotateKeysAfter:        6 * time.Hour,
   109→		IDTokensValidFor:       24 * time.Hour,
   110→		RefreshTokenPolicy:     refreshPolicy,
   111→		Web: server.WebConfig{
   112→			Issuer: "NetBird",
   113→		},
   114→	}
   115→
   116→	dexSrv, err := server.NewServer(ctx, dexConfig)
   117→	if err != nil {
   118→		stor.Close()
   119→		return nil, fmt.Errorf("failed to create dex server: %w", err)
   120→	}
   121→
   122→	return &Provider{
   123→		config:    config,
   124→		dexServer: dexSrv,
   125→		storage:   stor,
   126→		logger:    logger,
   127→	}, nil
   128→}
   129→
   130→// NewProviderFromYAML creates and initializes the Dex server from a YAMLConfig
   131→func NewProviderFromYAML(ctx context.Context, yamlConfig *YAMLConfig) (*Provider, error) {
   132→	// Configure log level from config, default to WARN to avoid logging sensitive data (emails)
   133→	logLevel := slog.LevelWarn
   134→	if yamlConfig.Logger.Level != "" {
   135→		switch strings.ToLower(yamlConfig.Logger.Level) {
   136→		case "debug":
   137→			logLevel = slog.LevelDebug
   138→		case "info":
   139→			logLevel = slog.LevelInfo
   140→		case "warn", "warning":
   141→			logLevel = slog.LevelWarn
   142→		case "error":
   143→			logLevel = slog.LevelError
   144→		}
   145→	}
   146→	logger := slog.New(NewLogrusHandler(logLevel))
   147→
   148→	stor, err := yamlConfig.Storage.OpenStorage(logger)
   149→	if err != nil {
   150→		return nil, fmt.Errorf("failed to open storage: %w", err)
   151→	}
   152→
   153→	if err := initializeStorage(ctx, stor, yamlConfig); err != nil {
   154→		stor.Close()
   155→		return nil, err
   156→	}
   157→
   158→	dexConfig := buildDexConfig(yamlConfig, stor, logger)
   159→	dexConfig.RefreshTokenPolicy, err = yamlConfig.GetRefreshTokenPolicy(logger)
   160→	if err != nil {
   161→		stor.Close()
   162→		return nil, fmt.Errorf("failed to create refresh token policy: %w", err)
   163→	}
   164→
   165→	dexSrv, err := server.NewServer(ctx, dexConfig)
   166→	if err != nil {
   167→		stor.Close()
   168→		return nil, fmt.Errorf("failed to create dex server: %w", err)
   169→	}
   170→
   171→	return &Provider{
   172→		config:     &Config{Issuer: yamlConfig.Issuer, GRPCAddr: yamlConfig.GRPC.Addr},
   173→		yamlConfig: yamlConfig,
   174→		dexServer:  dexSrv,
   175→		storage:    stor,
   176→		logger:     logger,
   177→	}, nil
   178→}
   179→
   180→// initializeStorage sets up connectors, passwords, and clients in storage
   181→func initializeStorage(ctx context.Context, stor storage.Storage, cfg *YAMLConfig) error {
   182→	if cfg.EnablePasswordDB {
   183→		if err := ensureLocalConnector(ctx, stor); err != nil {
   184→			return fmt.Errorf("failed to ensure local connector: %w", err)
   185→		}
   186→	}
   187→	if err := ensureStaticPasswords(ctx, stor, cfg.StaticPasswords); err != nil {
   188→		return err
   189→	}
   190→	if err := ensureStaticClients(ctx, stor, cfg.StaticClients); err != nil {
   191→		return err
   192→	}
   193→	return ensureStaticConnectors(ctx, stor, cfg.StaticConnectors)
   194→}
   195→
   196→// ensureStaticPasswords creates or updates static passwords in storage
   197→func ensureStaticPasswords(ctx context.Context, stor storage.Storage, passwords []Password) error {
   198→	for _, pw := range passwords {
   199→		existing, err := stor.GetPassword(ctx, pw.Email)
   200→		if errors.Is(err, storage.ErrNotFound) {
   201→			if err := stor.CreatePassword(ctx, storage.Password(pw)); err != nil {
   202→				return fmt.Errorf("failed to create password for %s: %w", pw.Email, err)
   203→			}
   204→			continue
   205→		}
   206→		if err != nil {
   207→			return fmt.Errorf("failed to get password for %s: %w", pw.Email, err)
   208→		}
   209→		if string(existing.Hash) != string(pw.Hash) {
   210→			if err := stor.UpdatePassword(ctx, pw.Email, func(old storage.Password) (storage.Password, error) {
   211→				old.Hash = pw.Hash
   212→				old.Username = pw.Username
   213→				return old, nil
   214→			}); err != nil {
   215→				return fmt.Errorf("failed to update password for %s: %w", pw.Email, err)
   216→			}
   217→		}
   218→	}
   219→	return nil
   220→}
   221→
   222→// ensureStaticClients creates or updates static clients in storage
   223→func ensureStaticClients(ctx context.Context, stor storage.Storage, clients []storage.Client) error {
   224→	for _, client := range clients {
   225→		_, err := stor.GetClient(ctx, client.ID)
   226→		if errors.Is(err, storage.ErrNotFound) {
   227→			if err := stor.CreateClient(ctx, client); err != nil {
   228→				return fmt.Errorf("failed to create client %s: %w", client.ID, err)
   229→			}
   230→			continue
   231→		}
   232→		if err != nil {
   233→			return fmt.Errorf("failed to get client %s: %w", client.ID, err)
   234→		}
   235→		if err := stor.UpdateClient(ctx, client.ID, func(old storage.Client) (storage.Client, error) {
   236→			old.RedirectURIs = client.RedirectURIs
   237→			old.Name = client.Name
   238→			old.Public = client.Public
   239→			return old, nil
   240→		}); err != nil {
   241→			return fmt.Errorf("failed to update client %s: %w", client.ID, err)
   242→		}
   243→	}
   244→	return nil
   245→}
   246→
   247→// buildDexConfig creates a server.Config with defaults applied
   248→func buildDexConfig(yamlConfig *YAMLConfig, stor storage.Storage, logger *slog.Logger) server.Config {
   249→	cfg := yamlConfig.ToServerConfig(stor, logger)
   250→	cfg.PrometheusRegistry = prometheus.NewRegistry()
   251→	if cfg.RotateKeysAfter == 0 {
   252→		cfg.RotateKeysAfter = 24 * 30 * time.Hour
   253→	}
   254→	if cfg.IDTokensValidFor == 0 {
   255→		cfg.IDTokensValidFor = 24 * time.Hour
   256→	}
   257→	if cfg.Web.Issuer == "" {
   258→		cfg.Web.Issuer = "NetBird"
   259→	}
   260→	if len(cfg.SupportedResponseTypes) == 0 {
   261→		cfg.SupportedResponseTypes = []string{"code"}
   262→	}
   263→	return cfg
   264→}
   265→
   266→// Start starts the HTTP server and optionally the gRPC API server
   267→func (p *Provider) Start(_ context.Context) error {
   268→	p.mu.Lock()
   269→	defer p.mu.Unlock()
   270→
   271→	if p.running {
   272→		return fmt.Errorf("already running")
   273→	}
   274→
   275→	// Determine listen address from config
   276→	var addr string
   277→	if p.yamlConfig != nil {
   278→		addr = p.yamlConfig.Web.HTTP
   279→		if addr == "" {
   280→			addr = p.yamlConfig.Web.HTTPS
   281→		}
   282→	} else if p.config != nil && p.config.Port > 0 {
   283→		addr = fmt.Sprintf(":%d", p.config.Port)
   284→	}
   285→	if addr == "" {
   286→		return fmt.Errorf("no listen address configured")
   287→	}
   288→
   289→	listener, err := net.Listen("tcp", addr)
   290→	if err != nil {
   291→		return fmt.Errorf("failed to listen on %s: %w", addr, err)
   292→	}
   293→	p.listener = listener
   294→
   295→	// Mount Dex at /oauth2/ path for reverse proxy compatibility
   296→	// Don't strip the prefix - Dex's issuer includes /oauth2 so it expects the full path
   297→	mux := http.NewServeMux()
   298→	mux.Handle("/oauth2/", p.dexServer)
   299→
   300→	p.httpServer = &http.Server{Handler: mux}
   301→	p.running = true
   302→
   303→	go func() {
   304→		if err := p.httpServer.Serve(listener); err != nil && err != http.ErrServerClosed {
   305→			p.logger.Error("http server error", "error", err)
   306→		}
   307→	}()
   308→
   309→	// Start gRPC API server if configured
   310→	if p.config.GRPCAddr != "" {
   311→		if err := p.startGRPCServer(); err != nil {
   312→			// Clean up HTTP server on failure
   313→			_ = p.httpServer.Close()
   314→			_ = p.listener.Close()
   315→			return fmt.Errorf("failed to start gRPC server: %w", err)
   316→		}
   317→	}
   318→
   319→	p.logger.Info("HTTP server started", "addr", addr)
   320→	return nil
   321→}
   322→
   323→// startGRPCServer starts the gRPC API server using Dex's built-in API
   324→func (p *Provider) startGRPCServer() error {
   325→	grpcListener, err := net.Listen("tcp", p.config.GRPCAddr)
   326→	if err != nil {
   327→		return fmt.Errorf("failed to listen on %s: %w", p.config.GRPCAddr, err)
   328→	}
   329→	p.grpcListener = grpcListener
   330→
   331→	p.grpcServer = grpc.NewServer()
   332→	// Use Dex's built-in API server implementation
   333→	// server.NewAPI(storage, logger, version, dexServer)
   334→	dexapi.RegisterDexServer(p.grpcServer, server.NewAPI(p.storage, p.logger, "netbird-dex", p.dexServer))
   335→
   336→	go func() {
   337→		if err := p.grpcServer.Serve(grpcListener); err != nil {
   338→			p.logger.Error("grpc server error", "error", err)
   339→		}
   340→	}()
   341→
   342→	p.logger.Info("gRPC API server started", "addr", p.config.GRPCAddr)
   343→	return nil
   344→}
   345→
   346→// Stop gracefully shuts down
   347→func (p *Provider) Stop(ctx context.Context) error {
   348→	p.mu.Lock()
   349→	defer p.mu.Unlock()
   350→
   351→	if !p.running {
   352→		return nil
   353→	}
   354→
   355→	var errs []error
   356→
   357→	// Stop gRPC server first
   358→	if p.grpcServer != nil {
   359→		p.grpcServer.GracefulStop()
   360→		p.grpcServer = nil
   361→	}
   362→	if p.grpcListener != nil {
   363→		p.grpcListener.Close()
   364→		p.grpcListener = nil
   365→	}
   366→
   367→	if p.httpServer != nil {
   368→		if err := p.httpServer.Shutdown(ctx); err != nil {
   369→			errs = append(errs, err)
   370→		}
   371→	}
   372→
   373→	// Explicitly close listener as fallback (Shutdown should do this, but be safe)
   374→	if p.listener != nil {
   375→		if err := p.listener.Close(); err != nil {
   376→			// Ignore "use of closed network connection" - expected after Shutdown
   377→			if !strings.Contains(err.Error(), "use of closed") {
   378→				errs = append(errs, err)
   379→			}
   380→		}
   381→		p.listener = nil
   382→	}
   383→
   384→	if p.storage != nil {
   385→		if err := p.storage.Close(); err != nil {
   386→			errs = append(errs, err)
   387→		}
   388→	}
   389→
   390→	p.httpServer = nil
   391→	p.running = false
   392→
   393→	if len(errs) > 0 {
   394→		return fmt.Errorf("shutdown errors: %v", errs)
   395→	}
   396→	return nil
   397→}
   398→
   399→// EnsureDefaultClients creates dashboard and CLI OAuth clients
   400→// Uses Dex's storage.Client directly - no custom wrappers
   401→func (p *Provider) EnsureDefaultClients(ctx context.Context, dashboardURIs, cliURIs []string) error {
   402→	clients := []storage.Client{
   403→		{
   404→			ID:           "netbird-dashboard",
   405→			Name:         "NetBird Dashboard",
   406→			RedirectURIs: dashboardURIs,
   407→			Public:       true,
   408→		},
   409→		{
   410→			ID:           "netbird-cli",
   411→			Name:         "NetBird CLI",
   412→			RedirectURIs: cliURIs,
   413→			Public:       true,
   414→		},
   415→	}
   416→
   417→	for _, client := range clients {
   418→		_, err := p.storage.GetClient(ctx, client.ID)
   419→		if err == storage.ErrNotFound {
   420→			if err := p.storage.CreateClient(ctx, client); err != nil {
   421→				return fmt.Errorf("failed to create client %s: %w", client.ID, err)
   422→			}
   423→			continue
   424→		}
   425→		if err != nil {
   426→			return fmt.Errorf("failed to get client %s: %w", client.ID, err)
   427→		}
   428→		// Update if exists
   429→		if err := p.storage.UpdateClient(ctx, client.ID, func(old storage.Client) (storage.Client, error) {
   430→			old.RedirectURIs = client.RedirectURIs
   431→			return old, nil
   432→		}); err != nil {
   433→			return fmt.Errorf("failed to update client %s: %w", client.ID, err)
   434→		}
   435→	}
   436→
   437→	p.logger.Info("default OIDC clients ensured")
   438→	return nil
   439→}
   440→
   441→// Storage returns the underlying Dex storage for direct access
   442→// Users can use storage.Client, storage.Password, storage.Connector directly
   443→func (p *Provider) Storage() storage.Storage {
   444→	return p.storage
   445→}
   446→
   447→// Handler returns the Dex server as an http.Handler for embedding in another server.
   448→// The handler expects requests with path prefix "/oauth2/".
   449→func (p *Provider) Handler() http.Handler {
   450→	return p.dexServer
   451→}
   452→
   453→// CreateUser creates a new user with the given email, username, and password.
   454→// Returns the encoded user ID in Dex's format (base64-encoded protobuf with connector ID).
   455→func (p *Provider) CreateUser(ctx context.Context, email, username, password string) (string, error) {
   456→	hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
   457→	if err != nil {
   458→		return "", fmt.Errorf("failed to hash password: %w", err)
   459→	}
   460→
   461→	userID := uuid.New().String()
   462→	err = p.storage.CreatePassword(ctx, storage.Password{
   463→		Email:    email,
   464→		Username: username,
   465→		UserID:   userID,
   466→		Hash:     hash,
   467→	})
   468→	if err != nil {
   469→		return "", err
   470→	}
   471→
   472→	// Encode the user ID in Dex's format: base64(protobuf{user_id, connector_id})
   473→	// This matches the format Dex uses in JWT tokens
   474→	encodedID := EncodeDexUserID(userID, "local")
   475→	return encodedID, nil
   476→}
   477→
   478→// EncodeDexUserID encodes user ID and connector ID into Dex's base64-encoded protobuf format.
   479→// Dex uses this format for the 'sub' claim in JWT tokens.
   480→// Format: base64(protobuf message with field 1 = user_id, field 2 = connector_id)
   481→func EncodeDexUserID(userID, connectorID string) string {
   482→	// Manually encode protobuf: field 1 (user_id) and field 2 (connector_id)
   483→	// Wire type 2 (length-delimited) for strings
   484→	var buf []byte
   485→
   486→	// Field 1: user_id (tag = 0x0a = field 1, wire type 2)
   487→	buf = append(buf, 0x0a)
   488→	buf = append(buf, byte(len(userID)))
   489→	buf = append(buf, []byte(userID)...)
   490→
   491→	// Field 2: connector_id (tag = 0x12 = field 2, wire type 2)
   492→	buf = append(buf, 0x12)
   493→	buf = append(buf, byte(len(connectorID)))
   494→	buf = append(buf, []byte(connectorID)...)
   495→
   496→	return base64.RawStdEncoding.EncodeToString(buf)
   497→}
   498→
   499→// DecodeDexUserID decodes Dex's base64-encoded user ID back to the raw user ID and connector ID.
   500→func DecodeDexUserID(encodedID string) (userID, connectorID string, err error) {
   501→	// Try RawStdEncoding first, then StdEncoding (with padding)
   502→	buf, err := base64.RawStdEncoding.DecodeString(encodedID)
   503→	if err != nil {
   504→		buf, err = base64.StdEncoding.DecodeString(encodedID)
   505→		if err != nil {
   506→			return "", "", fmt.Errorf("failed to decode base64: %w", err)
   507→		}
   508→	}
   509→
   510→	// Parse protobuf manually
   511→	i := 0
   512→	for i < len(buf) {
   513→		if i >= len(buf) {
   514→			break
   515→		}
   516→		tag := buf[i]
   517→		i++
   518→
   519→		fieldNum := tag >> 3
   520→		wireType := tag & 0x07
   521→
   522→		if wireType != 2 { // We only expect length-delimited strings
   523→			return "", "", fmt.Errorf("unexpected wire type %d", wireType)
   524→		}
   525→
   526→		if i >= len(buf) {
   527→			return "", "", fmt.Errorf("truncated message")
   528→		}
   529→		length := int(buf[i])
   530→		i++
   531→
   532→		if i+length > len(buf) {
   533→			return "", "", fmt.Errorf("truncated string field")
   534→		}
   535→		value := string(buf[i : i+length])
   536→		i += length
   537→
   538→		switch fieldNum {
   539→		case 1:
   540→			userID = value
   541→		case 2:
   542→			connectorID = value
   543→		}
   544→	}
   545→
   546→	return userID, connectorID, nil
   547→}
   548→
   549→// GetUser returns a user by email
   550→func (p *Provider) GetUser(ctx context.Context, email string) (storage.Password, error) {
   551→	return p.storage.GetPassword(ctx, email)
   552→}
   553→
   554→// GetUserByID returns a user by user ID.
   555→// The userID can be either an encoded Dex ID (base64 protobuf) or a raw UUID.
   556→// Note: This requires iterating through all users since dex storage doesn't index by userID.
   557→func (p *Provider) GetUserByID(ctx context.Context, userID string) (storage.Password, error) {
   558→	// Try to decode the user ID in case it's encoded
   559→	rawUserID, _, err := DecodeDexUserID(userID)
   560→	if err != nil {
   561→		// If decoding fails, assume it's already a raw UUID
   562→		rawUserID = userID
   563→	}
   564→
   565→	users, err := p.storage.ListPasswords(ctx)
   566→	if err != nil {
   567→		return storage.Password{}, fmt.Errorf("failed to list users: %w", err)
   568→	}
   569→	for _, user := range users {
   570→		if user.UserID == rawUserID {
   571→			return user, nil
   572→		}
   573→	}
   574→	return storage.Password{}, storage.ErrNotFound
   575→}
   576→
   577→// DeleteUser removes a user by email
   578→func (p *Provider) DeleteUser(ctx context.Context, email string) error {
   579→	return p.storage.DeletePassword(ctx, email)
   580→}
   581→
   582→// ListUsers returns all users
   583→func (p *Provider) ListUsers(ctx context.Context) ([]storage.Password, error) {
   584→	return p.storage.ListPasswords(ctx)
   585→}
   586→
   587→// UpdateUserPassword updates the password for a user identified by userID.
   588→// The userID can be either an encoded Dex ID (base64 protobuf) or a raw UUID.
   589→// It verifies the current password before updating.
   590→func (p *Provider) UpdateUserPassword(ctx context.Context, userID string, oldPassword, newPassword string) error {
   591→	// Get the user by ID to find their email
   592→	user, err := p.GetUserByID(ctx, userID)
   593→	if err != nil {
   594→		return fmt.Errorf("failed to get user: %w", err)
   595→	}
   596→
   597→	// Verify old password
   598→	if err := bcrypt.CompareHashAndPassword(user.Hash, []byte(oldPassword)); err != nil {
   599→		return fmt.Errorf("current password is incorrect")
   600→	}
   601→
   602→	// Hash the new password
   603→	newHash, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
   604→	if err != nil {
   605→		return fmt.Errorf("failed to hash new password: %w", err)
   606→	}
   607→
   608→	// Update the password in storage
   609→	err = p.storage.UpdatePassword(ctx, user.Email, func(old storage.Password) (storage.Password, error) {
   610→		old.Hash = newHash
   611→		return old, nil
   612→	})
   613→	if err != nil {
   614→		return fmt.Errorf("failed to update password: %w", err)
   615→	}
   616→
   617→	return nil
   618→}
   619→
   620→// GetIssuer returns the OIDC issuer URL.
   621→func (p *Provider) GetIssuer() string {
   622→	if p.config == nil {
   623→		return ""
   624→	}
   625→	issuer := strings.TrimSuffix(p.config.Issuer, "/")
   626→	if !strings.HasSuffix(issuer, "/oauth2") {
   627→		issuer += "/oauth2"
   628→	}
   629→	return issuer
   630→}
   631→
   632→// GetKeysLocation returns the JWKS endpoint URL for token validation.
   633→func (p *Provider) GetKeysLocation() string {
   634→	issuer := p.GetIssuer()
   635→	if issuer == "" {
   636→		return ""
   637→	}
   638→	return issuer + "/keys"
   639→}
   640→
   641→// GetTokenEndpoint returns the OAuth2 token endpoint URL.
   642→func (p *Provider) GetTokenEndpoint() string {
   643→	issuer := p.GetIssuer()
   644→	if issuer == "" {
   645→		return ""
   646→	}
   647→	return issuer + "/token"
   648→}
   649→
   650→// GetDeviceAuthEndpoint returns the OAuth2 device authorization endpoint URL.
   651→func (p *Provider) GetDeviceAuthEndpoint() string {
   652→	issuer := p.GetIssuer()
   653→	if issuer == "" {
   654→		return ""
   655→	}
   656→	return issuer + "/device/code"
   657→}
   658→
   659→// GetAuthorizationEndpoint returns the OAuth2 authorization endpoint URL.
   660→func (p *Provider) GetAuthorizationEndpoint() string {
   661→	issuer := p.GetIssuer()
   662→	if issuer == "" {
   663→		return ""
   664→	}
   665→	return issuer + "/auth"
   666→}
   667→

// Package dex provides an embedded Dex OIDC identity provider.
package dex

import (
	"context"
	"encoding/base64"
	"errors"
	"fmt"
	"log/slog"
	"net"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"time"

	dexapi "github.com/dexidp/dex/api/v2"
	"github.com/dexidp/dex/server"
	"github.com/dexidp/dex/storage"
	"github.com/dexidp/dex/storage/sql"
	"github.com/google/uuid"
	"github.com/prometheus/client_golang/prometheus"
	"golang.org/x/crypto/bcrypt"
	"google.golang.org/grpc"
)

// Config matches what management/internals/server/server.go expects
type Config struct {
	Issuer  string
	Port    int
	DataDir string
	DevMode bool

	// GRPCAddr is the address for the gRPC API (e.g., ":5557"). Empty disables gRPC.
	GRPCAddr string
}

// Provider wraps a Dex server
type Provider struct {
	config       *Config
	yamlConfig   *YAMLConfig
	dexServer    *server.Server
	httpServer   *http.Server
	listener     net.Listener
	grpcServer   *grpc.Server
	grpcListener net.Listener
	storage      storage.Storage
	logger       *slog.Logger
	mu           sync.Mutex
	running      bool
}

// NewProvider creates and initializes the Dex server
func NewProvider(ctx context.Context, config *Config) (*Provider, error) {
	if config.Issuer == "" {
		return nil, fmt.Errorf("issuer is required")
	}
	if config.Port <= 0 {
		return nil, fmt.Errorf("invalid port")
	}
	if config.DataDir == "" {
		return nil, fmt.Errorf("data directory is required")
	}

	logger := slog.New(slog.NewTextHandler(os.Stderr, nil))

	// Ensure data directory exists
	if err := os.MkdirAll(config.DataDir, 0700); err != nil {
		return nil, fmt.Errorf("failed to create data directory: %w", err)
	}

	// Initialize SQLite storage
	dbPath := filepath.Join(config.DataDir, "oidc.db")
	sqliteConfig := &sql.SQLite3{File: dbPath}
	stor, err := sqliteConfig.Open(logger)
	if err != nil {
		return nil, fmt.Errorf("failed to open storage: %w", err)
	}

	// Ensure a local connector exists (for password authentication)
	if err := ensureLocalConnector(ctx, stor); err != nil {
		stor.Close()
		return nil, fmt.Errorf("failed to ensure local connector: %w", err)
	}

	// Ensure issuer ends with /oauth2 for proper path mounting
	issuer := strings.TrimSuffix(config.Issuer, "/")
	if !strings.HasSuffix(issuer, "/oauth2") {
		issuer += "/oauth2"
	}

	// Build refresh token policy (required to avoid nil pointer panics)
	refreshPolicy, err := server.NewRefreshTokenPolicy(logger, false, "", "", "")
	if err != nil {
		stor.Close()
		return nil, fmt.Errorf("failed to create refresh token policy: %w", err)
	}

	// Build Dex server config - use Dex's types directly
	dexConfig := server.Config{
		Issuer:                 issuer,
		Storage:                stor,
		SkipApprovalScreen:     true,
		SupportedResponseTypes: []string{"code"},
		Logger:                 logger,
		PrometheusRegistry:     prometheus.NewRegistry(),
		RotateKeysAfter:        6 * time.Hour,
		IDTokensValidFor:       24 * time.Hour,
		RefreshTokenPolicy:     refreshPolicy,
		Web: server.WebConfig{
			Issuer: "NetBird",
		},
	}

	dexSrv, err := server.NewServer(ctx, dexConfig)
	if err != nil {
		stor.Close()
		return nil, fmt.Errorf("failed to create dex server: %w", err)
	}

	return &Provider{
		config:    config,
		dexServer: dexSrv,
		storage:   stor,
		logger:    logger,
	}, nil
}

// NewProviderFromYAML creates and initializes the Dex server from a YAMLConfig
func NewProviderFromYAML(ctx context.Context, yamlConfig *YAMLConfig) (*Provider, error) {
	// Configure log level from config, default to WARN to avoid logging sensitive data (emails)
	logLevel := slog.LevelWarn
	if yamlConfig.Logger.Level != "" {
		switch strings.ToLower(yamlConfig.Logger.Level) {
		case "debug":
			logLevel = slog.LevelDebug
		case "info":
			logLevel = slog.LevelInfo
		case "warn", "warning":
			logLevel = slog.LevelWarn
		case "error":
			logLevel = slog.LevelError
		}
	}
	logger := slog.New(NewLogrusHandler(logLevel))

	stor, err := yamlConfig.Storage.OpenStorage(logger)
	if err != nil {
		return nil, fmt.Errorf("failed to open storage: %w", err)
	}

	if err := initializeStorage(ctx, stor, yamlConfig); err != nil {
		stor.Close()
		return nil, err
	}

	dexConfig := buildDexConfig(yamlConfig, stor, logger)
	dexConfig.RefreshTokenPolicy, err = yamlConfig.GetRefreshTokenPolicy(logger)
	if err != nil {
		stor.Close()
		return nil, fmt.Errorf("failed to create refresh token policy: %w", err)
	}

	dexSrv, err := server.NewServer(ctx, dexConfig)
	if err != nil {
		stor.Close()
		return nil, fmt.Errorf("failed to create dex server: %w", err)
	}

	return &Provider{
		config:     &Config{Issuer: yamlConfig.Issuer, GRPCAddr: yamlConfig.GRPC.Addr},
		yamlConfig: yamlConfig,
		dexServer:  dexSrv,
		storage:    stor,
		logger:     logger,
	}, nil
}

// initializeStorage sets up connectors, passwords, and clients in storage
func initializeStorage(ctx context.Context, stor storage.Storage, cfg *YAMLConfig) error {
	if cfg.EnablePasswordDB {
		if err := ensureLocalConnector(ctx, stor); err != nil {
			return fmt.Errorf("failed to ensure local connector: %w", err)
		}
	}
	if err := ensureStaticPasswords(ctx, stor, cfg.StaticPasswords); err != nil {
		return err
	}
	if err := ensureStaticClients(ctx, stor, cfg.StaticClients); err != nil {
		return err
	}
	return ensureStaticConnectors(ctx, stor, cfg.StaticConnectors)
}

// ensureStaticPasswords creates or updates static passwords in storage
func ensureStaticPasswords(ctx context.Context, stor storage.Storage, passwords []Password) error {
	for _, pw := range passwords {
		existing, err := stor.GetPassword(ctx, pw.Email)
		if errors.Is(err, storage.ErrNotFound) {
			if err := stor.CreatePassword(ctx, storage.Password(pw)); err != nil {
				return fmt.Errorf("failed to create password for %s: %w", pw.Email, err)
			}
			continue
		}
		if err != nil {
			return fmt.Errorf("failed to get password for %s: %w", pw.Email, err)
		}
		if string(existing.Hash) != string(pw.Hash) {
			if err := stor.UpdatePassword(ctx, pw.Email, func(old storage.Password) (storage.Password, error) {
				old.Hash = pw.Hash
				old.Username = pw.Username
				return old, nil
			}); err != nil {
				return fmt.Errorf("failed to update password for %s: %w", pw.Email, err)
			}
		}
	}
	return nil
}

// ensureStaticClients creates or updates static clients in storage
func ensureStaticClients(ctx context.Context, stor storage.Storage, clients []storage.Client) error {
	for _, client := range clients {
		_, err := stor.GetClient(ctx, client.ID)
		if errors.Is(err, storage.ErrNotFound) {
			if err := stor.CreateClient(ctx, client); err != nil {
				return fmt.Errorf("failed to create client %s: %w", client.ID, err)
			}
			continue
		}
		if err != nil {
			return fmt.Errorf("failed to get client %s: %w", client.ID, err)
		}
		if err := stor.UpdateClient(ctx, client.ID, func(old storage.Client) (storage.Client, error) {
			old.RedirectURIs = client.RedirectURIs
			old.Name = client.Name
			old.Public = client.Public
			return old, nil
		}); err != nil {
			return fmt.Errorf("failed to update client %s: %w", client.ID, err)
		}
	}
	return nil
}

// buildDexConfig creates a server.Config with defaults applied
func buildDexConfig(yamlConfig *YAMLConfig, stor storage.Storage, logger *slog.Logger) server.Config {
	cfg := yamlConfig.ToServerConfig(stor, logger)
	cfg.PrometheusRegistry = prometheus.NewRegistry()
	if cfg.RotateKeysAfter == 0 {
		cfg.RotateKeysAfter = 24 * 30 * time.Hour
	}
	if cfg.IDTokensValidFor == 0 {
		cfg.IDTokensValidFor = 24 * time.Hour
	}
	if cfg.Web.Issuer == "" {
		cfg.Web.Issuer = "NetBird"
	}
	if len(cfg.SupportedResponseTypes) == 0 {
		cfg.SupportedResponseTypes = []string{"code"}
	}
	return cfg
}

// Start starts the HTTP server and optionally the gRPC API server
func (p *Provider) Start(_ context.Context) error {
	p.mu.Lock()
	defer p.mu.Unlock()

	if p.running {
		return fmt.Errorf("already running")
	}

	// Determine listen address from config
	var addr string
	if p.yamlConfig != nil {
		addr = p.yamlConfig.Web.HTTP
		if addr == "" {
			addr = p.yamlConfig.Web.HTTPS
		}
	} else if p.config != nil && p.config.Port > 0 {
		addr = fmt.Sprintf(":%d", p.config.Port)
	}
	if addr == "" {
		return fmt.Errorf("no listen address configured")
	}

	listener, err := net.Listen("tcp", addr)
	if err != nil {
		return fmt.Errorf("failed to listen on %s: %w", addr, err)
	}
	p.listener = listener

	// Mount Dex at /oauth2/ path for reverse proxy compatibility
	// Don't strip the prefix - Dex's issuer includes /oauth2 so it expects the full path
	mux := http.NewServeMux()
	mux.Handle("/oauth2/", p.dexServer)

	p.httpServer = &http.Server{Handler: mux}
	p.running = true

	go func() {
		if err := p.httpServer.Serve(listener); err != nil && err != http.ErrServerClosed {
			p.logger.Error("http server error", "error", err)
		}
	}()

	// Start gRPC API server if configured
	if p.config.GRPCAddr != "" {
		if err := p.startGRPCServer(); err != nil {
			// Clean up HTTP server on failure
			_ = p.httpServer.Close()
			_ = p.listener.Close()
			return fmt.Errorf("failed to start gRPC server: %w", err)
		}
	}

	p.logger.Info("HTTP server started", "addr", addr)
	return nil
}

// startGRPCServer starts the gRPC API server using Dex's built-in API
func (p *Provider) startGRPCServer() error {
	grpcListener, err := net.Listen("tcp", p.config.GRPCAddr)
	if err != nil {
		return fmt.Errorf("failed to listen on %s: %w", p.config.GRPCAddr, err)
	}
	p.grpcListener = grpcListener

	p.grpcServer = grpc.NewServer()
	// Use Dex's built-in API server implementation
	// server.NewAPI(storage, logger, version, dexServer)
	dexapi.RegisterDexServer(p.grpcServer, server.NewAPI(p.storage, p.logger, "netbird-dex", p.dexServer))

	go func() {
		if err := p.grpcServer.Serve(grpcListener); err != nil {
			p.logger.Error("grpc server error", "error", err)
		}
	}()

	p.logger.Info("gRPC API server started", "addr", p.config.GRPCAddr)
	return nil
}

// Stop gracefully shuts down
func (p *Provider) Stop(ctx context.Context) error {
	p.mu.Lock()
	defer p.mu.Unlock()

	if !p.running {
		return nil
	}

	var errs []error

	// Stop gRPC server first
	if p.grpcServer != nil {
		p.grpcServer.GracefulStop()
		p.grpcServer = nil
	}
	if p.grpcListener != nil {
		p.grpcListener.Close()
		p.grpcListener = nil
	}

	if p.httpServer != nil {
		if err := p.httpServer.Shutdown(ctx); err != nil {
			errs = append(errs, err)
		}
	}

	// Explicitly close listener as fallback (Shutdown should do this, but be safe)
	if p.listener != nil {
		if err := p.listener.Close(); err != nil {
			// Ignore "use of closed network connection" - expected after Shutdown
			if !strings.Contains(err.Error(), "use of closed") {
				errs = append(errs, err)
			}
		}
		p.listener = nil
	}

	if p.storage != nil {
		if err := p.storage.Close(); err != nil {
			errs = append(errs, err)
		}
	}

	p.httpServer = nil
	p.running = false

	if len(errs) > 0 {
		return fmt.Errorf("shutdown errors: %v", errs)
	}
	return nil
}

// EnsureDefaultClients creates dashboard and CLI OAuth clients
// Uses Dex's storage.Client directly - no custom wrappers
func (p *Provider) EnsureDefaultClients(ctx context.Context, dashboardURIs, cliURIs []string) error {
	clients := []storage.Client{
		{
			ID:           "netbird-dashboard",
			Name:         "NetBird Dashboard",
			RedirectURIs: dashboardURIs,
			Public:       true,
		},
		{
			ID:           "netbird-cli",
			Name:         "NetBird CLI",
			RedirectURIs: cliURIs,
			Public:       true,
		},
	}

	for _, client := range clients {
		_, err := p.storage.GetClient(ctx, client.ID)
		if err == storage.ErrNotFound {
			if err := p.storage.CreateClient(ctx, client); err != nil {
				return fmt.Errorf("failed to create client %s: %w", client.ID, err)
			}
			continue
		}
		if err != nil {
			return fmt.Errorf("failed to get client %s: %w", client.ID, err)
		}
		// Update if exists
		if err := p.storage.UpdateClient(ctx, client.ID, func(old storage.Client) (storage.Client, error) {
			old.RedirectURIs = client.RedirectURIs
			return old, nil
		}); err != nil {
			return fmt.Errorf("failed to update client %s: %w", client.ID, err)
		}
	}

	p.logger.Info("default OIDC clients ensured")
	return nil
}

// Storage returns the underlying Dex storage for direct access
// Users can use storage.Client, storage.Password, storage.Connector directly
func (p *Provider) Storage() storage.Storage {
	return p.storage
}

// Handler returns the Dex server as an http.Handler for embedding in another server.
// The handler expects requests with path prefix "/oauth2/".
func (p *Provider) Handler() http.Handler {
	return p.dexServer
}

// CreateUser creates a new user with the given email, username, and password.
// Returns the encoded user ID in Dex's format (base64-encoded protobuf with connector ID).
func (p *Provider) CreateUser(ctx context.Context, email, username, password string) (string, error) {
	hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
	if err != nil {
		return "", fmt.Errorf("failed to hash password: %w", err)
	}

	userID := uuid.New().String()
	err = p.storage.CreatePassword(ctx, storage.Password{
		Email:    email,
		Username: username,
		UserID:   userID,
		Hash:     hash,
	})
	if err != nil {
		return "", err
	}

	// Encode the user ID in Dex's format: base64(protobuf{user_id, connector_id})
	// This matches the format Dex uses in JWT tokens
	encodedID := EncodeDexUserID(userID, "local")
	return encodedID, nil
}

// EncodeDexUserID encodes user ID and connector ID into Dex's base64-encoded protobuf format.
// Dex uses this format for the 'sub' claim in JWT tokens.
// Format: base64(protobuf message with field 1 = user_id, field 2 = connector_id)
func EncodeDexUserID(userID, connectorID string) string {
	// Manually encode protobuf: field 1 (user_id) and field 2 (connector_id)
	// Wire type 2 (length-delimited) for strings
	var buf []byte

	// Field 1: user_id (tag = 0x0a = field 1, wire type 2)
	buf = append(buf, 0x0a)
	buf = append(buf, byte(len(userID)))
	buf = append(buf, []byte(userID)...)

	// Field 2: connector_id (tag = 0x12 = field 2, wire type 2)
	buf = append(buf, 0x12)
	buf = append(buf, byte(len(connectorID)))
	buf = append(buf, []byte(connectorID)...)

	return base64.RawStdEncoding.EncodeToString(buf)
}

// DecodeDexUserID decodes Dex's base64-encoded user ID back to the raw user ID and connector ID.
func DecodeDexUserID(encodedID string) (userID, connectorID string, err error) {
	// Try RawStdEncoding first, then StdEncoding (with padding)
	buf, err := base64.RawStdEncoding.DecodeString(encodedID)
	if err != nil {
		buf, err = base64.StdEncoding.DecodeString(encodedID)
		if err != nil {
			return "", "", fmt.Errorf("failed to decode base64: %w", err)
		}
	}

	// Parse protobuf manually
	i := 0
	for i < len(buf) {
		if i >= len(buf) {
			break
		}
		tag := buf[i]
		i++

		fieldNum := tag >> 3
		wireType := tag & 0x07

		if wireType != 2 { // We only expect length-delimited strings
			return "", "", fmt.Errorf("unexpected wire type %d", wireType)
		}

		if i >= len(buf) {
			return "", "", fmt.Errorf("truncated message")
		}
		length := int(buf[i])
		i++

		if i+length > len(buf) {
			return "", "", fmt.Errorf("truncated string field")
		}
		value := string(buf[i : i+length])
		i += length

		switch fieldNum {
		case 1:
			userID = value
		case 2:
			connectorID = value
		}
	}

	return userID, connectorID, nil
}

// GetUser returns a user by email
func (p *Provider) GetUser(ctx context.Context, email string) (storage.Password, error) {
	return p.storage.GetPassword(ctx, email)
}

// GetUserByID returns a user by user ID.
// The userID can be either an encoded Dex ID (base64 protobuf) or a raw UUID.
// Note: This requires iterating through all users since dex storage doesn't index by userID.
func (p *Provider) GetUserByID(ctx context.Context, userID string) (storage.Password, error) {
	// Try to decode the user ID in case it's encoded
	rawUserID, _, err := DecodeDexUserID(userID)
	if err != nil {
		// If decoding fails, assume it's already a raw UUID
		rawUserID = userID
	}

	users, err := p.storage.ListPasswords(ctx)
	if err != nil {
		return storage.Password{}, fmt.Errorf("failed to list users: %w", err)
	}
	for _, user := range users {
		if user.UserID == rawUserID {
			return user, nil
		}
	}
	return storage.Password{}, storage.ErrNotFound
}

// DeleteUser removes a user by email
func (p *Provider) DeleteUser(ctx context.Context, email string) error {
	return p.storage.DeletePassword(ctx, email)
}

// ListUsers returns all users
func (p *Provider) ListUsers(ctx context.Context) ([]storage.Password, error) {
	return p.storage.ListPasswords(ctx)
}

// UpdateUserPassword updates the password for a user identified by userID.
// The userID can be either an encoded Dex ID (base64 protobuf) or a raw UUID.
// It verifies the current password before updating.
func (p *Provider) UpdateUserPassword(ctx context.Context, userID string, oldPassword, newPassword string) error {
	// Get the user by ID to find their email
	user, err := p.GetUserByID(ctx, userID)
	if err != nil {
		return fmt.Errorf("failed to get user: %w", err)
	}

	// Verify old password
	if err := bcrypt.CompareHashAndPassword(user.Hash, []byte(oldPassword)); err != nil {
		return fmt.Errorf("current password is incorrect")
	}

	// Hash the new password
	newHash, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
	if err != nil {
		return fmt.Errorf("failed to hash new password: %w", err)
	}

	// Update the password in storage
	err = p.storage.UpdatePassword(ctx, user.Email, func(old storage.Password) (storage.Password, error) {
		old.Hash = newHash
		return old, nil
	})
	if err != nil {
		return fmt.Errorf("failed to update password: %w", err)
	}

	return nil
}

// GetIssuer returns the OIDC issuer URL.
func (p *Provider) GetIssuer() string {
	if p.config == nil {
		return ""
	}
	issuer := strings.TrimSuffix(p.config.Issuer, "/")
	if !strings.HasSuffix(issuer, "/oauth2") {
		issuer += "/oauth2"
	}
	return issuer
}

// GetKeysLocation returns the JWKS endpoint URL for token validation.
func (p *Provider) GetKeysLocation() string {
	issuer := p.GetIssuer()
	if issuer == "" {
		return ""
	}
	return issuer + "/keys"
}

// GetTokenEndpoint returns the OAuth2 token endpoint URL.
func (p *Provider) GetTokenEndpoint() string {
	issuer := p.GetIssuer()
	if issuer == "" {
		return ""
	}
	return issuer + "/token"
}

// GetDeviceAuthEndpoint returns the OAuth2 device authorization endpoint URL.
func (p *Provider) GetDeviceAuthEndpoint() string {
	issuer := p.GetIssuer()
	if issuer == "" {
		return ""
	}
	return issuer + "/device/code"
}

// GetAuthorizationEndpoint returns the OAuth2 authorization endpoint URL.
func (p *Provider) GetAuthorizationEndpoint() string {
	issuer := p.GetIssuer()
	if issuer == "" {
		return ""
	}
	return issuer + "/auth"
}


     1→package dex
     2→
     3→import (
     4→	"encoding/base64"
     5→	"encoding/json"
     6→	"fmt"
     7→	"log/slog"
     8→	"os"
     9→	"time"
    10→
    11→	"golang.org/x/crypto/bcrypt"
    12→	"gopkg.in/yaml.v3"
    13→
    14→	"github.com/dexidp/dex/server"
    15→	"github.com/dexidp/dex/storage"
    16→	"github.com/dexidp/dex/storage/sql"
    17→
    18→	"github.com/netbirdio/netbird/idp/dex/web"
    19→)
    20→
    21→// parseDuration parses a duration string (e.g., "6h", "24h", "168h").
    22→func parseDuration(s string) (time.Duration, error) {
    23→	return time.ParseDuration(s)
    24→}
    25→
    26→// YAMLConfig represents the YAML configuration file format (mirrors dex's config format)
    27→type YAMLConfig struct {
    28→	Issuer   string   `yaml:"issuer" json:"issuer"`
    29→	Storage  Storage  `yaml:"storage" json:"storage"`
    30→	Web      Web      `yaml:"web" json:"web"`
    31→	GRPC     GRPC     `yaml:"grpc" json:"grpc"`
    32→	OAuth2   OAuth2   `yaml:"oauth2" json:"oauth2"`
    33→	Expiry   Expiry   `yaml:"expiry" json:"expiry"`
    34→	Logger   Logger   `yaml:"logger" json:"logger"`
    35→	Frontend Frontend `yaml:"frontend" json:"frontend"`
    36→
    37→	// StaticConnectors are user defined connectors specified in the config file
    38→	StaticConnectors []Connector `yaml:"connectors" json:"connectors"`
    39→
    40→	// StaticClients cause the server to use this list of clients rather than
    41→	// querying the storage. Write operations, like creating a client, will fail.
    42→	StaticClients []storage.Client `yaml:"staticClients" json:"staticClients"`
    43→
    44→	// If enabled, the server will maintain a list of passwords which can be used
    45→	// to identify a user.
    46→	EnablePasswordDB bool `yaml:"enablePasswordDB" json:"enablePasswordDB"`
    47→
    48→	// StaticPasswords cause the server use this list of passwords rather than
    49→	// querying the storage.
    50→	StaticPasswords []Password `yaml:"staticPasswords" json:"staticPasswords"`
    51→}
    52→
    53→// Web is the config format for the HTTP server.
    54→type Web struct {
    55→	HTTP           string   `yaml:"http" json:"http"`
    56→	HTTPS          string   `yaml:"https" json:"https"`
    57→	AllowedOrigins []string `yaml:"allowedOrigins" json:"allowedOrigins"`
    58→	AllowedHeaders []string `yaml:"allowedHeaders" json:"allowedHeaders"`
    59→}
    60→
    61→// GRPC is the config for the gRPC API.
    62→type GRPC struct {
    63→	Addr        string `yaml:"addr" json:"addr"`
    64→	TLSCert     string `yaml:"tlsCert" json:"tlsCert"`
    65→	TLSKey      string `yaml:"tlsKey" json:"tlsKey"`
    66→	TLSClientCA string `yaml:"tlsClientCA" json:"tlsClientCA"`
    67→}
    68→
    69→// OAuth2 describes enabled OAuth2 extensions.
    70→type OAuth2 struct {
    71→	SkipApprovalScreen    bool     `yaml:"skipApprovalScreen" json:"skipApprovalScreen"`
    72→	AlwaysShowLoginScreen bool     `yaml:"alwaysShowLoginScreen" json:"alwaysShowLoginScreen"`
    73→	PasswordConnector     string   `yaml:"passwordConnector" json:"passwordConnector"`
    74→	ResponseTypes         []string `yaml:"responseTypes" json:"responseTypes"`
    75→	GrantTypes            []string `yaml:"grantTypes" json:"grantTypes"`
    76→}
    77→
    78→// Expiry holds configuration for the validity period of components.
    79→type Expiry struct {
    80→	SigningKeys    string              `yaml:"signingKeys" json:"signingKeys"`
    81→	IDTokens       string              `yaml:"idTokens" json:"idTokens"`
    82→	AuthRequests   string              `yaml:"authRequests" json:"authRequests"`
    83→	DeviceRequests string              `yaml:"deviceRequests" json:"deviceRequests"`
    84→	RefreshTokens  RefreshTokensExpiry `yaml:"refreshTokens" json:"refreshTokens"`
    85→}
    86→
    87→// RefreshTokensExpiry holds configuration for refresh token expiry.
    88→type RefreshTokensExpiry struct {
    89→	ReuseInterval     string `yaml:"reuseInterval" json:"reuseInterval"`
    90→	ValidIfNotUsedFor string `yaml:"validIfNotUsedFor" json:"validIfNotUsedFor"`
    91→	AbsoluteLifetime  string `yaml:"absoluteLifetime" json:"absoluteLifetime"`
    92→	DisableRotation   bool   `yaml:"disableRotation" json:"disableRotation"`
    93→}
    94→
    95→// Logger holds configuration required to customize logging.
    96→type Logger struct {
    97→	Level  string `yaml:"level" json:"level"`
    98→	Format string `yaml:"format" json:"format"`
    99→}
   100→
   101→// Frontend holds the server's frontend templates and assets config.
   102→type Frontend struct {
   103→	Dir     string            `yaml:"dir" json:"dir"`
   104→	Theme   string            `yaml:"theme" json:"theme"`
   105→	Issuer  string            `yaml:"issuer" json:"issuer"`
   106→	LogoURL string            `yaml:"logoURL" json:"logoURL"`
   107→	Extra   map[string]string `yaml:"extra" json:"extra"`
   108→}
   109→
   110→// Storage holds app's storage configuration.
   111→type Storage struct {
   112→	Type   string                 `yaml:"type" json:"type"`
   113→	Config map[string]interface{} `yaml:"config" json:"config"`
   114→}
   115→
   116→// Password represents a static user configuration
   117→type Password storage.Password
   118→
   119→func (p *Password) UnmarshalYAML(node *yaml.Node) error {
   120→	var data struct {
   121→		Email       string `yaml:"email"`
   122→		Username    string `yaml:"username"`
   123→		UserID      string `yaml:"userID"`
   124→		Hash        string `yaml:"hash"`
   125→		HashFromEnv string `yaml:"hashFromEnv"`
   126→	}
   127→	if err := node.Decode(&data); err != nil {
   128→		return err
   129→	}
   130→	*p = Password(storage.Password{
   131→		Email:    data.Email,
   132→		Username: data.Username,
   133→		UserID:   data.UserID,
   134→	})
   135→	if len(data.Hash) == 0 && len(data.HashFromEnv) > 0 {
   136→		data.Hash = os.Getenv(data.HashFromEnv)
   137→	}
   138→	if len(data.Hash) == 0 {
   139→		return fmt.Errorf("no password hash provided for user %s", data.Email)
   140→	}
   141→
   142→	// If this value is a valid bcrypt, use it.
   143→	_, bcryptErr := bcrypt.Cost([]byte(data.Hash))
   144→	if bcryptErr == nil {
   145→		p.Hash = []byte(data.Hash)
   146→		return nil
   147→	}
   148→
   149→	// For backwards compatibility try to base64 decode this value.
   150→	hashBytes, err := base64.StdEncoding.DecodeString(data.Hash)
   151→	if err != nil {
   152→		return fmt.Errorf("malformed bcrypt hash: %v", bcryptErr)
   153→	}
   154→	if _, err := bcrypt.Cost(hashBytes); err != nil {
   155→		return fmt.Errorf("malformed bcrypt hash: %v", err)
   156→	}
   157→	p.Hash = hashBytes
   158→	return nil
   159→}
   160→
   161→// Connector is a connector configuration that can unmarshal YAML dynamically.
   162→type Connector struct {
   163→	Type   string                 `yaml:"type" json:"type"`
   164→	Name   string                 `yaml:"name" json:"name"`
   165→	ID     string                 `yaml:"id" json:"id"`
   166→	Config map[string]interface{} `yaml:"config" json:"config"`
   167→}
   168→
   169→// ToStorageConnector converts a Connector to storage.Connector type.
   170→func (c *Connector) ToStorageConnector() (storage.Connector, error) {
   171→	data, err := json.Marshal(c.Config)
   172→	if err != nil {
   173→		return storage.Connector{}, fmt.Errorf("failed to marshal connector config: %v", err)
   174→	}
   175→
   176→	return storage.Connector{
   177→		ID:     c.ID,
   178→		Type:   c.Type,
   179→		Name:   c.Name,
   180→		Config: data,
   181→	}, nil
   182→}
   183→
   184→// StorageConfig is a configuration that can create a storage.
   185→type StorageConfig interface {
   186→	Open(logger *slog.Logger) (storage.Storage, error)
   187→}
   188→
   189→// OpenStorage opens a storage based on the config
   190→func (s *Storage) OpenStorage(logger *slog.Logger) (storage.Storage, error) {
   191→	switch s.Type {
   192→	case "sqlite3":
   193→		file, _ := s.Config["file"].(string)
   194→		if file == "" {
   195→			return nil, fmt.Errorf("sqlite3 storage requires 'file' config")
   196→		}
   197→		return (&sql.SQLite3{File: file}).Open(logger)
   198→	default:
   199→		return nil, fmt.Errorf("unsupported storage type: %s", s.Type)
   200→	}
   201→}
   202→
   203→// Validate validates the configuration
   204→func (c *YAMLConfig) Validate() error {
   205→	if c.Issuer == "" {
   206→		return fmt.Errorf("no issuer specified in config file")
   207→	}
   208→	if c.Storage.Type == "" {
   209→		return fmt.Errorf("no storage type specified in config file")
   210→	}
   211→	if c.Web.HTTP == "" && c.Web.HTTPS == "" {
   212→		return fmt.Errorf("must supply a HTTP/HTTPS address to listen on")
   213→	}
   214→	if !c.EnablePasswordDB && len(c.StaticPasswords) != 0 {
   215→		return fmt.Errorf("cannot specify static passwords without enabling password db")
   216→	}
   217→	return nil
   218→}
   219→
   220→// ToServerConfig converts YAMLConfig to dex server.Config
   221→func (c *YAMLConfig) ToServerConfig(stor storage.Storage, logger *slog.Logger) server.Config {
   222→	cfg := server.Config{
   223→		Issuer:             c.Issuer,
   224→		Storage:            stor,
   225→		Logger:             logger,
   226→		SkipApprovalScreen: c.OAuth2.SkipApprovalScreen,
   227→		AllowedOrigins:     c.Web.AllowedOrigins,
   228→		AllowedHeaders:     c.Web.AllowedHeaders,
   229→		Web: server.WebConfig{
   230→			Issuer:  c.Frontend.Issuer,
   231→			LogoURL: c.Frontend.LogoURL,
   232→			Theme:   c.Frontend.Theme,
   233→			Dir:     c.Frontend.Dir,
   234→			Extra:   c.Frontend.Extra,
   235→		},
   236→	}
   237→
   238→	// Use embedded NetBird-styled templates if no custom dir specified
   239→	if c.Frontend.Dir == "" {
   240→		cfg.Web.WebFS = web.FS()
   241→	}
   242→
   243→	if len(c.OAuth2.ResponseTypes) > 0 {
   244→		cfg.SupportedResponseTypes = c.OAuth2.ResponseTypes
   245→	}
   246→
   247→	// Apply expiry settings
   248→	if c.Expiry.SigningKeys != "" {
   249→		if d, err := parseDuration(c.Expiry.SigningKeys); err == nil {
   250→			cfg.RotateKeysAfter = d
   251→		}
   252→	}
   253→	if c.Expiry.IDTokens != "" {
   254→		if d, err := parseDuration(c.Expiry.IDTokens); err == nil {
   255→			cfg.IDTokensValidFor = d
   256→		}
   257→	}
   258→	if c.Expiry.AuthRequests != "" {
   259→		if d, err := parseDuration(c.Expiry.AuthRequests); err == nil {
   260→			cfg.AuthRequestsValidFor = d
   261→		}
   262→	}
   263→	if c.Expiry.DeviceRequests != "" {
   264→		if d, err := parseDuration(c.Expiry.DeviceRequests); err == nil {
   265→			cfg.DeviceRequestsValidFor = d
   266→		}
   267→	}
   268→
   269→	return cfg
   270→}
   271→
   272→// GetRefreshTokenPolicy creates a RefreshTokenPolicy from the expiry config.
   273→// This should be called after ToServerConfig and the policy set on the config.
   274→func (c *YAMLConfig) GetRefreshTokenPolicy(logger *slog.Logger) (*server.RefreshTokenPolicy, error) {
   275→	return server.NewRefreshTokenPolicy(
   276→		logger,
   277→		c.Expiry.RefreshTokens.DisableRotation,
   278→		c.Expiry.RefreshTokens.ValidIfNotUsedFor,
   279→		c.Expiry.RefreshTokens.AbsoluteLifetime,
   280→		c.Expiry.RefreshTokens.ReuseInterval,
   281→	)
   282→}
   283→
   284→// LoadConfig loads configuration from a YAML file
   285→func LoadConfig(path string) (*YAMLConfig, error) {
   286→	data, err := os.ReadFile(path)
   287→	if err != nil {
   288→		return nil, fmt.Errorf("failed to read config file: %w", err)
   289→	}
   290→
   291→	var cfg YAMLConfig
   292→	if err := yaml.Unmarshal(data, &cfg); err != nil {
   293→		return nil, fmt.Errorf("failed to parse config file: %w", err)
   294→	}
   295→
   296→	if err := cfg.Validate(); err != nil {
   297→		return nil, err
   298→	}
   299→
   300→	return &cfg, nil
   301→}
   302→

package dex

import (
	"encoding/base64"
	"encoding/json"
	"fmt"
	"log/slog"
	"os"
	"time"

	"golang.org/x/crypto/bcrypt"
	"gopkg.in/yaml.v3"

	"github.com/dexidp/dex/server"
	"github.com/dexidp/dex/storage"
	"github.com/dexidp/dex/storage/sql"

	"github.com/netbirdio/netbird/idp/dex/web"
)

// parseDuration parses a duration string (e.g., "6h", "24h", "168h").
func parseDuration(s string) (time.Duration, error) {
	return time.ParseDuration(s)
}

// YAMLConfig represents the YAML configuration file format (mirrors dex's config format)
type YAMLConfig struct {
	Issuer   string   `yaml:"issuer" json:"issuer"`
	Storage  Storage  `yaml:"storage" json:"storage"`
	Web      Web      `yaml:"web" json:"web"`
	GRPC     GRPC     `yaml:"grpc" json:"grpc"`
	OAuth2   OAuth2   `yaml:"oauth2" json:"oauth2"`
	Expiry   Expiry   `yaml:"expiry" json:"expiry"`
	Logger   Logger   `yaml:"logger" json:"logger"`
	Frontend Frontend `yaml:"frontend" json:"frontend"`

	// StaticConnectors are user defined connectors specified in the config file
	StaticConnectors []Connector `yaml:"connectors" json:"connectors"`

	// StaticClients cause the server to use this list of clients rather than
	// querying the storage. Write operations, like creating a client, will fail.
	StaticClients []storage.Client `yaml:"staticClients" json:"staticClients"`

	// If enabled, the server will maintain a list of passwords which can be used
	// to identify a user.
	EnablePasswordDB bool `yaml:"enablePasswordDB" json:"enablePasswordDB"`

	// StaticPasswords cause the server use this list of passwords rather than
	// querying the storage.
	StaticPasswords []Password `yaml:"staticPasswords" json:"staticPasswords"`
}

// Web is the config format for the HTTP server.
type Web struct {
	HTTP           string   `yaml:"http" json:"http"`
	HTTPS          string   `yaml:"https" json:"https"`
	AllowedOrigins []string `yaml:"allowedOrigins" json:"allowedOrigins"`
	AllowedHeaders []string `yaml:"allowedHeaders" json:"allowedHeaders"`
}

// GRPC is the config for the gRPC API.
type GRPC struct {
	Addr        string `yaml:"addr" json:"addr"`
	TLSCert     string `yaml:"tlsCert" json:"tlsCert"`
	TLSKey      string `yaml:"tlsKey" json:"tlsKey"`
	TLSClientCA string `yaml:"tlsClientCA" json:"tlsClientCA"`
}

// OAuth2 describes enabled OAuth2 extensions.
type OAuth2 struct {
	SkipApprovalScreen    bool     `yaml:"skipApprovalScreen" json:"skipApprovalScreen"`
	AlwaysShowLoginScreen bool     `yaml:"alwaysShowLoginScreen" json:"alwaysShowLoginScreen"`
	PasswordConnector     string   `yaml:"passwordConnector" json:"passwordConnector"`
	ResponseTypes         []string `yaml:"responseTypes" json:"responseTypes"`
	GrantTypes            []string `yaml:"grantTypes" json:"grantTypes"`
}

// Expiry holds configuration for the validity period of components.
type Expiry struct {
	SigningKeys    string              `yaml:"signingKeys" json:"signingKeys"`
	IDTokens       string              `yaml:"idTokens" json:"idTokens"`
	AuthRequests   string              `yaml:"authRequests" json:"authRequests"`
	DeviceRequests string              `yaml:"deviceRequests" json:"deviceRequests"`
	RefreshTokens  RefreshTokensExpiry `yaml:"refreshTokens" json:"refreshTokens"`
}

// RefreshTokensExpiry holds configuration for refresh token expiry.
type RefreshTokensExpiry struct {
	ReuseInterval     string `yaml:"reuseInterval" json:"reuseInterval"`
	ValidIfNotUsedFor string `yaml:"validIfNotUsedFor" json:"validIfNotUsedFor"`
	AbsoluteLifetime  string `yaml:"absoluteLifetime" json:"absoluteLifetime"`
	DisableRotation   bool   `yaml:"disableRotation" json:"disableRotation"`
}

// Logger holds configuration required to customize logging.
type Logger struct {
	Level  string `yaml:"level" json:"level"`
	Format string `yaml:"format" json:"format"`
}

// Frontend holds the server's frontend templates and assets config.
type Frontend struct {
	Dir     string            `yaml:"dir" json:"dir"`
	Theme   string            `yaml:"theme" json:"theme"`
	Issuer  string            `yaml:"issuer" json:"issuer"`
	LogoURL string            `yaml:"logoURL" json:"logoURL"`
	Extra   map[string]string `yaml:"extra" json:"extra"`
}

// Storage holds app's storage configuration.
type Storage struct {
	Type   string                 `yaml:"type" json:"type"`
	Config map[string]interface{} `yaml:"config" json:"config"`
}

// Password represents a static user configuration
type Password storage.Password

func (p *Password) UnmarshalYAML(node *yaml.Node) error {
	var data struct {
		Email       string `yaml:"email"`
		Username    string `yaml:"username"`
		UserID      string `yaml:"userID"`
		Hash        string `yaml:"hash"`
		HashFromEnv string `yaml:"hashFromEnv"`
	}
	if err := node.Decode(&data); err != nil {
		return err
	}
	*p = Password(storage.Password{
		Email:    data.Email,
		Username: data.Username,
		UserID:   data.UserID,
	})
	if len(data.Hash) == 0 && len(data.HashFromEnv) > 0 {
		data.Hash = os.Getenv(data.HashFromEnv)
	}
	if len(data.Hash) == 0 {
		return fmt.Errorf("no password hash provided for user %s", data.Email)
	}

	// If this value is a valid bcrypt, use it.
	_, bcryptErr := bcrypt.Cost([]byte(data.Hash))
	if bcryptErr == nil {
		p.Hash = []byte(data.Hash)
		return nil
	}

	// For backwards compatibility try to base64 decode this value.
	hashBytes, err := base64.StdEncoding.DecodeString(data.Hash)
	if err != nil {
		return fmt.Errorf("malformed bcrypt hash: %v", bcryptErr)
	}
	if _, err := bcrypt.Cost(hashBytes); err != nil {
		return fmt.Errorf("malformed bcrypt hash: %v", err)
	}
	p.Hash = hashBytes
	return nil
}

// Connector is a connector configuration that can unmarshal YAML dynamically.
type Connector struct {
	Type   string                 `yaml:"type" json:"type"`
	Name   string                 `yaml:"name" json:"name"`
	ID     string                 `yaml:"id" json:"id"`
	Config map[string]interface{} `yaml:"config" json:"config"`
}

// ToStorageConnector converts a Connector to storage.Connector type.
func (c *Connector) ToStorageConnector() (storage.Connector, error) {
	data, err := json.Marshal(c.Config)
	if err != nil {
		return storage.Connector{}, fmt.Errorf("failed to marshal connector config: %v", err)
	}

	return storage.Connector{
		ID:     c.ID,
		Type:   c.Type,
		Name:   c.Name,
		Config: data,
	}, nil
}

// StorageConfig is a configuration that can create a storage.
type StorageConfig interface {
	Open(logger *slog.Logger) (storage.Storage, error)
}

// OpenStorage opens a storage based on the config
func (s *Storage) OpenStorage(logger *slog.Logger) (storage.Storage, error) {
	switch s.Type {
	case "sqlite3":
		file, _ := s.Config["file"].(string)
		if file == "" {
			return nil, fmt.Errorf("sqlite3 storage requires 'file' config")
		}
		return (&sql.SQLite3{File: file}).Open(logger)
	default:
		return nil, fmt.Errorf("unsupported storage type: %s", s.Type)
	}
}

// Validate validates the configuration
func (c *YAMLConfig) Validate() error {
	if c.Issuer == "" {
		return fmt.Errorf("no issuer specified in config file")
	}
	if c.Storage.Type == "" {
		return fmt.Errorf("no storage type specified in config file")
	}
	if c.Web.HTTP == "" && c.Web.HTTPS == "" {
		return fmt.Errorf("must supply a HTTP/HTTPS address to listen on")
	}
	if !c.EnablePasswordDB && len(c.StaticPasswords) != 0 {
		return fmt.Errorf("cannot specify static passwords without enabling password db")
	}
	return nil
}

// ToServerConfig converts YAMLConfig to dex server.Config
func (c *YAMLConfig) ToServerConfig(stor storage.Storage, logger *slog.Logger) server.Config {
	cfg := server.Config{
		Issuer:             c.Issuer,
		Storage:            stor,
		Logger:             logger,
		SkipApprovalScreen: c.OAuth2.SkipApprovalScreen,
		AllowedOrigins:     c.Web.AllowedOrigins,
		AllowedHeaders:     c.Web.AllowedHeaders,
		Web: server.WebConfig{
			Issuer:  c.Frontend.Issuer,
			LogoURL: c.Frontend.LogoURL,
			Theme:   c.Frontend.Theme,
			Dir:     c.Frontend.Dir,
			Extra:   c.Frontend.Extra,
		},
	}

	// Use embedded NetBird-styled templates if no custom dir specified
	if c.Frontend.Dir == "" {
		cfg.Web.WebFS = web.FS()
	}

	if len(c.OAuth2.ResponseTypes) > 0 {
		cfg.SupportedResponseTypes = c.OAuth2.ResponseTypes
	}

	// Apply expiry settings
	if c.Expiry.SigningKeys != "" {
		if d, err := parseDuration(c.Expiry.SigningKeys); err == nil {
			cfg.RotateKeysAfter = d
		}
	}
	if c.Expiry.IDTokens != "" {
		if d, err := parseDuration(c.Expiry.IDTokens); err == nil {
			cfg.IDTokensValidFor = d
		}
	}
	if c.Expiry.AuthRequests != "" {
		if d, err := parseDuration(c.Expiry.AuthRequests); err == nil {
			cfg.AuthRequestsValidFor = d
		}
	}
	if c.Expiry.DeviceRequests != "" {
		if d, err := parseDuration(c.Expiry.DeviceRequests); err == nil {
			cfg.DeviceRequestsValidFor = d
		}
	}

	return cfg
}

// GetRefreshTokenPolicy creates a RefreshTokenPolicy from the expiry config.
// This should be called after ToServerConfig and the policy set on the config.
func (c *YAMLConfig) GetRefreshTokenPolicy(logger *slog.Logger) (*server.RefreshTokenPolicy, error) {
	return server.NewRefreshTokenPolicy(
		logger,
		c.Expiry.RefreshTokens.DisableRotation,
		c.Expiry.RefreshTokens.ValidIfNotUsedFor,
		c.Expiry.RefreshTokens.AbsoluteLifetime,
		c.Expiry.RefreshTokens.ReuseInterval,
	)
}

// LoadConfig loads configuration from a YAML file
func LoadConfig(path string) (*YAMLConfig, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return nil, fmt.Errorf("failed to read config file: %w", err)
	}

	var cfg YAMLConfig
	if err := yaml.Unmarshal(data, &cfg); err != nil {
		return nil, fmt.Errorf("failed to parse config file: %w", err)
	}

	if err := cfg.Validate(); err != nil {
		return nil, err
	}

	return &cfg, nil
}


Now I understand the issue. The problem is in `server.NewServer(ctx, dexConfig)` — when Dex initializes, it reads connectors from storage and tries to open/validate them. For OIDC connectors, this means fetching `.well-known/openid-configuration` from the issuer URL. If that IdP is unreachable, `NewServer` fails and the whole management server can't start.

The key call chain:
1. `NewProviderFromYAML` → `initializeStorage` → `ensureStaticConnectors` (writes connectors to storage)
2. `NewProviderFromYAML` → `server.NewServer(ctx, dexConfig)` (Dex reads connectors from storage and initializes them)

3. Dex's `NewServer` opens all connectors, which is internal to the library.

I'm considering two approaches: either pre-check if the IdP is reachable before storing the connector in `ensureStaticConnectors`, skipping unreachable ones with a warning, or defer adding OIDC connectors to storage entirely during initialization.

But there's a complication — connectors are persisted in SQLite, so even if we skip adding them on startup, they'll still be there from previous runs and cause the same issue on restart. The real solution needs to handle connectors that are already in storage but currently unreachable, either by removing or disabling them before calling `server.NewServer`. I think the clearest path forward is to validate connectors in two places: filter out unreachable ones during the static connector setup, and remove any stored connectors that are unreachable before the server starts. Then I can walk through these options with the user to see which approach fits their needs best.


Now I see the problem clearly. The critical path is:

1. `ensureStaticConnectors` writes connectors to SQLite storage
2. `server.NewServer()` reads all connectors from storage and **opens them** (OIDC connectors fetch `.well-known/openid-configuration`)
3. If any IdP is unreachable → `NewServer` fails → Dex can't start → management can't start

Even skipping a connector during `ensureStaticConnectors` doesn't help if it was stored from a previous run.

The pragmatic fix would be: **before calling `server.NewServer()`, probe each OIDC connector's issuer and temporarily remove unreachable ones from storage**, then re-add them in a background goroutine once they become reachable.

Concretely, in `provider.go` you'd add something like:

```go
// Before server.NewServer():
unreachable := removeUnreachableConnectors(ctx, stor, logger)

dexSrv, err := server.NewServer(ctx, dexConfig)
// ...

// After start, re-add in background:
go restoreConnectorsWhenReady(ctx, stor, unreachable, logger)
```

Where `removeUnreachableConnectors` does an HTTP GET to `<issuer>/.well-known/openid-configuration` with a short timeout for each OIDC connector.

The tradeoff: those IdPs won't be available for login until they come back online, but Dex and management will start fine.

Want me to implement this?


where you gonan store them?


[Request interrupted by user]


where you gonan store them to restore?


Good point. Options:

1. **In memory** — just keep the `[]storage.Connector` slice in the goroutine / on the `Provider` struct. Simplest, and it's fine because the source of truth is already in the YAML config (`StaticConnectors`). If the process restarts, `ensureStaticConnectors` will try to add them again anyway.

2. **Don't remove them at all** — instead, patch `ensureStaticConnectors` to skip unreachable OIDC connectors, and only write reachable ones to storage. Unreachable ones never hit storage, so `NewServer` never sees them. Background goroutine retries adding them from the original `cfg.StaticConnectors` list.

Option 2 is cleaner — you never remove anything, you just defer the storage write. The YAML config is already the persistent source of truth.


it is not in the yaml, it is in the database


Right — connectors are created via the API and stored in SQLite, not in a YAML config. So there's no external source of truth to restore from.

Options:

1. **Separate table / flag** — add a `disabled` column or a separate "quarantine" table in the DB. Before `NewServer`, mark unreachable connectors as disabled (Dex won't load them), background goroutine re-enables them when reachable.

2. **In-memory stash on the `Provider`** — before `NewServer`, pull unreachable connectors out of Dex storage into a `map[string]storage.Connector` on the `Provider` struct. Delete from DB. Background goroutine re-inserts them. If the process crashes before restore, the connectors are lost.

3. **Don't touch storage at all** — instead of removing connectors, catch and ignore the `NewServer` error for connector init failures. But that requires Dex library changes or a fork.

Option 1 is the safest — the connector data never leaves the database, just gets a flag. But it requires changes to the Dex storage layer (or a parallel table we manage ourselves).

Option 2 is simplest to implement but risks data loss on crash. Though in practice, if an IdP is permanently down, the connector is useless anyway — and the admin can re-add it.

Which direction do you prefer?


I see ContinueOnConnectorFailure in server/server.go in dex. How can I configure it