diff --git a/api/types/authentication.go b/api/types/authentication.go index 64dce4cd884d9..550520e099881 100644 --- a/api/types/authentication.go +++ b/api/types/authentication.go @@ -328,29 +328,12 @@ func (c *AuthPreferenceV2) GetSecondFactor() constants.SecondFactorType { return legacySecondFactorFromSecondFactors(c.Spec.SecondFactors) } -// 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 - } -} - // SetSecondFactor sets the type of second factor. func (c *AuthPreferenceV2) SetSecondFactor(s constants.SecondFactorType) { c.Spec.SecondFactor = s // Set SecondFactors from SecondFactor. - c.Spec.SecondFactors = SecondFactorsFromLegacySecondFactor(c.Spec.SecondFactor) + c.Spec.SecondFactors = secondFactorsFromLegacySecondFactor(c.Spec.SecondFactor) } // GetSecondFactors gets a list of supported second factors. @@ -360,27 +343,7 @@ func (c *AuthPreferenceV2) GetSecondFactors() []SecondFactorType { } // If SecondFactors isn't set, try to convert the old SecondFactor field. - return SecondFactorsFromLegacySecondFactor(c.Spec.SecondFactor) -} - -// SecondFactorsFromLegacySecondFactor returns the list of SecondFactorTypes supported by the given second factor type. -func SecondFactorsFromLegacySecondFactor(sf constants.SecondFactorType) []SecondFactorType { - switch sf { - case constants.SecondFactorOff: - return nil - case constants.SecondFactorOptional, constants.SecondFactorOn: - 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 - } + return secondFactorsFromLegacySecondFactor(c.Spec.SecondFactor) } // SetSecondFactors sets the list of supported second factors. @@ -396,29 +359,27 @@ func (c *AuthPreferenceV2) SetSecondFactors(s []SecondFactorType) { // It is empty if there is nothing to suggest. func (c *AuthPreferenceV2) GetPreferredLocalMFA() constants.SecondFactorType { if c.IsSecondFactorWebauthnAllowed() { - return SecondFactorTypeWebauthnString + return secondFactorTypeWebauthnString } if c.IsSecondFactorTOTPAllowed() { - return SecondFactorTypeOTPString + 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 { + // TODO(Joerger): outside of tests, second factor should always be enabled. + // All calls should be removed and the old off/optional second factors removed. return len(c.GetSecondFactors()) > 0 } // 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 { + // TODO(Joerger): outside of tests, second factor should always be enforced. + // All calls should be removed and the old off/optional second factors removed. return len(c.GetSecondFactors()) > 0 && c.Spec.SecondFactor != constants.SecondFactorOptional } @@ -718,6 +679,9 @@ func (c *AuthPreferenceV2) CheckAndSetDefaults() error { if c.Spec.Type == "" { c.Spec.Type = constants.Local } + if c.Spec.SecondFactor == "" && len(c.Spec.SecondFactors) == 0 { + c.Spec.SecondFactor = constants.SecondFactorOTP + } if c.Spec.AllowLocalAuth == nil { c.Spec.AllowLocalAuth = NewBoolOption(true) } @@ -753,28 +717,23 @@ func (c *AuthPreferenceV2) CheckAndSetDefaults() error { c.Spec.SecondFactor = 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 err := c.Spec.Webauthn.CheckAndSetDefaults(c.Spec.U2F); err != nil { - return trace.Wrap(err) - } - } - - // If SecondFactors isn't set, set from legacy SecondFactor. - if len(c.Spec.SecondFactors) == 0 { - c.Spec.SecondFactors = SecondFactorsFromLegacySecondFactor(c.Spec.SecondFactor) - } - // Validate expected fields for webauthn. hasWebauthn := c.IsSecondFactorWebauthnAllowed() if hasWebauthn { + // 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 err := c.Spec.Webauthn.CheckAndSetDefaults(c.Spec.U2F); err != nil { + return trace.Wrap(err) + } + } + if c.Spec.Webauthn == nil { return trace.BadParameter("missing required webauthn configuration") } @@ -784,6 +743,13 @@ func (c *AuthPreferenceV2) CheckAndSetDefaults() error { } } + // Validate SecondFactor if set. + switch c.Spec.SecondFactor { + case "", constants.SecondFactorOff, constants.SecondFactorOTP, constants.SecondFactorWebauthn, constants.SecondFactorOn, constants.SecondFactorOptional: + default: + return trace.BadParameter("second factor type %q not supported", c.Spec.SecondFactor) + } + // Set/validate AllowPasswordless. We need Webauthn first to do this properly. switch { case c.Spec.AllowPasswordless == nil: @@ -1221,116 +1187,3 @@ func (r *RequireMFAType) setFromEnum(val int32) error { *r = RequireMFAType(val) return nil } - -// MarshalJSON marshals SecondFactorType to boolean or string. -func (s *SecondFactorType) MarshalYAML() (interface{}, error) { - val, err := s.encode() - if err != nil { - return nil, trace.Wrap(err) - } - return val, nil -} - -// UnmarshalYAML supports parsing SecondFactorType from boolean or alias. -func (s *SecondFactorType) UnmarshalYAML(unmarshal func(interface{}) error) error { - var val interface{} - err := unmarshal(&val) - if err != nil { - return trace.Wrap(err) - } - - err = s.decode(val) - return trace.Wrap(err) -} - -// MarshalJSON marshals SecondFactorType to boolean or string. -func (s *SecondFactorType) MarshalJSON() ([]byte, error) { - val, err := s.encode() - if err != nil { - return nil, trace.Wrap(err) - } - out, err := json.Marshal(val) - return out, trace.Wrap(err) -} - -// UnmarshalJSON supports parsing SecondFactorType from boolean or alias. -func (s *SecondFactorType) UnmarshalJSON(data []byte) error { - var val interface{} - err := json.Unmarshal(data, &val) - if err != nil { - return trace.Wrap(err) - } - - err = s.decode(val) - return trace.Wrap(err) -} - -const ( - // SecondFactorTypeOTPString is the string representation of SecondFactorType_SECOND_FACTOR_TYPE_OTP - SecondFactorTypeOTPString = "otp" - // SecondFactorTypeWebauthnString is the string representation of SecondFactorType_SECOND_FACTOR_TYPE_WEBAUTHN - SecondFactorTypeWebauthnString = "webauthn" - // SecondFactorTypeSSOString is the string representation of SecondFactorType_SECOND_FACTOR_TYPE_SSO - SecondFactorTypeSSOString = "sso" -) - -// ToString returns the user friendly string representation of the second factor type. -func (s *SecondFactorType) ToString() string { - str, _ := s.encode() - return str -} - -func (s *SecondFactorType) encode() (string, error) { - switch *s { - case SecondFactorType_SECOND_FACTOR_TYPE_UNSPECIFIED: - return "", nil - case SecondFactorType_SECOND_FACTOR_TYPE_OTP: - return SecondFactorTypeOTPString, nil - case SecondFactorType_SECOND_FACTOR_TYPE_WEBAUTHN: - return SecondFactorTypeWebauthnString, nil - case SecondFactorType_SECOND_FACTOR_TYPE_SSO: - return SecondFactorTypeSSOString, nil - default: - return "", trace.BadParameter("SecondFactorType invalid value %v", *s) - } -} - -func (s *SecondFactorType) decode(val any) error { - switch v := val.(type) { - case string: - switch v { - case SecondFactorTypeOTPString: - *s = SecondFactorType_SECOND_FACTOR_TYPE_OTP - case SecondFactorTypeWebauthnString: - *s = SecondFactorType_SECOND_FACTOR_TYPE_WEBAUTHN - case SecondFactorTypeSSOString: - *s = SecondFactorType_SECOND_FACTOR_TYPE_SSO - case "": - *s = SecondFactorType_SECOND_FACTOR_TYPE_UNSPECIFIED - default: - return trace.BadParameter("SecondFactorType invalid value %v", val) - } - case int32: - return trace.Wrap(s.setFromEnum(v)) - case int64: - return trace.Wrap(s.setFromEnum(int32(v))) - case int: - return trace.Wrap(s.setFromEnum(int32(v))) - case float64: - return trace.Wrap(s.setFromEnum(int32(v))) - case float32: - return trace.Wrap(s.setFromEnum(int32(v))) - default: - return trace.BadParameter("RequireMFAType invalid type %T", val) - } - return nil -} - -// setFromEnum sets the value from enum value as int32. -func (r *SecondFactorType) setFromEnum(val int32) error { - if _, ok := SecondFactorType_name[val]; !ok { - return trace.BadParameter("invalid SecondFactorType mode %v", val) - } - *r = SecondFactorType(val) - return nil -} diff --git a/api/types/second_factor.go b/api/types/second_factor.go new file mode 100644 index 0000000000000..a448fb83151c9 --- /dev/null +++ b/api/types/second_factor.go @@ -0,0 +1,172 @@ +/* +Copyright 2024 Gravitational, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package types + +import ( + "encoding/json" + "slices" + + "github.com/gravitational/trace" + + "github.com/gravitational/teleport/api/constants" +) + +// 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 + } +} + +// secondFactorsFromLegacySecondFactor returns the list of SecondFactorTypes supported by the given second factor type. +func secondFactorsFromLegacySecondFactor(sf constants.SecondFactorType) []SecondFactorType { + switch sf { + case constants.SecondFactorOff: + return nil + case constants.SecondFactorOptional, constants.SecondFactorOn: + 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} + default: + return nil + } +} + +// MarshalJSON marshals SecondFactorType to string. +func (s *SecondFactorType) MarshalYAML() (interface{}, error) { + val, err := s.encode() + if err != nil { + return nil, trace.Wrap(err) + } + return val, nil +} + +// UnmarshalYAML supports parsing SecondFactorType from string. +func (s *SecondFactorType) UnmarshalYAML(unmarshal func(interface{}) error) error { + var val interface{} + err := unmarshal(&val) + if err != nil { + return trace.Wrap(err) + } + + err = s.decode(val) + return trace.Wrap(err) +} + +// MarshalJSON marshals SecondFactorType to string. +func (s *SecondFactorType) MarshalJSON() ([]byte, error) { + val, err := s.encode() + if err != nil { + return nil, trace.Wrap(err) + } + out, err := json.Marshal(val) + return out, trace.Wrap(err) +} + +// UnmarshalJSON supports parsing SecondFactorType from string. +func (s *SecondFactorType) UnmarshalJSON(data []byte) error { + var val interface{} + err := json.Unmarshal(data, &val) + if err != nil { + return trace.Wrap(err) + } + + err = s.decode(val) + return trace.Wrap(err) +} + +const ( + // secondFactorTypeOTPString is the string representation of SecondFactorType_SECOND_FACTOR_TYPE_OTP + secondFactorTypeOTPString = "otp" + // secondFactorTypeWebauthnString is the string representation of SecondFactorType_SECOND_FACTOR_TYPE_WEBAUTHN + secondFactorTypeWebauthnString = "webauthn" + // secondFactorTypeSSOString is the string representation of SecondFactorType_SECOND_FACTOR_TYPE_SSO + secondFactorTypeSSOString = "sso" +) + +// ToString returns the user friendly string representation of the second factor type. +func (s *SecondFactorType) ToString() string { + str, _ := s.encode() + return str +} + +func (s *SecondFactorType) encode() (string, error) { + switch *s { + case SecondFactorType_SECOND_FACTOR_TYPE_UNSPECIFIED: + return "", nil + case SecondFactorType_SECOND_FACTOR_TYPE_OTP: + return secondFactorTypeOTPString, nil + case SecondFactorType_SECOND_FACTOR_TYPE_WEBAUTHN: + return secondFactorTypeWebauthnString, nil + case SecondFactorType_SECOND_FACTOR_TYPE_SSO: + return secondFactorTypeSSOString, nil + default: + return "", trace.BadParameter("SecondFactorType invalid value %v", *s) + } +} + +func (s *SecondFactorType) decode(val any) error { + switch v := val.(type) { + case string: + switch v { + case secondFactorTypeOTPString: + *s = SecondFactorType_SECOND_FACTOR_TYPE_OTP + case secondFactorTypeWebauthnString: + *s = SecondFactorType_SECOND_FACTOR_TYPE_WEBAUTHN + case secondFactorTypeSSOString: + *s = SecondFactorType_SECOND_FACTOR_TYPE_SSO + case "": + *s = SecondFactorType_SECOND_FACTOR_TYPE_UNSPECIFIED + default: + return trace.BadParameter("SecondFactorType invalid value %v", val) + } + case int32: + return trace.Wrap(s.setFromEnum(v)) + case int64: + return trace.Wrap(s.setFromEnum(int32(v))) + case int: + return trace.Wrap(s.setFromEnum(int32(v))) + case float64: + return trace.Wrap(s.setFromEnum(int32(v))) + case float32: + return trace.Wrap(s.setFromEnum(int32(v))) + default: + return trace.BadParameter("RequireMFAType invalid type %T", val) + } + return nil +} + +// setFromEnum sets the value from enum value as int32. +func (r *SecondFactorType) setFromEnum(val int32) error { + if _, ok := SecondFactorType_name[val]; !ok { + return trace.BadParameter("invalid SecondFactorType mode %v", val) + } + *r = SecondFactorType(val) + return nil +} diff --git a/lib/config/configuration_test.go b/lib/config/configuration_test.go index 40507beddf618..2984dec892c66 100644 --- a/lib/config/configuration_test.go +++ b/lib/config/configuration_test.go @@ -795,9 +795,8 @@ func TestApplyConfig(t *testing.T) { Labels: map[string]string{types.OriginLabel: types.OriginConfigFile}, }, Spec: types.AuthPreferenceSpecV2{ - Type: constants.Local, - SecondFactor: constants.SecondFactorOptional, - SecondFactors: types.SecondFactorsFromLegacySecondFactor(constants.SecondFactorOptional), + Type: constants.Local, + SecondFactor: constants.SecondFactorOptional, Webauthn: &types.Webauthn{ RPID: "goteleport.com", AttestationAllowedCAs: []string{ @@ -4979,9 +4978,9 @@ func TestProxyUntrustedCert(t *testing.T) { certDir := t.TempDir() certPath := filepath.Join(certDir, "leaf.crt") keyPath := filepath.Join(certDir, "leaf.key") - err = os.WriteFile(certPath, leafCert, 0600) + err = os.WriteFile(certPath, leafCert, 0o600) require.NoError(t, err) - err = os.WriteFile(keyPath, leafPrivPem, 0600) + err = os.WriteFile(keyPath, leafPrivPem, 0o600) require.NoError(t, err) fc := &FileConfig{ @@ -5052,7 +5051,6 @@ debug_service: } func TestSignatureAlgorithmSuite(t *testing.T) { - for desc, tc := range map[string]struct { fips bool hsm bool diff --git a/lib/config/fileconf.go b/lib/config/fileconf.go index e78956a41c761..155085dd5fb38 100644 --- a/lib/config/fileconf.go +++ b/lib/config/fileconf.go @@ -1085,22 +1085,10 @@ func (a *AuthenticationConfig) Parse() (types.AuthPreference, error) { } } - if a.SecondFactor != "" { - secondFactors := types.SecondFactorsFromLegacySecondFactor(a.SecondFactor) - var secondFactorStrings []string - for _, sf := range secondFactors { - secondFactorStrings = append(secondFactorStrings, sf.ToString()) - } - - log.Warnf(``+ - `The "second_factor" setting is marked for removal in favor of second_factors. `+ - `Please update your configuration to use second_factors. e.g. "second_factors: %v".`, secondFactorStrings) - - if a.SecondFactors != nil { - log.Warnf(`` + - `second_factor and second_factors are both set. second_factors will take precedence. ` + - `second_factor should be unset to remove this warning.`) - } + if a.SecondFactor != "" && a.SecondFactors != nil { + log.Warn(`` + + `second_factor and second_factors are both set. second_factors will take precedence. ` + + `second_factor should be unset to remove this warning.`) } return types.NewAuthPreferenceFromConfigFile(types.AuthPreferenceSpecV2{