Skip to content

Commit

Permalink
Update auth preference methods to use and prefer SecondFactors.
Browse files Browse the repository at this point in the history
  • Loading branch information
Joerger committed Oct 4, 2024
1 parent 8c869ac commit 4e28795
Showing 1 changed file with 88 additions and 87 deletions.
175 changes: 88 additions & 87 deletions api/types/authentication.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"fmt"
"log/slog"
"net/url"
"slices"
"strings"
"time"

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -318,32 +320,63 @@ 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.
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
Expand All @@ -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.
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -718,72 +740,51 @@ 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.
switch {
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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -1216,18 +1217,18 @@ 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:
return SecondFactorTypeWebauthnString, nil
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)
}
}

Expand Down

0 comments on commit 4e28795

Please sign in to comment.