diff --git a/api/types/authentication.go b/api/types/authentication.go index 594832c132e2b..3e219a9086216 100644 --- a/api/types/authentication.go +++ b/api/types/authentication.go @@ -87,11 +87,12 @@ type AuthPreference interface { IsSecondFactorEnabled() bool // IsSecondFactorEnforced checks if second factor is enforced. IsSecondFactorEnforced() bool - // IsSecondFactorTOTPAllowed checks if users are allowed to register TOTP devices. + // IsSecondFactorTOTPAllowed checks if users can use TOTP as an MFA method. IsSecondFactorTOTPAllowed() bool - // IsSecondFactorWebauthnAllowed checks if users are allowed to register - // Webauthn devices. + // IsSecondFactorWebauthnAllowed checks if users can use WebAuthn as an MFA method. IsSecondFactorWebauthnAllowed() bool + // IsSecondFactorSSOAllowed checks if users can use SSO as an MFA method. + IsSecondFactorSSOAllowed() bool // IsAdminActionMFAEnforced checks if admin action MFA is enforced. IsAdminActionMFAEnforced() bool @@ -383,17 +384,21 @@ func (c *AuthPreferenceV2) IsSecondFactorEnforced() bool { return len(c.GetSecondFactors()) > 0 && c.Spec.SecondFactor != constants.SecondFactorOptional } -// IsSecondFactorTOTPAllowed checks if users are allowed to register TOTP devices. +// IsSecondFactorTOTPAllowed checks if users can use TOTP as an MFA method. func (c *AuthPreferenceV2) IsSecondFactorTOTPAllowed() bool { return slices.Contains(c.GetSecondFactors(), SecondFactorType_SECOND_FACTOR_TYPE_OTP) } -// IsSecondFactorWebauthnAllowed checks if users are allowed to register -// Webauthn devices. +// IsSecondFactorWebauthnAllowed checks if users can use WebAuthn as an MFA method. func (c *AuthPreferenceV2) IsSecondFactorWebauthnAllowed() bool { return slices.Contains(c.GetSecondFactors(), SecondFactorType_SECOND_FACTOR_TYPE_WEBAUTHN) } +// IsSecondFactorSSOAllowed checks if users can use SSO as an MFA method. +func (c *AuthPreferenceV2) IsSecondFactorSSOAllowed() bool { + return slices.Contains(c.GetSecondFactors(), SecondFactorType_SECOND_FACTOR_TYPE_SSO) +} + // IsAdminActionMFAEnforced checks if admin action MFA is enforced. func (c *AuthPreferenceV2) IsAdminActionMFAEnforced() bool { // OTP is not supported for Admin MFA. @@ -679,7 +684,6 @@ func (c *AuthPreferenceV2) CheckAndSetDefaults() error { if c.Spec.Type == "" { c.Spec.Type = constants.Local } - if c.Spec.AllowLocalAuth == nil { c.Spec.AllowLocalAuth = NewBoolOption(true) } @@ -722,7 +726,7 @@ func (c *AuthPreferenceV2) CheckAndSetDefaults() error { c.Spec.SecondFactor = constants.SecondFactorWebauthn case "": // default to OTP if SecondFactors is also not set. - if len(c.Spec.SecondFactors) == 0 { + if c.Spec.SecondFactors == nil { c.Spec.SecondFactor = constants.SecondFactorOTP } default: @@ -771,6 +775,16 @@ func (c *AuthPreferenceV2) CheckAndSetDefaults() error { return trace.BadParameter("missing required Webauthn configuration for headless=true") } + // Prevent accidental local lockout by disabling local second factor methods, (likely leaving only sso enabled). + hasTOTP := c.IsSecondFactorTOTPAllowed() + if c.GetAllowLocalAuth() && !hasTOTP && !hasWebauthn { + switch c.Spec.SecondFactor { + case constants.SecondFactorOptional, constants.SecondFactorOff: + default: + return trace.BadParameter("missing a local second factor method for local users (otp, webauthn), either add a local second factor method or disable local auth") + } + } + // Validate connector name for type=local. if c.Spec.Type == constants.Local { switch connectorName := c.Spec.ConnectorName; connectorName { diff --git a/api/types/second_factor.go b/api/types/second_factor.go index d876a35493c83..e7bdfdec3a575 100644 --- a/api/types/second_factor.go +++ b/api/types/second_factor.go @@ -29,9 +29,10 @@ import ( func legacySecondFactorFromSecondFactors(secondFactors []SecondFactorType) constants.SecondFactorType { hasOTP := slices.Contains(secondFactors, SecondFactorType_SECOND_FACTOR_TYPE_OTP) hasWebAuthn := slices.Contains(secondFactors, SecondFactorType_SECOND_FACTOR_TYPE_WEBAUTHN) + hasSSO := slices.Contains(secondFactors, SecondFactorType_SECOND_FACTOR_TYPE_SSO) switch { - case hasOTP && hasWebAuthn: + case (hasOTP && hasWebAuthn) || hasSSO: return constants.SecondFactorOn case hasWebAuthn: return constants.SecondFactorWebauthn diff --git a/lib/modules/modules.go b/lib/modules/modules.go index ddd4b67a8aa57..69cb6704f8c9c 100644 --- a/lib/modules/modules.go +++ b/lib/modules/modules.go @@ -33,7 +33,6 @@ import ( "github.com/gravitational/teleport" "github.com/gravitational/teleport/api/client/proto" - "github.com/gravitational/teleport/api/constants" "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/api/types/accesslist" "github.com/gravitational/teleport/api/utils/keys" @@ -325,8 +324,7 @@ func ValidateResource(res types.Resource) error { if GetModules().Features().Cloud || !IsInsecureTestMode() { switch r := res.(type) { case types.AuthPreference: - switch r.GetSecondFactor() { - case constants.SecondFactorOff, constants.SecondFactorOptional: + if !r.IsSecondFactorEnforced() { return trace.Wrap(ErrCannotDisableSecondFactor) } }