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.
|
## Note: this parameter is optional. If not provided, user won't be redirected upon successful authentication.
|
||||||
default_redirection_url: https://home.example.com/
|
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
|
## Server Configuration
|
||||||
##
|
##
|
||||||
|
|
|
@ -59,3 +59,28 @@ redirected to that URL. If not defined, the user is not redirected after authent
|
||||||
```yaml
|
```yaml
|
||||||
default_redirection_url: https://home.example.com:8080/
|
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
|
layout: default
|
||||||
title: Password Policy
|
title: Password Policy
|
||||||
parent: Configuration
|
parent: Configuration
|
||||||
nav_order: 17
|
nav_order: 18
|
||||||
---
|
---
|
||||||
|
|
||||||
# Password Policy
|
# 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.
|
other configuration using the environment but instead of loading a file the value of the environment variable is used.
|
||||||
|
|
||||||
| Configuration Key | Environment Variable |
|
| Configuration Key | Environment Variable |
|
||||||
|:-----------------------------------------------:|:------------------------------------------------------:|
|
|:-------------------------------------------------:|:--------------------------------------------------------:|
|
||||||
| tls_key | AUTHELIA_TLS_KEY_FILE |
|
| tls_key | AUTHELIA_TLS_KEY_FILE |
|
||||||
| jwt_secret | AUTHELIA_JWT_SECRET_FILE |
|
| jwt_secret | AUTHELIA_JWT_SECRET_FILE |
|
||||||
| duo_api.secret_key | AUTHELIA_DUO_API_SECRET_KEY_FILE |
|
| duo_api.secret_key | AUTHELIA_DUO_API_SECRET_KEY_FILE |
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
layout: default
|
layout: default
|
||||||
title: Webauthn
|
title: Webauthn
|
||||||
parent: Configuration
|
parent: Configuration
|
||||||
nav_order: 16
|
nav_order: 17
|
||||||
---
|
---
|
||||||
|
|
||||||
The Webauthn section has tunable options for the Webauthn implementation.
|
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.
|
## Note: this parameter is optional. If not provided, user won't be redirected upon successful authentication.
|
||||||
default_redirection_url: https://home.example.com/
|
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
|
## Server Configuration
|
||||||
##
|
##
|
||||||
|
|
|
@ -6,6 +6,7 @@ type Configuration struct {
|
||||||
CertificatesDirectory string `koanf:"certificates_directory"`
|
CertificatesDirectory string `koanf:"certificates_directory"`
|
||||||
JWTSecret string `koanf:"jwt_secret"`
|
JWTSecret string `koanf:"jwt_secret"`
|
||||||
DefaultRedirectionURL string `koanf:"default_redirection_url"`
|
DefaultRedirectionURL string `koanf:"default_redirection_url"`
|
||||||
|
Default2FAMethod string `koanf:"default_2fa_method"`
|
||||||
|
|
||||||
Log LogConfiguration `koanf:"log"`
|
Log LogConfiguration `koanf:"log"`
|
||||||
IdentityProviders IdentityProvidersConfiguration `koanf:"identity_providers"`
|
IdentityProviders IdentityProvidersConfiguration `koanf:"identity_providers"`
|
||||||
|
|
|
@ -12,6 +12,7 @@ var Keys = []string{
|
||||||
"certificates_directory",
|
"certificates_directory",
|
||||||
"jwt_secret",
|
"jwt_secret",
|
||||||
"default_redirection_url",
|
"default_redirection_url",
|
||||||
|
"default_2fa_method",
|
||||||
"log.level",
|
"log.level",
|
||||||
"log.format",
|
"log.format",
|
||||||
"log.file_path",
|
"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)
|
ValidateTheme(config, validator)
|
||||||
|
|
||||||
ValidateLog(config, validator)
|
ValidateLog(config, validator)
|
||||||
|
@ -65,3 +67,33 @@ func ValidateConfiguration(config *schema.Configuration, validator *schema.Struc
|
||||||
|
|
||||||
ValidatePasswordPolicy(&config.PasswordPolicy, validator)
|
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
|
package validator
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"runtime"
|
"runtime"
|
||||||
"testing"
|
"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")
|
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.
|
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'"
|
errFmtReplacedConfigurationKey = "invalid configuration key '%s' was replaced by '%s'"
|
||||||
|
|
||||||
errFmtLoggingLevelInvalid = "log: option 'level' must be one of '%s' but it is configured as '%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 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 validOIDCScopes = []string{oidc.ScopeOpenID, oidc.ScopeEmail, oidc.ScopeProfile, oidc.ScopeGroups, "offline_access"}
|
||||||
var validOIDCGrantTypes = []string{"implicit", "refresh_token", "authorization_code", "password", "client_credentials"}
|
var validOIDCGrantTypes = []string{"implicit", "refresh_token", "authorization_code", "password", "client_credentials"}
|
||||||
var validOIDCResponseModes = []string{"form_post", "query", "fragment"}
|
var validOIDCResponseModes = []string{"form_post", "query", "fragment"}
|
||||||
|
|
|
@ -10,7 +10,7 @@ import (
|
||||||
"github.com/authelia/authelia/v4/internal/utils"
|
"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) {
|
func ValidateIdentityProviders(config *schema.IdentityProvidersConfiguration, validator *schema.StructValidator) {
|
||||||
validateOIDC(config.OIDC, validator)
|
validateOIDC(config.OIDC, validator)
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,7 +39,7 @@ func UserInfoPOST(ctx *middlewares.AutheliaCtx) {
|
||||||
changed bool
|
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 {
|
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)
|
ctx.Error(fmt.Errorf("unable to save user two factor method: %v", err), messageOperationFailed)
|
||||||
return
|
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.
|
// 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 {
|
if len(methods) == 0 {
|
||||||
// No point attempting to change the method if no methods are available.
|
// No point attempting to change the method if no methods are available.
|
||||||
return false
|
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)
|
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 = ""
|
i.Method = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
if 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 {
|
switch {
|
||||||
case i.HasTOTP && totp:
|
case i.HasTOTP && totp:
|
||||||
i.Method = SecondFactorMethodTOTP
|
i.Method = SecondFactorMethodTOTP
|
||||||
|
@ -45,6 +54,8 @@ func (i *UserInfo) SetDefaultPreferred2FAMethod(methods []string) (changed bool)
|
||||||
i.Method = SecondFactorMethodWebauthn
|
i.Method = SecondFactorMethodWebauthn
|
||||||
case i.HasDuo && duo:
|
case i.HasDuo && duo:
|
||||||
i.Method = SecondFactorMethodDuo
|
i.Method = SecondFactorMethodDuo
|
||||||
|
case fallback != "" && utils.IsStringInSlice(fallback, methods):
|
||||||
|
i.Method = fallback
|
||||||
case totp:
|
case totp:
|
||||||
i.Method = SecondFactorMethodTOTP
|
i.Method = SecondFactorMethodTOTP
|
||||||
case webauthn:
|
case webauthn:
|
||||||
|
@ -53,6 +64,3 @@ func (i *UserInfo) SetDefaultPreferred2FAMethod(methods []string) (changed bool)
|
||||||
i.Method = SecondFactorMethodDuo
|
i.Method = SecondFactorMethodDuo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return before != i.Method
|
|
||||||
}
|
|
||||||
|
|
|
@ -8,10 +8,10 @@ import (
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestUserInfo_SetDefaultMethod_ShouldConfigureConfigDefault(t *testing.T) {
|
func TestUserInfo_SetDefaultMethod(t *testing.T) {
|
||||||
none := "none"
|
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
|
method := have.Method
|
||||||
|
|
||||||
if method == "" {
|
if method == "" {
|
||||||
|
@ -37,18 +37,25 @@ func TestUserInfo_SetDefaultMethod_ShouldConfigureConfigDefault(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
available := none
|
available := none
|
||||||
if len(availableMethods) != 0 {
|
if len(methods) != 0 {
|
||||||
available = strings.Join(availableMethods, " ")
|
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 {
|
testCases := []struct {
|
||||||
have UserInfo
|
have UserInfo
|
||||||
availableMethods []string
|
|
||||||
changed bool
|
|
||||||
want UserInfo
|
want UserInfo
|
||||||
|
|
||||||
|
methods []string
|
||||||
|
fallback string
|
||||||
|
|
||||||
|
changed bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
have: UserInfo{
|
have: UserInfo{
|
||||||
|
@ -57,14 +64,14 @@ func TestUserInfo_SetDefaultMethod_ShouldConfigureConfigDefault(t *testing.T) {
|
||||||
HasTOTP: true,
|
HasTOTP: true,
|
||||||
HasWebauthn: true,
|
HasWebauthn: true,
|
||||||
},
|
},
|
||||||
availableMethods: []string{SecondFactorMethodWebauthn, SecondFactorMethodDuo},
|
|
||||||
changed: true,
|
|
||||||
want: UserInfo{
|
want: UserInfo{
|
||||||
Method: SecondFactorMethodWebauthn,
|
Method: SecondFactorMethodWebauthn,
|
||||||
HasDuo: true,
|
HasDuo: true,
|
||||||
HasTOTP: true,
|
HasTOTP: true,
|
||||||
HasWebauthn: true,
|
HasWebauthn: true,
|
||||||
},
|
},
|
||||||
|
methods: []string{SecondFactorMethodWebauthn, SecondFactorMethodDuo},
|
||||||
|
changed: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
have: UserInfo{
|
have: UserInfo{
|
||||||
|
@ -72,14 +79,14 @@ func TestUserInfo_SetDefaultMethod_ShouldConfigureConfigDefault(t *testing.T) {
|
||||||
HasTOTP: true,
|
HasTOTP: true,
|
||||||
HasWebauthn: true,
|
HasWebauthn: true,
|
||||||
},
|
},
|
||||||
availableMethods: []string{SecondFactorMethodTOTP, SecondFactorMethodWebauthn, SecondFactorMethodDuo},
|
|
||||||
changed: true,
|
|
||||||
want: UserInfo{
|
want: UserInfo{
|
||||||
Method: SecondFactorMethodTOTP,
|
Method: SecondFactorMethodTOTP,
|
||||||
HasDuo: true,
|
HasDuo: true,
|
||||||
HasTOTP: true,
|
HasTOTP: true,
|
||||||
HasWebauthn: true,
|
HasWebauthn: true,
|
||||||
},
|
},
|
||||||
|
methods: []string{SecondFactorMethodTOTP, SecondFactorMethodWebauthn, SecondFactorMethodDuo},
|
||||||
|
changed: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
have: UserInfo{
|
have: UserInfo{
|
||||||
|
@ -88,14 +95,14 @@ func TestUserInfo_SetDefaultMethod_ShouldConfigureConfigDefault(t *testing.T) {
|
||||||
HasTOTP: false,
|
HasTOTP: false,
|
||||||
HasWebauthn: false,
|
HasWebauthn: false,
|
||||||
},
|
},
|
||||||
availableMethods: []string{SecondFactorMethodTOTP},
|
|
||||||
changed: true,
|
|
||||||
want: UserInfo{
|
want: UserInfo{
|
||||||
Method: SecondFactorMethodTOTP,
|
Method: SecondFactorMethodTOTP,
|
||||||
HasDuo: true,
|
HasDuo: true,
|
||||||
HasTOTP: false,
|
HasTOTP: false,
|
||||||
HasWebauthn: false,
|
HasWebauthn: false,
|
||||||
},
|
},
|
||||||
|
methods: []string{SecondFactorMethodTOTP},
|
||||||
|
changed: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
have: UserInfo{
|
have: UserInfo{
|
||||||
|
@ -104,14 +111,14 @@ func TestUserInfo_SetDefaultMethod_ShouldConfigureConfigDefault(t *testing.T) {
|
||||||
HasTOTP: false,
|
HasTOTP: false,
|
||||||
HasWebauthn: false,
|
HasWebauthn: false,
|
||||||
},
|
},
|
||||||
availableMethods: []string{SecondFactorMethodTOTP},
|
|
||||||
changed: true,
|
|
||||||
want: UserInfo{
|
want: UserInfo{
|
||||||
Method: SecondFactorMethodTOTP,
|
Method: SecondFactorMethodTOTP,
|
||||||
HasDuo: false,
|
HasDuo: false,
|
||||||
HasTOTP: false,
|
HasTOTP: false,
|
||||||
HasWebauthn: false,
|
HasWebauthn: false,
|
||||||
},
|
},
|
||||||
|
methods: []string{SecondFactorMethodTOTP},
|
||||||
|
changed: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
have: UserInfo{
|
have: UserInfo{
|
||||||
|
@ -120,14 +127,14 @@ func TestUserInfo_SetDefaultMethod_ShouldConfigureConfigDefault(t *testing.T) {
|
||||||
HasTOTP: false,
|
HasTOTP: false,
|
||||||
HasWebauthn: false,
|
HasWebauthn: false,
|
||||||
},
|
},
|
||||||
availableMethods: []string{SecondFactorMethodWebauthn},
|
|
||||||
changed: true,
|
|
||||||
want: UserInfo{
|
want: UserInfo{
|
||||||
Method: SecondFactorMethodWebauthn,
|
Method: SecondFactorMethodWebauthn,
|
||||||
HasDuo: false,
|
HasDuo: false,
|
||||||
HasTOTP: false,
|
HasTOTP: false,
|
||||||
HasWebauthn: false,
|
HasWebauthn: false,
|
||||||
},
|
},
|
||||||
|
methods: []string{SecondFactorMethodWebauthn},
|
||||||
|
changed: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
have: UserInfo{
|
have: UserInfo{
|
||||||
|
@ -136,14 +143,14 @@ func TestUserInfo_SetDefaultMethod_ShouldConfigureConfigDefault(t *testing.T) {
|
||||||
HasTOTP: false,
|
HasTOTP: false,
|
||||||
HasWebauthn: false,
|
HasWebauthn: false,
|
||||||
},
|
},
|
||||||
availableMethods: []string{SecondFactorMethodDuo},
|
|
||||||
changed: true,
|
|
||||||
want: UserInfo{
|
want: UserInfo{
|
||||||
Method: SecondFactorMethodDuo,
|
Method: SecondFactorMethodDuo,
|
||||||
HasDuo: false,
|
HasDuo: false,
|
||||||
HasTOTP: false,
|
HasTOTP: false,
|
||||||
HasWebauthn: false,
|
HasWebauthn: false,
|
||||||
},
|
},
|
||||||
|
methods: []string{SecondFactorMethodDuo},
|
||||||
|
changed: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
have: UserInfo{
|
have: UserInfo{
|
||||||
|
@ -152,14 +159,14 @@ func TestUserInfo_SetDefaultMethod_ShouldConfigureConfigDefault(t *testing.T) {
|
||||||
HasTOTP: true,
|
HasTOTP: true,
|
||||||
HasWebauthn: 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,
|
changed: false,
|
||||||
want: UserInfo{
|
|
||||||
Method: SecondFactorMethodWebauthn,
|
|
||||||
HasDuo: false,
|
|
||||||
HasTOTP: true,
|
|
||||||
HasWebauthn: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
have: UserInfo{
|
have: UserInfo{
|
||||||
|
@ -168,14 +175,14 @@ func TestUserInfo_SetDefaultMethod_ShouldConfigureConfigDefault(t *testing.T) {
|
||||||
HasTOTP: true,
|
HasTOTP: true,
|
||||||
HasWebauthn: true,
|
HasWebauthn: true,
|
||||||
},
|
},
|
||||||
availableMethods: []string{SecondFactorMethodWebauthn, SecondFactorMethodDuo},
|
|
||||||
changed: true,
|
|
||||||
want: UserInfo{
|
want: UserInfo{
|
||||||
Method: SecondFactorMethodWebauthn,
|
Method: SecondFactorMethodWebauthn,
|
||||||
HasDuo: false,
|
HasDuo: false,
|
||||||
HasTOTP: true,
|
HasTOTP: true,
|
||||||
HasWebauthn: true,
|
HasWebauthn: true,
|
||||||
},
|
},
|
||||||
|
methods: []string{SecondFactorMethodWebauthn, SecondFactorMethodDuo},
|
||||||
|
changed: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
have: UserInfo{
|
have: UserInfo{
|
||||||
|
@ -184,14 +191,14 @@ func TestUserInfo_SetDefaultMethod_ShouldConfigureConfigDefault(t *testing.T) {
|
||||||
HasTOTP: true,
|
HasTOTP: true,
|
||||||
HasWebauthn: true,
|
HasWebauthn: true,
|
||||||
},
|
},
|
||||||
availableMethods: []string{SecondFactorMethodDuo},
|
|
||||||
changed: true,
|
|
||||||
want: UserInfo{
|
want: UserInfo{
|
||||||
Method: SecondFactorMethodDuo,
|
Method: SecondFactorMethodDuo,
|
||||||
HasDuo: false,
|
HasDuo: false,
|
||||||
HasTOTP: true,
|
HasTOTP: true,
|
||||||
HasWebauthn: true,
|
HasWebauthn: true,
|
||||||
},
|
},
|
||||||
|
methods: []string{SecondFactorMethodDuo},
|
||||||
|
changed: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
have: UserInfo{
|
have: UserInfo{
|
||||||
|
@ -200,20 +207,104 @@ func TestUserInfo_SetDefaultMethod_ShouldConfigureConfigDefault(t *testing.T) {
|
||||||
HasTOTP: true,
|
HasTOTP: true,
|
||||||
HasWebauthn: true,
|
HasWebauthn: true,
|
||||||
},
|
},
|
||||||
availableMethods: nil,
|
|
||||||
changed: false,
|
|
||||||
want: UserInfo{
|
want: UserInfo{
|
||||||
Method: "",
|
Method: "",
|
||||||
HasDuo: false,
|
HasDuo: false,
|
||||||
HasTOTP: true,
|
HasTOTP: true,
|
||||||
HasWebauthn: 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 {
|
for i, tc := range testCases {
|
||||||
t.Run(testName(i, tc.have, tc.availableMethods), func(t *testing.T) {
|
t.Run(testName(i, tc.have, tc.methods, tc.fallback), func(t *testing.T) {
|
||||||
changed := tc.have.SetDefaultPreferred2FAMethod(tc.availableMethods)
|
changed := tc.have.SetDefaultPreferred2FAMethod(tc.methods, tc.fallback)
|
||||||
|
|
||||||
assert.Equal(t, tc.changed, changed)
|
assert.Equal(t, tc.changed, changed)
|
||||||
assert.Equal(t, tc.want, tc.have)
|
assert.Equal(t, tc.want, tc.have)
|
||||||
|
|
Loading…
Reference in New Issue