feat(configuration): configurable default second factor method (#3081)

This allows configuring the default second factor method.
pull/3145/head^2
James Elliott 2022-04-18 09:58:24 +10:00 committed by GitHub
parent 9bd10e6409
commit e99fb7a08f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 353 additions and 319 deletions

View File

@ -27,6 +27,11 @@ jwt_secret: a_very_important_secret
## Note: this parameter is optional. If not provided, user won't be redirected upon successful authentication.
default_redirection_url: https://home.example.com/
## Set the default 2FA method for new users and for when a user has a preferred method configured that has been
## disabled. This setting must be a method that is enabled.
## Options are totp, webauthn, mobile_push.
default_2fa_method: ""
##
## Server Configuration
##

View File

@ -59,3 +59,28 @@ redirected to that URL. If not defined, the user is not redirected after authent
```yaml
default_redirection_url: https://home.example.com:8080/
```
## default_2fa_method
<div markdown="1">
type: string
{: .label .label-config .label-purple }
default: ""
{: .label .label-config .label-blue }
required: no
{: .label .label-config .label-green }
</div>
Sets the default second factor method for users. This must be blank or one of the enabled methods. New users will by
default have this method selected for them. In addition if this was configured to `webauthn` and a user had the `totp`
method, and the `totp` method was disabled in the configuration, the users' method would automatically update to the
`webauthn` method.
Options are:
- totp
- webauthn
- mobile_push
```yaml
default_2fa_method: totp
```

View File

@ -2,7 +2,7 @@
layout: default
title: Password Policy
parent: Configuration
nav_order: 17
nav_order: 18
---
# Password Policy

View File

@ -28,21 +28,21 @@ Here is the list of the environment variables which are considered secrets and c
secrets can be loaded into the configuration if they end with one of the suffixes above, you can set the value of any
other configuration using the environment but instead of loading a file the value of the environment variable is used.
|Configuration Key |Environment Variable |
|:-----------------------------------------------:|:------------------------------------------------------:|
|tls_key |AUTHELIA_TLS_KEY_FILE |
|jwt_secret |AUTHELIA_JWT_SECRET_FILE |
|duo_api.secret_key |AUTHELIA_DUO_API_SECRET_KEY_FILE |
|session.secret |AUTHELIA_SESSION_SECRET_FILE |
|session.redis.password |AUTHELIA_SESSION_REDIS_PASSWORD_FILE |
|session.redis.high_availability.sentinel_password|AUTHELIA_REDIS_HIGH_AVAILABILITY_SENTINEL_PASSWORD_FILE |
|storage.encryption_key |AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE |
|storage.mysql.password |AUTHELIA_STORAGE_MYSQL_PASSWORD_FILE |
|storage.postgres.password |AUTHELIA_STORAGE_POSTGRES_PASSWORD_FILE |
|notifier.smtp.password |AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE |
|authentication_backend.ldap.password |AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE |
|identity_providers.oidc.issuer_private_key |AUTHELIA_IDENTITY_PROVIDERS_OIDC_ISSUER_PRIVATE_KEY_FILE|
|identity_providers.oidc.hmac_secret |AUTHELIA_IDENTITY_PROVIDERS_OIDC_HMAC_SECRET_FILE |
| Configuration Key | Environment Variable |
|:-------------------------------------------------:|:--------------------------------------------------------:|
| tls_key | AUTHELIA_TLS_KEY_FILE |
| jwt_secret | AUTHELIA_JWT_SECRET_FILE |
| duo_api.secret_key | AUTHELIA_DUO_API_SECRET_KEY_FILE |
| session.secret | AUTHELIA_SESSION_SECRET_FILE |
| session.redis.password | AUTHELIA_SESSION_REDIS_PASSWORD_FILE |
| session.redis.high_availability.sentinel_password | AUTHELIA_REDIS_HIGH_AVAILABILITY_SENTINEL_PASSWORD_FILE |
| storage.encryption_key | AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE |
| storage.mysql.password | AUTHELIA_STORAGE_MYSQL_PASSWORD_FILE |
| storage.postgres.password | AUTHELIA_STORAGE_POSTGRES_PASSWORD_FILE |
| notifier.smtp.password | AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE |
| authentication_backend.ldap.password | AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE |
| identity_providers.oidc.issuer_private_key | AUTHELIA_IDENTITY_PROVIDERS_OIDC_ISSUER_PRIVATE_KEY_FILE |
| identity_providers.oidc.hmac_secret | AUTHELIA_IDENTITY_PROVIDERS_OIDC_HMAC_SECRET_FILE |
## Secrets in configuration file

View File

@ -2,7 +2,7 @@
layout: default
title: Webauthn
parent: Configuration
nav_order: 16
nav_order: 17
---
The Webauthn section has tunable options for the Webauthn implementation.

View File

@ -27,6 +27,11 @@ jwt_secret: a_very_important_secret
## Note: this parameter is optional. If not provided, user won't be redirected upon successful authentication.
default_redirection_url: https://home.example.com/
## Set the default 2FA method for new users and for when a user has a preferred method configured that has been
## disabled. This setting must be a method that is enabled.
## Options are totp, webauthn, mobile_push.
default_2fa_method: ""
##
## Server Configuration
##

View File

@ -6,6 +6,7 @@ type Configuration struct {
CertificatesDirectory string `koanf:"certificates_directory"`
JWTSecret string `koanf:"jwt_secret"`
DefaultRedirectionURL string `koanf:"default_redirection_url"`
Default2FAMethod string `koanf:"default_2fa_method"`
Log LogConfiguration `koanf:"log"`
IdentityProviders IdentityProvidersConfiguration `koanf:"identity_providers"`

View File

@ -12,6 +12,7 @@ var Keys = []string{
"certificates_directory",
"jwt_secret",
"default_redirection_url",
"default_2fa_method",
"log.level",
"log.format",
"log.file_path",

View File

@ -1,253 +0,0 @@
package schema
import (
"testing"
"github.com/stretchr/testify/assert"
)
var ValidKeys = []string{
// Root Keys.
"certificates_directory",
"theme",
"default_redirection_url",
"jwt_secret",
// Log keys.
"log.level",
"log.format",
"log.file_path",
"log.keep_stdout",
// Server Keys.
"server.host",
"server.port",
"server.read_buffer_size",
"server.write_buffer_size",
"server.path",
"server.asset_path",
"server.enable_pprof",
"server.enable_expvars",
"server.disable_healthcheck",
"server.tls.key",
"server.tls.certificate",
"server.tls.client_certificates",
"server.headers.csp_template",
// TOTP Keys.
"totp.disable",
"totp.issuer",
"totp.algorithm",
"totp.digits",
"totp.period",
"totp.skew",
"totp.secret_size",
// Webauthn Keys.
"webauthn.disable",
"webauthn.display_name",
"webauthn.attestation_conveyance_preference",
"webauthn.user_verification",
"webauthn.timeout",
// DUO API Keys.
"duo_api.disable",
"duo_api.hostname",
"duo_api.enable_self_enrollment",
"duo_api.secret_key",
"duo_api.integration_key",
// Access Control Keys.
"access_control.default_policy",
"access_control.networks",
"access_control.networks[].name",
"access_control.networks[].networks",
"access_control.rules",
"access_control.rules[].domain",
"access_control.rules[].domain_regex",
"access_control.rules[].methods",
"access_control.rules[].networks",
"access_control.rules[].subject",
"access_control.rules[].policy",
"access_control.rules[].resources",
// Session Keys.
"session.name",
"session.domain",
"session.secret",
"session.same_site",
"session.expiration",
"session.inactivity",
"session.remember_me_duration",
// Redis Session Keys.
"session.redis.host",
"session.redis.port",
"session.redis.username",
"session.redis.password",
"session.redis.database_index",
"session.redis.maximum_active_connections",
"session.redis.minimum_idle_connections",
"session.redis.tls.minimum_version",
"session.redis.tls.skip_verify",
"session.redis.tls.server_name",
"session.redis.high_availability.sentinel_name",
"session.redis.high_availability.sentinel_username",
"session.redis.high_availability.sentinel_password",
"session.redis.high_availability.nodes",
"session.redis.high_availability.nodes[].host",
"session.redis.high_availability.nodes[].port",
"session.redis.high_availability.route_by_latency",
"session.redis.high_availability.route_randomly",
// Storage Keys.
"storage.encryption_key",
// Local Storage Keys.
"storage.local.path",
// MySQL Storage Keys.
"storage.mysql.host",
"storage.mysql.port",
"storage.mysql.database",
"storage.mysql.username",
"storage.mysql.password",
"storage.mysql.timeout",
// PostgreSQL Storage Keys.
"storage.postgres.host",
"storage.postgres.port",
"storage.postgres.database",
"storage.postgres.username",
"storage.postgres.password",
"storage.postgres.timeout",
"storage.postgres.schema",
"storage.postgres.ssl.mode",
"storage.postgres.ssl.root_certificate",
"storage.postgres.ssl.certificate",
"storage.postgres.ssl.key",
"storage.postgres.sslmode", // Deprecated. TODO: Remove in v4.36.0.
// FileSystem Notifier Keys.
"notifier.filesystem.filename",
"notifier.disable_startup_check",
// SMTP Notifier Keys.
"notifier.smtp.host",
"notifier.smtp.port",
"notifier.smtp.timeout",
"notifier.smtp.username",
"notifier.smtp.password",
"notifier.smtp.identifier",
"notifier.smtp.sender",
"notifier.smtp.subject",
"notifier.smtp.startup_check_address",
"notifier.smtp.disable_require_tls",
"notifier.smtp.disable_html_emails",
"notifier.smtp.tls.minimum_version",
"notifier.smtp.tls.skip_verify",
"notifier.smtp.tls.server_name",
"notifier.template_path",
// Regulation Keys.
"regulation.max_retries",
"regulation.find_time",
"regulation.ban_time",
// Authentication Backend Keys.
"authentication_backend.disable_reset_password",
"authentication_backend.password_reset.custom_url",
"authentication_backend.refresh_interval",
// LDAP Authentication Backend Keys.
"authentication_backend.ldap.implementation",
"authentication_backend.ldap.url",
"authentication_backend.ldap.timeout",
"authentication_backend.ldap.base_dn",
"authentication_backend.ldap.username_attribute",
"authentication_backend.ldap.additional_users_dn",
"authentication_backend.ldap.users_filter",
"authentication_backend.ldap.additional_groups_dn",
"authentication_backend.ldap.groups_filter",
"authentication_backend.ldap.group_name_attribute",
"authentication_backend.ldap.mail_attribute",
"authentication_backend.ldap.display_name_attribute",
"authentication_backend.ldap.user",
"authentication_backend.ldap.password",
"authentication_backend.ldap.start_tls",
"authentication_backend.ldap.tls.minimum_version",
"authentication_backend.ldap.tls.skip_verify",
"authentication_backend.ldap.tls.server_name",
// File Authentication Backend Keys.
"authentication_backend.file.path",
"authentication_backend.file.password.algorithm",
"authentication_backend.file.password.iterations",
"authentication_backend.file.password.key_length",
"authentication_backend.file.password.salt_length",
"authentication_backend.file.password.memory",
"authentication_backend.file.password.parallelism",
// Identity Provider Keys.
"identity_providers.oidc.hmac_secret",
"identity_providers.oidc.issuer_private_key",
"identity_providers.oidc.id_token_lifespan",
"identity_providers.oidc.access_token_lifespan",
"identity_providers.oidc.refresh_token_lifespan",
"identity_providers.oidc.authorize_code_lifespan",
"identity_providers.oidc.enforce_pkce",
"identity_providers.oidc.enable_pkce_plain_challenge",
"identity_providers.oidc.enable_client_debug_messages",
"identity_providers.oidc.minimum_parameter_entropy",
"identity_providers.oidc.cors.endpoints",
"identity_providers.oidc.cors.allowed_origins",
"identity_providers.oidc.cors.allowed_origins_from_client_redirect_uris",
"identity_providers.oidc.clients",
"identity_providers.oidc.clients[].id",
"identity_providers.oidc.clients[].description",
"identity_providers.oidc.clients[].secret",
"identity_providers.oidc.clients[].sector_identifier",
"identity_providers.oidc.clients[].public",
"identity_providers.oidc.clients[].redirect_uris",
"identity_providers.oidc.clients[].authorization_policy",
"identity_providers.oidc.clients[].pre_configured_consent_duration",
"identity_providers.oidc.clients[].scopes",
"identity_providers.oidc.clients[].audience",
"identity_providers.oidc.clients[].grant_types",
"identity_providers.oidc.clients[].response_types",
"identity_providers.oidc.clients[].response_modes",
"identity_providers.oidc.clients[].userinfo_signing_algorithm",
// NTP keys.
"ntp.address",
"ntp.version",
"ntp.max_desync",
"ntp.disable_startup_check",
"ntp.disable_failure",
// Password Policy keys.
"password_policy.standard.enabled",
"password_policy.standard.min_length",
"password_policy.standard.max_length",
"password_policy.standard.require_uppercase",
"password_policy.standard.require_lowercase",
"password_policy.standard.require_number",
"password_policy.standard.require_special",
"password_policy.zxcvbn.enabled",
"password_policy.zxcvbn.min_score",
}
func TestOldKeys(t *testing.T) {
for _, key := range ValidKeys {
assert.Contains(t, Keys, key)
}
for _, key := range Keys {
assert.Contains(t, ValidKeys, key)
}
}
func TestDuplicates(t *testing.T) {
assert.Equal(t, len(Keys), len(ValidKeys))
}

View File

@ -33,6 +33,8 @@ func ValidateConfiguration(config *schema.Configuration, validator *schema.Struc
}
}
validateDefault2FAMethod(config, validator)
ValidateTheme(config, validator)
ValidateLog(config, validator)
@ -65,3 +67,33 @@ func ValidateConfiguration(config *schema.Configuration, validator *schema.Struc
ValidatePasswordPolicy(&config.PasswordPolicy, validator)
}
func validateDefault2FAMethod(config *schema.Configuration, validator *schema.StructValidator) {
if config.Default2FAMethod == "" {
return
}
if !utils.IsStringInSlice(config.Default2FAMethod, validDefault2FAMethods) {
validator.Push(fmt.Errorf(errFmtInvalidDefault2FAMethod, config.Default2FAMethod, strings.Join(validDefault2FAMethods, "', '")))
return
}
var enabledMethods []string
if !config.TOTP.Disable {
enabledMethods = append(enabledMethods, "totp")
}
if !config.Webauthn.Disable {
enabledMethods = append(enabledMethods, "webauthn")
}
if !config.DuoAPI.Disable {
enabledMethods = append(enabledMethods, "mobile_push")
}
if !utils.IsStringInSlice(config.Default2FAMethod, enabledMethods) {
validator.Push(fmt.Errorf(errFmtInvalidDefault2FAMethodDisabled, config.Default2FAMethod, strings.Join(enabledMethods, "', '")))
}
}

View File

@ -1,6 +1,7 @@
package validator
import (
"fmt"
"runtime"
"testing"
@ -158,3 +159,114 @@ func TestShouldNotRaiseErrorOnValidCertificatesDirectory(t *testing.T) {
assert.EqualError(t, validator.Warnings()[0], "access control: no rules have been specified so the 'default_policy' of 'two_factor' is going to be applied to all requests")
}
func TestValidateDefault2FAMethod(t *testing.T) {
testCases := []struct {
desc string
have *schema.Configuration
expectedErrs []string
}{
{
desc: "ShouldAllowConfiguredMethodTOTP",
have: &schema.Configuration{
Default2FAMethod: "totp",
DuoAPI: schema.DuoAPIConfiguration{
SecretKey: "a key",
IntegrationKey: "another key",
Hostname: "none",
},
},
},
{
desc: "ShouldAllowConfiguredMethodWebauthn",
have: &schema.Configuration{
Default2FAMethod: "webauthn",
DuoAPI: schema.DuoAPIConfiguration{
SecretKey: "a key",
IntegrationKey: "another key",
Hostname: "none",
},
},
},
{
desc: "ShouldAllowConfiguredMethodMobilePush",
have: &schema.Configuration{
Default2FAMethod: "mobile_push",
DuoAPI: schema.DuoAPIConfiguration{
SecretKey: "a key",
IntegrationKey: "another key",
Hostname: "none",
},
},
},
{
desc: "ShouldNotAllowDisabledMethodTOTP",
have: &schema.Configuration{
Default2FAMethod: "totp",
DuoAPI: schema.DuoAPIConfiguration{
SecretKey: "a key",
IntegrationKey: "another key",
Hostname: "none",
},
TOTP: schema.TOTPConfiguration{Disable: true},
},
expectedErrs: []string{
"option 'default_2fa_method' is configured as 'totp' but must be one of the following enabled method values: 'webauthn', 'mobile_push'",
},
},
{
desc: "ShouldNotAllowDisabledMethodWebauthn",
have: &schema.Configuration{
Default2FAMethod: "webauthn",
DuoAPI: schema.DuoAPIConfiguration{
SecretKey: "a key",
IntegrationKey: "another key",
Hostname: "none",
},
Webauthn: schema.WebauthnConfiguration{Disable: true},
},
expectedErrs: []string{
"option 'default_2fa_method' is configured as 'webauthn' but must be one of the following enabled method values: 'totp', 'mobile_push'",
},
},
{
desc: "ShouldNotAllowDisabledMethodMobilePush",
have: &schema.Configuration{
Default2FAMethod: "mobile_push",
DuoAPI: schema.DuoAPIConfiguration{Disable: true},
},
expectedErrs: []string{
"option 'default_2fa_method' is configured as 'mobile_push' but must be one of the following enabled method values: 'totp', 'webauthn'",
},
},
{
desc: "ShouldNotAllowInvalidMethodDuo",
have: &schema.Configuration{
Default2FAMethod: "duo",
},
expectedErrs: []string{
"option 'default_2fa_method' is configured as 'duo' but must be one of the following values: 'totp', 'webauthn', 'mobile_push'",
},
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
validator := schema.NewStructValidator()
validateDefault2FAMethod(tc.have, validator)
assert.Len(t, validator.Warnings(), 0)
errs := validator.Errors()
require.Len(t, errs, len(tc.expectedErrs))
for i, expected := range tc.expectedErrs {
t.Run(fmt.Sprintf("Err%d", i+1), func(t *testing.T) {
assert.EqualError(t, errs[i], expected)
})
}
})
}
}

View File

@ -265,6 +265,11 @@ const (
TODO (cont): The main consideration is making sure we do not overwrite the destination key name if it already exists.
*/
errFmtInvalidDefault2FAMethod = "option 'default_2fa_method' is configured as '%s' but must be one of " +
"the following values: '%s'"
errFmtInvalidDefault2FAMethodDisabled = "option 'default_2fa_method' is configured as '%s' " +
"but must be one of the following enabled method values: '%s'"
errFmtReplacedConfigurationKey = "invalid configuration key '%s' was replaced by '%s'"
errFmtLoggingLevelInvalid = "log: option 'level' must be one of '%s' but it is configured as '%s'"
@ -292,6 +297,8 @@ var validACLHTTPMethodVerbs = append(validRFC7231HTTPMethodVerbs, validRFC4918HT
var validACLRulePolicies = []string{policyBypass, policyOneFactor, policyTwoFactor, policyDeny}
var validDefault2FAMethods = []string{"totp", "webauthn", "mobile_push"}
var validOIDCScopes = []string{oidc.ScopeOpenID, oidc.ScopeEmail, oidc.ScopeProfile, oidc.ScopeGroups, "offline_access"}
var validOIDCGrantTypes = []string{"implicit", "refresh_token", "authorization_code", "password", "client_credentials"}
var validOIDCResponseModes = []string{"form_post", "query", "fragment"}

View File

@ -10,7 +10,7 @@ import (
"github.com/authelia/authelia/v4/internal/utils"
)
// ValidateIdentityProviders validates and update IdentityProviders configuration.
// ValidateIdentityProviders validates and updates the IdentityProviders configuration.
func ValidateIdentityProviders(config *schema.IdentityProvidersConfiguration, validator *schema.StructValidator) {
validateOIDC(config.OIDC, validator)
}

View File

@ -39,7 +39,7 @@ func UserInfoPOST(ctx *middlewares.AutheliaCtx) {
changed bool
)
if changed = userInfo.SetDefaultPreferred2FAMethod(ctx.AvailableSecondFactorMethods()); changed {
if changed = userInfo.SetDefaultPreferred2FAMethod(ctx.AvailableSecondFactorMethods(), ctx.Configuration.Default2FAMethod); changed {
if err = ctx.Providers.StorageProvider.SavePreferred2FAMethod(ctx, userSession.Username, userInfo.Method); err != nil {
ctx.Error(fmt.Errorf("unable to save user two factor method: %v", err), messageOperationFailed)
return

View File

@ -23,7 +23,7 @@ type UserInfo struct {
}
// SetDefaultPreferred2FAMethod configures the default method based on what is configured as available and the users available methods.
func (i *UserInfo) SetDefaultPreferred2FAMethod(methods []string) (changed bool) {
func (i *UserInfo) SetDefaultPreferred2FAMethod(methods []string, fallback string) (changed bool) {
if len(methods) == 0 {
// No point attempting to change the method if no methods are available.
return false
@ -33,11 +33,20 @@ func (i *UserInfo) SetDefaultPreferred2FAMethod(methods []string) (changed bool)
totp, webauthn, duo := utils.IsStringInSlice(SecondFactorMethodTOTP, methods), utils.IsStringInSlice(SecondFactorMethodWebauthn, methods), utils.IsStringInSlice(SecondFactorMethodDuo, methods)
if i.Method != "" && !utils.IsStringInSlice(i.Method, methods) {
if i.Method == "" && utils.IsStringInSlice(fallback, methods) {
i.Method = fallback
} else if i.Method != "" && !utils.IsStringInSlice(i.Method, methods) {
i.Method = ""
}
if i.Method == "" {
i.setMethod(totp, webauthn, duo, methods, fallback)
}
return before != i.Method
}
func (i *UserInfo) setMethod(totp, webauthn, duo bool, methods []string, fallback string) {
switch {
case i.HasTOTP && totp:
i.Method = SecondFactorMethodTOTP
@ -45,6 +54,8 @@ func (i *UserInfo) SetDefaultPreferred2FAMethod(methods []string) (changed bool)
i.Method = SecondFactorMethodWebauthn
case i.HasDuo && duo:
i.Method = SecondFactorMethodDuo
case fallback != "" && utils.IsStringInSlice(fallback, methods):
i.Method = fallback
case totp:
i.Method = SecondFactorMethodTOTP
case webauthn:
@ -52,7 +63,4 @@ func (i *UserInfo) SetDefaultPreferred2FAMethod(methods []string) (changed bool)
case duo:
i.Method = SecondFactorMethodDuo
}
}
return before != i.Method
}

View File

@ -8,10 +8,10 @@ import (
"github.com/stretchr/testify/assert"
)
func TestUserInfo_SetDefaultMethod_ShouldConfigureConfigDefault(t *testing.T) {
func TestUserInfo_SetDefaultMethod(t *testing.T) {
none := "none"
testName := func(i int, have UserInfo, availableMethods []string) string {
testName := func(i int, have UserInfo, methods []string, fallback string) string {
method := have.Method
if method == "" {
@ -37,18 +37,25 @@ func TestUserInfo_SetDefaultMethod_ShouldConfigureConfigDefault(t *testing.T) {
}
available := none
if len(availableMethods) != 0 {
available = strings.Join(availableMethods, " ")
if len(methods) != 0 {
available = strings.Join(methods, " ")
}
return fmt.Sprintf("%d/method %s%s/available methods %s", i+1, method, has, available)
if fallback != "" {
fallback = "/fallback " + fallback
}
return fmt.Sprintf("%d/method %s%s/available methods %s%s", i+1, method, has, available, fallback)
}
testCases := []struct {
have UserInfo
availableMethods []string
changed bool
want UserInfo
methods []string
fallback string
changed bool
}{
{
have: UserInfo{
@ -57,14 +64,14 @@ func TestUserInfo_SetDefaultMethod_ShouldConfigureConfigDefault(t *testing.T) {
HasTOTP: true,
HasWebauthn: true,
},
availableMethods: []string{SecondFactorMethodWebauthn, SecondFactorMethodDuo},
changed: true,
want: UserInfo{
Method: SecondFactorMethodWebauthn,
HasDuo: true,
HasTOTP: true,
HasWebauthn: true,
},
methods: []string{SecondFactorMethodWebauthn, SecondFactorMethodDuo},
changed: true,
},
{
have: UserInfo{
@ -72,14 +79,14 @@ func TestUserInfo_SetDefaultMethod_ShouldConfigureConfigDefault(t *testing.T) {
HasTOTP: true,
HasWebauthn: true,
},
availableMethods: []string{SecondFactorMethodTOTP, SecondFactorMethodWebauthn, SecondFactorMethodDuo},
changed: true,
want: UserInfo{
Method: SecondFactorMethodTOTP,
HasDuo: true,
HasTOTP: true,
HasWebauthn: true,
},
methods: []string{SecondFactorMethodTOTP, SecondFactorMethodWebauthn, SecondFactorMethodDuo},
changed: true,
},
{
have: UserInfo{
@ -88,14 +95,14 @@ func TestUserInfo_SetDefaultMethod_ShouldConfigureConfigDefault(t *testing.T) {
HasTOTP: false,
HasWebauthn: false,
},
availableMethods: []string{SecondFactorMethodTOTP},
changed: true,
want: UserInfo{
Method: SecondFactorMethodTOTP,
HasDuo: true,
HasTOTP: false,
HasWebauthn: false,
},
methods: []string{SecondFactorMethodTOTP},
changed: true,
},
{
have: UserInfo{
@ -104,14 +111,14 @@ func TestUserInfo_SetDefaultMethod_ShouldConfigureConfigDefault(t *testing.T) {
HasTOTP: false,
HasWebauthn: false,
},
availableMethods: []string{SecondFactorMethodTOTP},
changed: true,
want: UserInfo{
Method: SecondFactorMethodTOTP,
HasDuo: false,
HasTOTP: false,
HasWebauthn: false,
},
methods: []string{SecondFactorMethodTOTP},
changed: true,
},
{
have: UserInfo{
@ -120,14 +127,14 @@ func TestUserInfo_SetDefaultMethod_ShouldConfigureConfigDefault(t *testing.T) {
HasTOTP: false,
HasWebauthn: false,
},
availableMethods: []string{SecondFactorMethodWebauthn},
changed: true,
want: UserInfo{
Method: SecondFactorMethodWebauthn,
HasDuo: false,
HasTOTP: false,
HasWebauthn: false,
},
methods: []string{SecondFactorMethodWebauthn},
changed: true,
},
{
have: UserInfo{
@ -136,14 +143,14 @@ func TestUserInfo_SetDefaultMethod_ShouldConfigureConfigDefault(t *testing.T) {
HasTOTP: false,
HasWebauthn: false,
},
availableMethods: []string{SecondFactorMethodDuo},
changed: true,
want: UserInfo{
Method: SecondFactorMethodDuo,
HasDuo: false,
HasTOTP: false,
HasWebauthn: false,
},
methods: []string{SecondFactorMethodDuo},
changed: true,
},
{
have: UserInfo{
@ -152,14 +159,14 @@ func TestUserInfo_SetDefaultMethod_ShouldConfigureConfigDefault(t *testing.T) {
HasTOTP: true,
HasWebauthn: true,
},
availableMethods: []string{SecondFactorMethodTOTP, SecondFactorMethodWebauthn, SecondFactorMethodDuo},
want: UserInfo{
Method: SecondFactorMethodWebauthn,
HasDuo: false,
HasTOTP: true,
HasWebauthn: true,
},
methods: []string{SecondFactorMethodTOTP, SecondFactorMethodWebauthn, SecondFactorMethodDuo},
changed: false,
want: UserInfo{
Method: SecondFactorMethodWebauthn,
HasDuo: false,
HasTOTP: true,
HasWebauthn: true,
},
},
{
have: UserInfo{
@ -168,14 +175,14 @@ func TestUserInfo_SetDefaultMethod_ShouldConfigureConfigDefault(t *testing.T) {
HasTOTP: true,
HasWebauthn: true,
},
availableMethods: []string{SecondFactorMethodWebauthn, SecondFactorMethodDuo},
changed: true,
want: UserInfo{
Method: SecondFactorMethodWebauthn,
HasDuo: false,
HasTOTP: true,
HasWebauthn: true,
},
methods: []string{SecondFactorMethodWebauthn, SecondFactorMethodDuo},
changed: true,
},
{
have: UserInfo{
@ -184,14 +191,14 @@ func TestUserInfo_SetDefaultMethod_ShouldConfigureConfigDefault(t *testing.T) {
HasTOTP: true,
HasWebauthn: true,
},
availableMethods: []string{SecondFactorMethodDuo},
changed: true,
want: UserInfo{
Method: SecondFactorMethodDuo,
HasDuo: false,
HasTOTP: true,
HasWebauthn: true,
},
methods: []string{SecondFactorMethodDuo},
changed: true,
},
{
have: UserInfo{
@ -200,20 +207,104 @@ func TestUserInfo_SetDefaultMethod_ShouldConfigureConfigDefault(t *testing.T) {
HasTOTP: true,
HasWebauthn: true,
},
availableMethods: nil,
changed: false,
want: UserInfo{
Method: "",
HasDuo: false,
HasTOTP: true,
HasWebauthn: true,
},
methods: nil,
changed: false,
},
{
have: UserInfo{
Method: "",
HasDuo: false,
HasTOTP: false,
HasWebauthn: false,
},
want: UserInfo{
Method: SecondFactorMethodDuo,
HasDuo: false,
HasTOTP: false,
HasWebauthn: false,
},
methods: []string{SecondFactorMethodTOTP, SecondFactorMethodWebauthn, SecondFactorMethodDuo},
fallback: SecondFactorMethodDuo,
changed: true,
},
{
have: UserInfo{
Method: "",
HasDuo: false,
HasTOTP: false,
HasWebauthn: false,
},
want: UserInfo{
Method: SecondFactorMethodTOTP,
HasDuo: false,
HasTOTP: false,
HasWebauthn: false,
},
methods: []string{SecondFactorMethodTOTP, SecondFactorMethodWebauthn},
fallback: SecondFactorMethodDuo,
changed: true,
},
{
have: UserInfo{
Method: SecondFactorMethodTOTP,
HasDuo: true,
HasTOTP: false,
HasWebauthn: false,
},
want: UserInfo{
Method: SecondFactorMethodDuo,
HasDuo: true,
HasTOTP: false,
HasWebauthn: false,
},
methods: []string{SecondFactorMethodWebauthn, SecondFactorMethodDuo},
changed: true,
},
{
have: UserInfo{
Method: SecondFactorMethodTOTP,
HasDuo: false,
HasTOTP: false,
HasWebauthn: false,
},
want: UserInfo{
Method: SecondFactorMethodWebauthn,
HasDuo: false,
HasTOTP: false,
HasWebauthn: false,
},
methods: []string{SecondFactorMethodWebauthn, SecondFactorMethodDuo},
fallback: SecondFactorMethodWebauthn,
changed: true,
},
{
have: UserInfo{
Method: SecondFactorMethodWebauthn,
HasDuo: false,
HasTOTP: false,
HasWebauthn: false,
},
want: UserInfo{
Method: SecondFactorMethodDuo,
HasDuo: false,
HasTOTP: false,
HasWebauthn: false,
},
methods: []string{SecondFactorMethodTOTP, SecondFactorMethodDuo},
fallback: SecondFactorMethodDuo,
changed: true,
},
}
for i, tc := range testCases {
t.Run(testName(i, tc.have, tc.availableMethods), func(t *testing.T) {
changed := tc.have.SetDefaultPreferred2FAMethod(tc.availableMethods)
t.Run(testName(i, tc.have, tc.methods, tc.fallback), func(t *testing.T) {
changed := tc.have.SetDefaultPreferred2FAMethod(tc.methods, tc.fallback)
assert.Equal(t, tc.changed, changed)
assert.Equal(t, tc.want, tc.have)