diff --git a/api/types/authentication.go b/api/types/authentication.go index 776da9b1142b5..58f06a67fe50a 100644 --- a/api/types/authentication.go +++ b/api/types/authentication.go @@ -23,6 +23,7 @@ import ( "fmt" "log/slog" "net/url" + "slices" "strings" "time" @@ -82,8 +83,9 @@ type AuthPreference interface { // method when various options are available. // It is empty if there is nothing to suggest. GetPreferredLocalMFA() constants.SecondFactorType - // IsSecondFactorEnforced checks if second factor is enforced - // (not disabled or set to optional). + // IsSecondFactorEnabled checks if second factor is enabled. + IsSecondFactorEnabled() bool + // IsSecondFactorEnforced checks if second factor is enforced. IsSecondFactorEnforced() bool // IsSecondFactorTOTPAllowed checks if users are allowed to register TOTP devices. IsSecondFactorTOTPAllowed() bool @@ -318,7 +320,12 @@ func (c *AuthPreferenceV2) SetType(s string) { // GetSecondFactor returns the type of second factor. func (c *AuthPreferenceV2) GetSecondFactor() constants.SecondFactorType { - return c.Spec.SecondFactor + if c.Spec.SecondFactor != "" { + return c.Spec.SecondFactor + } + + // If SecondFactor isn't set, try to convert the new SecondFactors field. + return LegacySecondFactorFromSecondFactors(c.Spec.SecondFactors) } // SetSecondFactor sets the type of second factor. @@ -326,24 +333,50 @@ func (c *AuthPreferenceV2) SetSecondFactor(s constants.SecondFactorType) { c.Spec.SecondFactor = s } +// LegacySecondFactorFromSecondFactors returns a suitable legacy second factor for the given list of second factors. +func LegacySecondFactorFromSecondFactors(secondFactors []SecondFactorType) constants.SecondFactorType { + hasOTP := slices.Contains(secondFactors, SecondFactorType_SECOND_FACTOR_TYPE_OTP) + hasWebAuthn := slices.Contains(secondFactors, SecondFactorType_SECOND_FACTOR_TYPE_WEBAUTHN) + + switch { + case hasOTP && hasWebAuthn: + return constants.SecondFactorOn + case hasWebAuthn: + return constants.SecondFactorWebauthn + case hasOTP: + return constants.SecondFactorOTP + default: + return constants.SecondFactorOff + } +} + // GetSecondFactors gets a list of supported second factors. func (c *AuthPreferenceV2) GetSecondFactors() []SecondFactorType { if c.Spec.SecondFactors != nil { return c.Spec.SecondFactors } - switch sf := c.GetSecondFactor(); sf { + // If SecondFactors isn't set, try to convert the old SecondFactor field. + return SecondFactorsFromLegacySecondFactor(c.Spec.SecondFactor, c.Spec.Webauthn != nil) +} + +// SecondFactorsFromLegacySecondFactor returns the list of SecondFactorTypes supported by the given second factor type. +func SecondFactorsFromLegacySecondFactor(sf constants.SecondFactorType, webauthnConfigured bool) []SecondFactorType { + switch sf { case constants.SecondFactorOff: return nil case constants.SecondFactorOptional, constants.SecondFactorOn: - if _, err := c.GetWebauthn(); err == nil { - return []SecondFactorType{SecondFactorType_SECOND_FACTOR_TYPE_WEBAUTHN, SecondFactorType_SECOND_FACTOR_TYPE_OTP} + if !webauthnConfigured { + return []SecondFactorType{SecondFactorType_SECOND_FACTOR_TYPE_OTP} } - return []SecondFactorType{SecondFactorType_SECOND_FACTOR_TYPE_OTP} + return []SecondFactorType{SecondFactorType_SECOND_FACTOR_TYPE_WEBAUTHN, SecondFactorType_SECOND_FACTOR_TYPE_OTP} case constants.SecondFactorOTP: return []SecondFactorType{SecondFactorType_SECOND_FACTOR_TYPE_OTP} case constants.SecondFactorWebauthn: return []SecondFactorType{SecondFactorType_SECOND_FACTOR_TYPE_WEBAUTHN} + case "": + // default to OTP + return []SecondFactorType{SecondFactorType_SECOND_FACTOR_TYPE_OTP} default: slog.WarnContext(context.Background(), "Found unknown second_factor setting", "second_factor", sf) return nil @@ -355,54 +388,46 @@ func (c *AuthPreferenceV2) SetSecondFactors(s []SecondFactorType) { c.Spec.SecondFactors = s } +// GetPreferredLocalMFA returns a server-side hint for clients to pick an MFA +// method when various options are available. +// It is empty if there is nothing to suggest. func (c *AuthPreferenceV2) GetPreferredLocalMFA() constants.SecondFactorType { - switch sf := c.GetSecondFactor(); sf { - case constants.SecondFactorOff: - return "" // Nothing to suggest. - case constants.SecondFactorOTP, constants.SecondFactorWebauthn: - return sf // Single method. - case constants.SecondFactorOn, constants.SecondFactorOptional: - // In order of preference: - // 1. WebAuthn (public-key based) - // 2. OTP - if _, err := c.GetWebauthn(); err == nil { - return constants.SecondFactorWebauthn - } - return constants.SecondFactorOTP - default: - slog.WarnContext(context.Background(), "Found unknown second_factor setting", "second_factor", sf) - return "" // Unsure, say nothing. + if c.IsSecondFactorWebauthnAllowed() { + return SecondFactorTypeWebauthnString + } + + if c.IsSecondFactorTOTPAllowed() { + return SecondFactorTypeOTPString } + + return "" +} + +// IsSecondFactorEnforced checks if second factor is enabled. +// +// TODO(Joerger): outside of tests, second factor should always be enabled. +// All calls should be removed and the old off/optional second factors removed. +func (c *AuthPreferenceV2) IsSecondFactorEnabled() bool { + return len(c.GetSecondFactors()) > 0 } -// IsSecondFactorEnforced checks if second factor is enforced (not disabled or set to optional). +// IsSecondFactorEnforced checks if second factor is enforced. +// +// TODO(Joerger): outside of tests, second factor should always be enforced. +// All calls should be removed and the old off/optional second factors removed. func (c *AuthPreferenceV2) IsSecondFactorEnforced() bool { - return c.Spec.SecondFactor != constants.SecondFactorOff && c.Spec.SecondFactor != constants.SecondFactorOptional + return len(c.GetSecondFactors()) > 0 && c.Spec.SecondFactor != constants.SecondFactorOptional } // IsSecondFactorTOTPAllowed checks if users are allowed to register TOTP devices. func (c *AuthPreferenceV2) IsSecondFactorTOTPAllowed() bool { - return c.Spec.SecondFactor == constants.SecondFactorOTP || - c.Spec.SecondFactor == constants.SecondFactorOptional || - c.Spec.SecondFactor == constants.SecondFactorOn + return slices.Contains(c.GetSecondFactors(), SecondFactorType_SECOND_FACTOR_TYPE_OTP) } // IsSecondFactorWebauthnAllowed checks if users are allowed to register // Webauthn devices. func (c *AuthPreferenceV2) IsSecondFactorWebauthnAllowed() bool { - // Is Webauthn configured and enabled? - switch _, err := c.GetWebauthn(); { - case trace.IsNotFound(err): // OK, expected to happen in some cases. - return false - case err != nil: - slog.WarnContext(context.Background(), "Got unexpected error when reading Webauthn config", "error", err) - return false - } - - // Are second factor settings in accordance? - return c.Spec.SecondFactor == constants.SecondFactorWebauthn || - c.Spec.SecondFactor == constants.SecondFactorOptional || - c.Spec.SecondFactor == constants.SecondFactorOn + return slices.Contains(c.GetSecondFactors(), SecondFactorType_SECOND_FACTOR_TYPE_WEBAUTHN) } // IsAdminActionMFAEnforced checks if admin action MFA is enforced. @@ -680,9 +705,6 @@ func (c *AuthPreferenceV2) CheckAndSetDefaults() error { if c.Spec.Type == "" { c.Spec.Type = constants.Local } - if c.Spec.SecondFactor == "" { - c.Spec.SecondFactor = constants.SecondFactorOTP - } if c.Spec.AllowLocalAuth == nil { c.Spec.AllowLocalAuth = NewBoolOption(true) } @@ -718,64 +740,43 @@ func (c *AuthPreferenceV2) CheckAndSetDefaults() error { c.Spec.SecondFactor = constants.SecondFactorWebauthn } - // Make sure second factor makes sense. - sf := c.Spec.SecondFactor - switch sf { - case constants.SecondFactorOff, constants.SecondFactorOTP: - case constants.SecondFactorWebauthn: - // If U2F is present validate it, we can derive Webauthn from it. - if c.Spec.U2F != nil { - if err := c.Spec.U2F.Check(); err != nil { - return trace.Wrap(err) - } - if c.Spec.Webauthn == nil { - // Not a problem, try to derive from U2F. - c.Spec.Webauthn = &Webauthn{} - } + // If U2F is present validate it, we can derive Webauthn from it. + if c.Spec.U2F != nil { + if err := c.Spec.U2F.Check(); err != nil { + return trace.Wrap(err) } if c.Spec.Webauthn == nil { - return trace.BadParameter("missing required webauthn configuration for second factor type %q", sf) + // Not a problem, try to derive from U2F. + c.Spec.Webauthn = &Webauthn{} } if err := c.Spec.Webauthn.CheckAndSetDefaults(c.Spec.U2F); err != nil { return trace.Wrap(err) } - case constants.SecondFactorOn, constants.SecondFactorOptional: - // The following scenarios are allowed for "on" and "optional": - // - Webauthn is configured (preferred) - // - U2F is configured, Webauthn derived from it (U2F-compat mode) + } - if c.Spec.U2F == nil && c.Spec.Webauthn == nil { - return trace.BadParameter("missing required webauthn configuration for second factor type %q", sf) - } + // If SecondFactors isn't set, set from legacy SecondFactor. + if len(c.Spec.SecondFactors) == 0 { + c.Spec.SecondFactors = SecondFactorsFromLegacySecondFactor(c.Spec.SecondFactor, c.Spec.Webauthn != nil) + } - // Is U2F configured? - if c.Spec.U2F != nil { - if err := c.Spec.U2F.Check(); err != nil { - return trace.Wrap(err) - } - if c.Spec.Webauthn == nil { - // Not a problem, try to derive from U2F. - c.Spec.Webauthn = &Webauthn{} - } + // Validate expected fields for webauthn. + hasWebauthn := c.IsSecondFactorWebauthnAllowed() + if hasWebauthn { + if c.Spec.Webauthn == nil { + return trace.BadParameter("missing required webauthn configuration") } - // Is Webauthn valid? At this point we should always have a config. if err := c.Spec.Webauthn.CheckAndSetDefaults(c.Spec.U2F); err != nil { return trace.Wrap(err) } - default: - return trace.BadParameter("second factor type %q not supported", c.Spec.SecondFactor) } // Set/validate AllowPasswordless. We need Webauthn first to do this properly. - hasWebauthn := sf == constants.SecondFactorWebauthn || - sf == constants.SecondFactorOn || - sf == constants.SecondFactorOptional switch { case c.Spec.AllowPasswordless == nil: c.Spec.AllowPasswordless = NewBoolOption(hasWebauthn) case !hasWebauthn && c.Spec.AllowPasswordless.Value: - return trace.BadParameter("missing required Webauthn configuration for passwordless=true") + return trace.BadParameter("missing required webauthn configuration for passwordless=true") } // Set/validate AllowHeadless. We need Webauthn first to do this properly. @@ -783,7 +784,7 @@ func (c *AuthPreferenceV2) CheckAndSetDefaults() error { case c.Spec.AllowHeadless == nil: c.Spec.AllowHeadless = NewBoolOption(hasWebauthn) case !hasWebauthn && c.Spec.AllowHeadless.Value: - return trace.BadParameter("missing required Webauthn configuration for headless=true") + return trace.BadParameter("missing required webauthn configuration for headless=true") } // Validate connector name for type=local. @@ -878,7 +879,7 @@ func (c *AuthPreferenceV2) CheckSetPIVSlot() { // String represents a human readable version of authentication settings. func (c *AuthPreferenceV2) String() string { - return fmt.Sprintf("AuthPreference(Type=%q,SecondFactor=%q)", c.Spec.Type, c.Spec.SecondFactor) + return fmt.Sprintf("AuthPreference(Type=%q,SecondFactors=%q)", c.Spec.Type, c.GetSecondFactors()) } // Clone returns a copy of the AuthPreference resource. @@ -1216,10 +1217,10 @@ const ( SecondFactorTypeSSOString = "sso" ) -func (s *SecondFactorType) encode() (interface{}, error) { +func (s *SecondFactorType) encode() (string, error) { switch *s { case SecondFactorType_SECOND_FACTOR_TYPE_UNSPECIFIED: - return nil, nil + return "", nil case SecondFactorType_SECOND_FACTOR_TYPE_OTP: return SecondFactorTypeOTPString, nil case SecondFactorType_SECOND_FACTOR_TYPE_WEBAUTHN: @@ -1227,7 +1228,7 @@ func (s *SecondFactorType) encode() (interface{}, error) { case SecondFactorType_SECOND_FACTOR_TYPE_SSO: return SecondFactorTypeSSOString, nil default: - return nil, trace.BadParameter("SecondFactorType invalid value %v", *s) + return "", trace.BadParameter("SecondFactorType invalid value %v", *s) } }