feat(configuration): configurable default second factor method (#3081)
This allows configuring the default second factor method.pull/3145/head^2
parent
9bd10e6409
commit
e99fb7a08f
|
@ -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
|
||||
##
|
||||
|
|
|
@ -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
|
||||
```
|
|
@ -2,7 +2,7 @@
|
|||
layout: default
|
||||
title: Password Policy
|
||||
parent: Configuration
|
||||
nav_order: 17
|
||||
nav_order: 18
|
||||
---
|
||||
|
||||
# Password Policy
|
||||
|
|
|
@ -29,7 +29,7 @@ secrets can be loaded into the configuration if they end with one of the suffixe
|
|||
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 |
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
##
|
||||
|
|
|
@ -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"`
|
||||
|
|
|
@ -12,6 +12,7 @@ var Keys = []string{
|
|||
"certificates_directory",
|
||||
"jwt_secret",
|
||||
"default_redirection_url",
|
||||
"default_2fa_method",
|
||||
"log.level",
|
||||
"log.format",
|
||||
"log.file_path",
|
||||
|
|
|
@ -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))
|
||||
}
|
|
@ -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, "', '")))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
@ -53,6 +64,3 @@ func (i *UserInfo) SetDefaultPreferred2FAMethod(methods []string) (changed bool)
|
|||
i.Method = SecondFactorMethodDuo
|
||||
}
|
||||
}
|
||||
|
||||
return before != i.Method
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue