mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-18 16:26:38 +00:00
[management, infrastructure, idp] Simplified IdP Management - Embedded IdP (#5008)
Embed Dex as a built-in IdP to simplify self-hosting setup. Adds an embedded OIDC Identity Provider (Dex) with local user management and optional external IdP connectors (Google/GitHub/OIDC/SAML), plus device-auth flow for CLI login. Introduces instance onboarding/setup endpoints (including owner creation), field-level encryption for sensitive user data, a streamlined self-hosting provisioning script, and expanded APIs + test coverage for IdP management. more at https://github.com/netbirdio/netbird/pull/5008#issuecomment-3718987393
This commit is contained in:
301
idp/dex/config.go
Normal file
301
idp/dex/config.go
Normal file
@@ -0,0 +1,301 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user