implemented api for mfa configuration

This commit is contained in:
jnfrati
2026-04-16 14:53:40 +02:00
parent 77b65d9475
commit 59abdc2363
10 changed files with 135 additions and 21 deletions
+26
View File
@@ -382,6 +382,9 @@ func (am *DefaultAccountManager) UpdateAccountSettings(ctx context.Context, acco
if err = am.handleInactivityExpirationSettings(ctx, oldSettings, newSettings, userID, accountID); err != nil {
return nil, err
}
if err = am.handleLocalMfaSettings(ctx, oldSettings, newSettings, userID, accountID); err != nil {
return nil, err
}
if oldSettings.DNSDomain != newSettings.DNSDomain {
eventMeta := map[string]any{
"old_dns_domain": oldSettings.DNSDomain,
@@ -542,6 +545,29 @@ func (am *DefaultAccountManager) handleInactivityExpirationSettings(ctx context.
return nil
}
func (am *DefaultAccountManager) handleLocalMfaSettings(ctx context.Context, oldSettings, newSettings *types.Settings, userID, accountID string) error {
if oldSettings.LocalMfaEnabled == newSettings.LocalMfaEnabled {
return nil
}
embeddedIdp, ok := am.idpManager.(*idp.EmbeddedIdPManager)
if !ok {
return nil
}
if err := embeddedIdp.SetMFAEnabled(ctx, newSettings.LocalMfaEnabled); err != nil {
return fmt.Errorf("failed to toggle MFA: %w", err)
}
if newSettings.LocalMfaEnabled {
am.StoreEvent(ctx, userID, accountID, accountID, activity.AccountLocalMfaEnabled, nil)
} else {
am.StoreEvent(ctx, userID, accountID, accountID, activity.AccountLocalMfaDisabled, nil)
}
return nil
}
func (am *DefaultAccountManager) peerLoginExpirationJob(ctx context.Context, accountID string) func() (time.Duration, bool) {
return func() (time.Duration, bool) {
//nolint
+8
View File
@@ -232,6 +232,11 @@ const (
// DomainValidated indicates that a custom domain was validated
DomainValidated Activity = 120
// AccountLocalMfaEnabled indicates that a user enabled TOTP MFA for local users
AccountLocalMfaEnabled Activity = 121
// AccountLocalMfaDisabled indicates that a user disabled TOTP MFA for local users
AccountLocalMfaDisabled Activity = 122
AccountDeleted Activity = 99999
)
@@ -379,6 +384,9 @@ var activityMap = map[Activity]Code{
AccountPeerExposeEnabled: {"Account peer expose enabled", "account.setting.peer.expose.enable"},
AccountPeerExposeDisabled: {"Account peer expose disabled", "account.setting.peer.expose.disable"},
AccountLocalMfaEnabled: {"Account local MFA enabled", "account.setting.local.mfa.enable"},
AccountLocalMfaDisabled: {"Account local MFA disabled", "account.setting.local.mfa.disable"},
DomainAdded: {"Domain added", "domain.add"},
DomainDeleted: {"Domain deleted", "domain.delete"},
DomainValidated: {"Domain validated", "domain.validate"},
@@ -228,6 +228,9 @@ func (h *handler) updateAccountRequestSettings(req api.PutApiAccountsAccountIdJS
if req.Settings.AutoUpdateAlways != nil {
returnSettings.AutoUpdateAlways = *req.Settings.AutoUpdateAlways
}
if req.Settings.LocalMfaEnabled != nil {
returnSettings.LocalMfaEnabled = *req.Settings.LocalMfaEnabled
}
return returnSettings, nil
}
@@ -354,6 +357,7 @@ func toAccountResponse(accountID string, settings *types.Settings, meta *types.A
AutoUpdateAlways: &settings.AutoUpdateAlways,
EmbeddedIdpEnabled: &settings.EmbeddedIdpEnabled,
LocalAuthDisabled: &settings.LocalAuthDisabled,
LocalMfaEnabled: &settings.LocalMfaEnabled,
}
if settings.NetworkRange.IsValid() {
+44 -12
View File
@@ -51,8 +51,12 @@ type EmbeddedIdPConfig struct {
// Existing local users are preserved and will be able to login again if re-enabled.
// Cannot be enabled if no external identity provider connectors are configured.
LocalAuthDisabled bool
// EnableMFA will enforce TOTP multi factor authentication for local users
EnableMFA bool
// MfaSessionMaxLifetime is the maximum MFA session duration from creation (e.g. "24h").
// Defaults to "24h" if empty.
MfaSessionMaxLifetime string
// MfaSessionIdleTimeout is the idle timeout after which the MFA session expires (e.g. "1h").
// Defaults to "1h" if empty.
MfaSessionIdleTimeout string
// Dashboard Post logout redirect URIs, these are required to tell
// Dex what to allow when an RP-Initiated logout is started by the frontend
// at least one of these must match the dashboard base URL or the dashboard
@@ -179,10 +183,10 @@ func (c *EmbeddedIdPConfig) ToYAMLConfig() (*dex.YAMLConfig, error) {
StaticConnectors: c.StaticConnectors,
}
if c.EnableMFA {
if err := configureMFA(cfg); err != nil {
return nil, err
}
// Always initialize MFA providers and sessions so TOTP can be toggled at runtime.
// MFAChain on clients is NOT set here — it's synced from the DB setting on startup.
if err := configureMFA(cfg, c.MfaSessionMaxLifetime, c.MfaSessionIdleTimeout); err != nil {
return nil, err
}
// Add owner user if provided
@@ -222,7 +226,7 @@ func sanitizePostLogoutRedirectURIs(uris []string) []string {
return result
}
func configureMFA(cfg *dex.YAMLConfig) error {
func configureMFA(cfg *dex.YAMLConfig, sessionMaxLifetime, sessionIdleTimeout string) error {
totpConfig := dex.TOTPConfig{
Issuer: "NetBird",
}
@@ -240,21 +244,27 @@ func configureMFA(cfg *dex.YAMLConfig) error {
ConnectorTypes: []string{"local"},
}}
if sessionMaxLifetime == "" {
sessionMaxLifetime = "24h"
}
if sessionIdleTimeout == "" {
sessionIdleTimeout = "1h"
}
rememberMeEnabled := false
cfg.Sessions = &dex.Sessions{
CookieName: "netbird-session",
AbsoluteLifetime: "24h",
ValidIfNotUsedFor: "1h",
AbsoluteLifetime: sessionMaxLifetime,
ValidIfNotUsedFor: sessionIdleTimeout,
RememberMeCheckedByDefault: &rememberMeEnabled,
SSOSharedWithDefault: "",
}
// Absolutely required, otherwise the dex server will omit the MFA configuration entirely
os.Setenv("DEX_SESSIONS_ENABLED", "true")
for i := range cfg.StaticClients {
cfg.StaticClients[i].MFAChain = []string{"default-totp"}
}
// Note: MFAChain on clients is NOT set here.
// It is toggled at runtime via SetMFAEnabled() based on the account settings DB value.
return nil
}
@@ -291,6 +301,7 @@ type EmbeddedIdPManager struct {
provider *dex.Provider
appMetrics telemetry.AppMetrics
config EmbeddedIdPConfig
mfaEnabled bool
}
// NewEmbeddedIdPManager creates a new instance of EmbeddedIdPManager from a configuration.
@@ -717,6 +728,27 @@ func (m *EmbeddedIdPManager) IsLocalAuthDisabled() bool {
return m.config.LocalAuthDisabled
}
// SetMFAEnabled enables or disables TOTP MFA for local users by updating the MFAChain on OAuth2 clients.
func (m *EmbeddedIdPManager) SetMFAEnabled(ctx context.Context, enabled bool) error {
var mfaChain []string
if enabled {
mfaChain = []string{"default-totp"}
}
if err := m.provider.SetClientsMFAChain(ctx, []string{
staticClientCLI,
staticClientDashboard,
}, mfaChain); err != nil {
return fmt.Errorf("failed to set MFA enabled=%v: %w", enabled, err)
}
m.mfaEnabled = enabled
return nil
}
// IsMFAEnabled returns whether TOTP MFA is currently enabled for local users.
func (m *EmbeddedIdPManager) IsMFAEnabled() bool {
return m.mfaEnabled
}
// HasNonLocalConnectors checks if there are any identity provider connectors other than local.
func (m *EmbeddedIdPManager) HasNonLocalConnectors(ctx context.Context) (bool, error) {
return m.provider.HasNonLocalConnectors(ctx)
+5
View File
@@ -72,6 +72,10 @@ type Settings struct {
// LocalAuthDisabled indicates if local (email/password) authentication is disabled.
// This is a runtime-only field, not stored in the database.
LocalAuthDisabled bool `gorm:"-"`
// LocalMfaEnabled indicates if TOTP MFA is enabled for local users.
// Only applicable when the embedded IDP is enabled.
LocalMfaEnabled bool
}
// Copy copies the Settings struct
@@ -98,6 +102,7 @@ func (s *Settings) Copy() *Settings {
AutoUpdateAlways: s.AutoUpdateAlways,
EmbeddedIdpEnabled: s.EmbeddedIdpEnabled,
LocalAuthDisabled: s.LocalAuthDisabled,
LocalMfaEnabled: s.LocalMfaEnabled,
}
if s.Extra != nil {
settings.Extra = s.Extra.Copy()