diff --git a/config.template.yml b/config.template.yml index 1292632a..042aeecd 100644 --- a/config.template.yml +++ b/config.template.yml @@ -191,6 +191,13 @@ authentication_backend: ## Disable both the HTML element and the API for reset password functionality. disable_reset_password: false + ## Password Reset Options. + password_reset: + + ## External reset password url that redirects the user to an external reset portal. This disables the internal reset + ## functionality. + custom_url: "" + ## The amount of time to wait before we refresh data from the authentication backend. Uses duration notation. ## To disable this feature set it to 'disable', this will slightly reduce security because for Authelia, users will ## always belong to groups they belonged to at the time of login even if they have been removed from them in LDAP. diff --git a/docs/configuration/authentication/index.md b/docs/configuration/authentication/index.md index e781aea6..be120444 100644 --- a/docs/configuration/authentication/index.md +++ b/docs/configuration/authentication/index.md @@ -18,6 +18,8 @@ There are two ways to store the users along with their password: ```yaml authentication_backend: disable_reset_password: false + password_reset: + custom_url: "" file: {} ldap: {} ``` @@ -36,6 +38,21 @@ required: no This setting controls if users can reset their password from the web frontend or not. +### password_reset + +#### custom_url +
+type: string +{: .label .label-config .label-purple } +default: "" +{: .label .label-config .label-blue } +required: no +{: .label .label-config .label-green } +
+ +The custom password reset URL. This replaces the inbuilt password reset functionality and disables the endpoints if +this is configured to anything other than nothing or an empty string. + ### file The [file](file.md) authentication provider. diff --git a/internal/configuration/config.template.yml b/internal/configuration/config.template.yml index 1292632a..042aeecd 100644 --- a/internal/configuration/config.template.yml +++ b/internal/configuration/config.template.yml @@ -191,6 +191,13 @@ authentication_backend: ## Disable both the HTML element and the API for reset password functionality. disable_reset_password: false + ## Password Reset Options. + password_reset: + + ## External reset password url that redirects the user to an external reset portal. This disables the internal reset + ## functionality. + custom_url: "" + ## The amount of time to wait before we refresh data from the authentication backend. Uses duration notation. ## To disable this feature set it to 'disable', this will slightly reduce security because for Authelia, users will ## always belong to groups they belonged to at the time of login even if they have been removed from them in LDAP. diff --git a/internal/configuration/schema/authentication.go b/internal/configuration/schema/authentication.go index c97a5744..07e6f56d 100644 --- a/internal/configuration/schema/authentication.go +++ b/internal/configuration/schema/authentication.go @@ -1,6 +1,9 @@ package schema -import "time" +import ( + "net/url" + "time" +) // LDAPAuthenticationBackendConfiguration represents the configuration related to LDAP server. type LDAPAuthenticationBackendConfiguration struct { @@ -45,10 +48,18 @@ type PasswordConfiguration struct { // AuthenticationBackendConfiguration represents the configuration related to the authentication backend. type AuthenticationBackendConfiguration struct { - DisableResetPassword bool `koanf:"disable_reset_password"` - RefreshInterval string `koanf:"refresh_interval"` - LDAP *LDAPAuthenticationBackendConfiguration `koanf:"ldap"` - File *FileAuthenticationBackendConfiguration `koanf:"file"` + LDAP *LDAPAuthenticationBackendConfiguration `koanf:"ldap"` + File *FileAuthenticationBackendConfiguration `koanf:"file"` + + PasswordReset PasswordResetAuthenticationBackendConfiguration `koanf:"password_reset"` + + DisableResetPassword bool `koanf:"disable_reset_password"` + RefreshInterval string `koanf:"refresh_interval"` +} + +// PasswordResetAuthenticationBackendConfiguration represents the configuration related to password reset functionality. +type PasswordResetAuthenticationBackendConfiguration struct { + CustomURL url.URL `koanf:"custom_url"` } // DefaultPasswordConfiguration represents the default configuration related to Argon2id hashing. diff --git a/internal/configuration/validator/access_control_test.go b/internal/configuration/validator/access_control_test.go index 3ce26fed..5af97a0a 100644 --- a/internal/configuration/validator/access_control_test.go +++ b/internal/configuration/validator/access_control_test.go @@ -32,8 +32,8 @@ func (suite *AccessControl) SetupTest() { func (suite *AccessControl) TestShouldValidateCompleteConfiguration() { ValidateAccessControl(suite.config, suite.validator) - suite.Assert().False(suite.validator.HasWarnings()) - suite.Assert().False(suite.validator.HasErrors()) + suite.Assert().Len(suite.validator.Warnings(), 0) + suite.Assert().Len(suite.validator.Errors(), 0) } func (suite *AccessControl) TestShouldValidateEitherDomainsOrDomainsRegex() { @@ -55,7 +55,7 @@ func (suite *AccessControl) TestShouldValidateEitherDomainsOrDomainsRegex() { ValidateRules(suite.config, suite.validator) - suite.Assert().False(suite.validator.HasWarnings()) + suite.Assert().Len(suite.validator.Warnings(), 0) suite.Require().Len(suite.validator.Errors(), 1) assert.EqualError(suite.T(), suite.validator.Errors()[0], "access control: rule #3: rule is invalid: must have the option 'domain' or 'domain_regex' configured") @@ -66,7 +66,7 @@ func (suite *AccessControl) TestShouldRaiseErrorInvalidDefaultPolicy() { ValidateAccessControl(suite.config, suite.validator) - suite.Assert().False(suite.validator.HasWarnings()) + suite.Assert().Len(suite.validator.Warnings(), 0) suite.Require().Len(suite.validator.Errors(), 1) suite.Assert().EqualError(suite.validator.Errors()[0], "access control: option 'default_policy' must be one of 'bypass', 'one_factor', 'two_factor', 'deny' but it is configured as 'invalid'") @@ -82,7 +82,7 @@ func (suite *AccessControl) TestShouldRaiseErrorInvalidNetworkGroupNetwork() { ValidateAccessControl(suite.config, suite.validator) - suite.Assert().False(suite.validator.HasWarnings()) + suite.Assert().Len(suite.validator.Warnings(), 0) suite.Require().Len(suite.validator.Errors(), 1) suite.Assert().EqualError(suite.validator.Errors()[0], "access control: networks: network group 'internal' is invalid: the network 'abc.def.ghi.jkl' is not a valid IP or CIDR notation") @@ -93,7 +93,7 @@ func (suite *AccessControl) TestShouldRaiseErrorWithNoRulesDefined() { ValidateRules(suite.config, suite.validator) - suite.Assert().False(suite.validator.HasWarnings()) + suite.Assert().Len(suite.validator.Warnings(), 0) suite.Require().Len(suite.validator.Errors(), 1) suite.Assert().EqualError(suite.validator.Errors()[0], "access control: 'default_policy' option 'deny' is invalid: when no rules are specified it must be 'two_factor' or 'one_factor'") @@ -106,7 +106,7 @@ func (suite *AccessControl) TestShouldRaiseWarningWithNoRulesDefined() { ValidateRules(suite.config, suite.validator) - suite.Assert().False(suite.validator.HasErrors()) + suite.Assert().Len(suite.validator.Errors(), 0) suite.Require().Len(suite.validator.Warnings(), 1) suite.Assert().EqualError(suite.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") @@ -122,7 +122,7 @@ func (suite *AccessControl) TestShouldRaiseErrorsWithEmptyRules() { ValidateRules(suite.config, suite.validator) - suite.Assert().False(suite.validator.HasWarnings()) + suite.Assert().Len(suite.validator.Warnings(), 0) suite.Require().Len(suite.validator.Errors(), 4) suite.Assert().EqualError(suite.validator.Errors()[0], "access control: rule #1: rule is invalid: must have the option 'domain' or 'domain_regex' configured") @@ -141,7 +141,7 @@ func (suite *AccessControl) TestShouldRaiseErrorInvalidPolicy() { ValidateRules(suite.config, suite.validator) - suite.Assert().False(suite.validator.HasWarnings()) + suite.Assert().Len(suite.validator.Warnings(), 0) suite.Require().Len(suite.validator.Errors(), 1) suite.Assert().EqualError(suite.validator.Errors()[0], "access control: rule #1 (domain 'public.example.com'): rule 'policy' option 'invalid' is invalid: must be one of 'deny', 'two_factor', 'one_factor' or 'bypass'") @@ -158,7 +158,7 @@ func (suite *AccessControl) TestShouldRaiseErrorInvalidNetwork() { ValidateRules(suite.config, suite.validator) - suite.Assert().False(suite.validator.HasWarnings()) + suite.Assert().Len(suite.validator.Warnings(), 0) suite.Require().Len(suite.validator.Errors(), 1) suite.Assert().EqualError(suite.validator.Errors()[0], "access control: rule #1 (domain 'public.example.com'): the network 'abc.def.ghi.jkl/32' is not a valid Group Name, IP, or CIDR notation") @@ -175,7 +175,7 @@ func (suite *AccessControl) TestShouldRaiseErrorInvalidMethod() { ValidateRules(suite.config, suite.validator) - suite.Assert().False(suite.validator.HasWarnings()) + suite.Assert().Len(suite.validator.Warnings(), 0) suite.Require().Len(suite.validator.Errors(), 1) suite.Assert().EqualError(suite.validator.Errors()[0], "access control: rule #1 (domain 'public.example.com'): 'methods' option 'HOP' is invalid: must be one of 'GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'TRACE', 'CONNECT', 'OPTIONS', 'COPY', 'LOCK', 'MKCOL', 'MOVE', 'PROPFIND', 'PROPPATCH', 'UNLOCK'") diff --git a/internal/configuration/validator/authentication.go b/internal/configuration/validator/authentication.go index b0aae841..c6af9459 100644 --- a/internal/configuration/validator/authentication.go +++ b/internal/configuration/validator/authentication.go @@ -33,6 +33,15 @@ func ValidateAuthenticationBackend(config *schema.AuthenticationBackendConfigura validator.Push(fmt.Errorf(errFmtAuthBackendRefreshInterval, config.RefreshInterval, err)) } } + + if config.PasswordReset.CustomURL.String() != "" { + switch config.PasswordReset.CustomURL.Scheme { + case schemeHTTP, schemeHTTPS: + config.DisableResetPassword = false + default: + validator.Push(fmt.Errorf(errFmtAuthBackendPasswordResetCustomURLScheme, config.PasswordReset.CustomURL.String(), config.PasswordReset.CustomURL.Scheme)) + } + } } // validateFileAuthenticationBackend validates and updates the file authentication backend configuration. diff --git a/internal/configuration/validator/authentication_test.go b/internal/configuration/validator/authentication_test.go index c1d1aa2a..a575a231 100644 --- a/internal/configuration/validator/authentication_test.go +++ b/internal/configuration/validator/authentication_test.go @@ -1,6 +1,7 @@ package validator import ( + "net/url" "testing" "time" @@ -58,8 +59,8 @@ func (suite *FileBasedAuthenticationBackend) SetupTest() { func (suite *FileBasedAuthenticationBackend) TestShouldValidateCompleteConfiguration() { ValidateAuthenticationBackend(&suite.config, suite.validator) - suite.Assert().False(suite.validator.HasWarnings()) - suite.Assert().False(suite.validator.HasErrors()) + suite.Assert().Len(suite.validator.Warnings(), 0) + suite.Assert().Len(suite.validator.Errors(), 0) } func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenNoPathProvided() { @@ -67,7 +68,7 @@ func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenNoPathProvi ValidateAuthenticationBackend(&suite.config, suite.validator) - suite.Assert().False(suite.validator.HasWarnings()) + suite.Assert().Len(suite.validator.Warnings(), 0) suite.Require().Len(suite.validator.Errors(), 1) suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: file: option 'path' is required") @@ -79,7 +80,7 @@ func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenMemoryNotMo ValidateAuthenticationBackend(&suite.config, suite.validator) - suite.Assert().False(suite.validator.HasWarnings()) + suite.Assert().Len(suite.validator.Warnings(), 0) suite.Require().Len(suite.validator.Errors(), 1) suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: file: password: option 'memory' must at least be parallelism multiplied by 8 when using algorithm 'argon2id' with parallelism 2 it should be at least 16 but it is configured as '8'") @@ -97,7 +98,7 @@ func (suite *FileBasedAuthenticationBackend) TestShouldSetDefaultConfigurationWh ValidateAuthenticationBackend(&suite.config, suite.validator) - suite.Assert().False(suite.validator.HasWarnings()) + suite.Assert().Len(suite.validator.Warnings(), 0) suite.Assert().Len(suite.validator.Errors(), 0) suite.Assert().Equal(schema.DefaultPasswordConfiguration.KeyLength, suite.config.File.Password.KeyLength) @@ -115,7 +116,7 @@ func (suite *FileBasedAuthenticationBackend) TestShouldSetDefaultConfigurationWh ValidateAuthenticationBackend(&suite.config, suite.validator) - suite.Assert().False(suite.validator.HasWarnings()) + suite.Assert().Len(suite.validator.Warnings(), 0) suite.Assert().Len(suite.validator.Errors(), 0) suite.Assert().Equal(schema.DefaultPasswordSHA512Configuration.KeyLength, suite.config.File.Password.KeyLength) @@ -130,7 +131,7 @@ func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenKeyLengthTo ValidateAuthenticationBackend(&suite.config, suite.validator) - suite.Assert().False(suite.validator.HasWarnings()) + suite.Assert().Len(suite.validator.Warnings(), 0) suite.Require().Len(suite.validator.Errors(), 1) suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: file: password: option 'key_length' must be 16 or more when using algorithm 'argon2id' but it is configured as '1'") @@ -141,7 +142,7 @@ func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenSaltLengthT ValidateAuthenticationBackend(&suite.config, suite.validator) - suite.Assert().False(suite.validator.HasWarnings()) + suite.Assert().Len(suite.validator.Warnings(), 0) suite.Require().Len(suite.validator.Errors(), 1) suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: file: password: option 'salt_length' must be 2 or more but it is configured a '-1'") @@ -152,7 +153,7 @@ func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenBadAlgorith ValidateAuthenticationBackend(&suite.config, suite.validator) - suite.Assert().False(suite.validator.HasWarnings()) + suite.Assert().Len(suite.validator.Warnings(), 0) suite.Require().Len(suite.validator.Errors(), 1) suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: file: password: option 'algorithm' must be either 'argon2id' or 'sha512' but it is configured as 'bogus'") @@ -163,7 +164,7 @@ func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenIterationsT ValidateAuthenticationBackend(&suite.config, suite.validator) - suite.Assert().False(suite.validator.HasWarnings()) + suite.Assert().Len(suite.validator.Warnings(), 0) suite.Require().Len(suite.validator.Errors(), 1) suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: file: password: option 'iterations' must be 1 or more but it is configured as '-1'") @@ -174,7 +175,7 @@ func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenParallelism ValidateAuthenticationBackend(&suite.config, suite.validator) - suite.Assert().False(suite.validator.HasWarnings()) + suite.Assert().Len(suite.validator.Warnings(), 0) suite.Require().Len(suite.validator.Errors(), 1) suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: file: password: option 'parallelism' must be 1 or more when using algorithm 'argon2id' but it is configured as '-1'") @@ -189,8 +190,8 @@ func (suite *FileBasedAuthenticationBackend) TestShouldSetDefaultValues() { ValidateAuthenticationBackend(&suite.config, suite.validator) - suite.Assert().False(suite.validator.HasWarnings()) - suite.Assert().False(suite.validator.HasErrors()) + suite.Assert().Len(suite.validator.Warnings(), 0) + suite.Assert().Len(suite.validator.Errors(), 0) suite.Assert().Equal(schema.DefaultPasswordConfiguration.Algorithm, suite.config.File.Password.Algorithm) suite.Assert().Equal(schema.DefaultPasswordConfiguration.Iterations, suite.config.File.Password.Iterations) @@ -226,8 +227,47 @@ func (suite *LDAPAuthenticationBackendSuite) SetupTest() { func (suite *LDAPAuthenticationBackendSuite) TestShouldValidateCompleteConfiguration() { ValidateAuthenticationBackend(&suite.config, suite.validator) - suite.Assert().False(suite.validator.HasWarnings()) - suite.Assert().False(suite.validator.HasErrors()) + suite.Assert().Len(suite.validator.Warnings(), 0) + suite.Assert().Len(suite.validator.Errors(), 0) +} + +func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenResetURLIsInvalid() { + suite.config.PasswordReset.CustomURL = url.URL{Scheme: "ldap", Host: "google.com"} + suite.config.DisableResetPassword = true + + suite.Assert().True(suite.config.DisableResetPassword) + + ValidateAuthenticationBackend(&suite.config, suite.validator) + + suite.Assert().Len(suite.validator.Warnings(), 0) + suite.Require().Len(suite.validator.Errors(), 1) + + suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: password_reset: option 'custom_url' is configured to 'ldap://google.com' which has the scheme 'ldap' but the scheme must be either 'http' or 'https'") + + suite.Assert().True(suite.config.DisableResetPassword) +} + +func (suite *FileBasedAuthenticationBackend) TestShouldNotRaiseErrorWhenResetURLIsValid() { + suite.config.PasswordReset.CustomURL = url.URL{Scheme: "https", Host: "google.com"} + + ValidateAuthenticationBackend(&suite.config, suite.validator) + + suite.Assert().Len(suite.validator.Warnings(), 0) + suite.Assert().Len(suite.validator.Errors(), 0) +} + +func (suite *FileBasedAuthenticationBackend) TestShouldConfigureDisableResetPasswordWhenCustomURL() { + suite.config.PasswordReset.CustomURL = url.URL{Scheme: "https", Host: "google.com"} + suite.config.DisableResetPassword = true + + suite.Assert().True(suite.config.DisableResetPassword) + + ValidateAuthenticationBackend(&suite.config, suite.validator) + + suite.Assert().Len(suite.validator.Warnings(), 0) + suite.Assert().Len(suite.validator.Errors(), 0) + + suite.Assert().False(suite.config.DisableResetPassword) } func (suite *LDAPAuthenticationBackendSuite) TestShouldValidateDefaultImplementationAndUsernameAttribute() { @@ -238,8 +278,8 @@ func (suite *LDAPAuthenticationBackendSuite) TestShouldValidateDefaultImplementa suite.Assert().Equal(schema.LDAPImplementationCustom, suite.config.LDAP.Implementation) suite.Assert().Equal(suite.config.LDAP.UsernameAttribute, schema.DefaultLDAPAuthenticationBackendConfiguration.UsernameAttribute) - suite.Assert().False(suite.validator.HasWarnings()) - suite.Assert().False(suite.validator.HasErrors()) + suite.Assert().Len(suite.validator.Warnings(), 0) + suite.Assert().Len(suite.validator.Errors(), 0) } func (suite *LDAPAuthenticationBackendSuite) TestShouldRaiseErrorWhenImplementationIsInvalidMSAD() { @@ -247,7 +287,7 @@ func (suite *LDAPAuthenticationBackendSuite) TestShouldRaiseErrorWhenImplementat ValidateAuthenticationBackend(&suite.config, suite.validator) - suite.Assert().False(suite.validator.HasWarnings()) + suite.Assert().Len(suite.validator.Warnings(), 0) suite.Require().Len(suite.validator.Errors(), 1) suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: ldap: option 'implementation' is configured as 'masd' but must be one of the following values: 'custom', 'activedirectory'") @@ -257,7 +297,7 @@ func (suite *LDAPAuthenticationBackendSuite) TestShouldRaiseErrorWhenURLNotProvi suite.config.LDAP.URL = "" ValidateAuthenticationBackend(&suite.config, suite.validator) - suite.Assert().False(suite.validator.HasWarnings()) + suite.Assert().Len(suite.validator.Warnings(), 0) suite.Require().Len(suite.validator.Errors(), 1) suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: ldap: option 'url' is required") @@ -268,7 +308,7 @@ func (suite *LDAPAuthenticationBackendSuite) TestShouldRaiseErrorWhenUserNotProv ValidateAuthenticationBackend(&suite.config, suite.validator) - suite.Assert().False(suite.validator.HasWarnings()) + suite.Assert().Len(suite.validator.Warnings(), 0) suite.Require().Len(suite.validator.Errors(), 1) suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: ldap: option 'user' is required") @@ -279,7 +319,7 @@ func (suite *LDAPAuthenticationBackendSuite) TestShouldRaiseErrorWhenPasswordNot ValidateAuthenticationBackend(&suite.config, suite.validator) - suite.Assert().False(suite.validator.HasWarnings()) + suite.Assert().Len(suite.validator.Warnings(), 0) suite.Require().Len(suite.validator.Errors(), 1) suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: ldap: option 'password' is required") @@ -290,7 +330,7 @@ func (suite *LDAPAuthenticationBackendSuite) TestShouldRaiseErrorWhenBaseDNNotPr ValidateAuthenticationBackend(&suite.config, suite.validator) - suite.Assert().False(suite.validator.HasWarnings()) + suite.Assert().Len(suite.validator.Warnings(), 0) suite.Assert().Len(suite.validator.Errors(), 1) suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: ldap: option 'base_dn' is required") @@ -301,7 +341,7 @@ func (suite *LDAPAuthenticationBackendSuite) TestShouldRaiseOnEmptyGroupsFilter( ValidateAuthenticationBackend(&suite.config, suite.validator) - suite.Assert().False(suite.validator.HasWarnings()) + suite.Assert().Len(suite.validator.Warnings(), 0) suite.Require().Len(suite.validator.Errors(), 1) suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: ldap: option 'groups_filter' is required") @@ -312,7 +352,7 @@ func (suite *LDAPAuthenticationBackendSuite) TestShouldRaiseOnEmptyUsersFilter() ValidateAuthenticationBackend(&suite.config, suite.validator) - suite.Assert().False(suite.validator.HasWarnings()) + suite.Assert().Len(suite.validator.Warnings(), 0) suite.Require().Len(suite.validator.Errors(), 1) suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: ldap: option 'users_filter' is required") @@ -323,8 +363,8 @@ func (suite *LDAPAuthenticationBackendSuite) TestShouldNotRaiseOnEmptyUsernameAt ValidateAuthenticationBackend(&suite.config, suite.validator) - suite.Assert().False(suite.validator.HasWarnings()) - suite.Assert().False(suite.validator.HasErrors()) + suite.Assert().Len(suite.validator.Warnings(), 0) + suite.Assert().Len(suite.validator.Errors(), 0) } func (suite *LDAPAuthenticationBackendSuite) TestShouldRaiseOnBadRefreshInterval() { @@ -332,7 +372,7 @@ func (suite *LDAPAuthenticationBackendSuite) TestShouldRaiseOnBadRefreshInterval ValidateAuthenticationBackend(&suite.config, suite.validator) - suite.Assert().False(suite.validator.HasWarnings()) + suite.Assert().Len(suite.validator.Warnings(), 0) suite.Require().Len(suite.validator.Errors(), 1) suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: option 'refresh_interval' is configured to 'blah' but it must be either a duration notation or one of 'disable', or 'always': could not parse 'blah' as a duration") @@ -341,8 +381,8 @@ func (suite *LDAPAuthenticationBackendSuite) TestShouldRaiseOnBadRefreshInterval func (suite *LDAPAuthenticationBackendSuite) TestShouldSetDefaultImplementation() { ValidateAuthenticationBackend(&suite.config, suite.validator) - suite.Assert().False(suite.validator.HasWarnings()) - suite.Assert().False(suite.validator.HasErrors()) + suite.Assert().Len(suite.validator.Warnings(), 0) + suite.Assert().Len(suite.validator.Errors(), 0) suite.Assert().Equal(schema.LDAPImplementationCustom, suite.config.LDAP.Implementation) } @@ -353,7 +393,7 @@ func (suite *LDAPAuthenticationBackendSuite) TestShouldRaiseErrorOnBadFilterPlac ValidateAuthenticationBackend(&suite.config, suite.validator) - suite.Assert().False(suite.validator.HasWarnings()) + suite.Assert().Len(suite.validator.Warnings(), 0) suite.Assert().True(suite.validator.HasErrors()) suite.Require().Len(suite.validator.Errors(), 4) @@ -366,8 +406,8 @@ func (suite *LDAPAuthenticationBackendSuite) TestShouldRaiseErrorOnBadFilterPlac func (suite *LDAPAuthenticationBackendSuite) TestShouldSetDefaultGroupNameAttribute() { ValidateAuthenticationBackend(&suite.config, suite.validator) - suite.Assert().False(suite.validator.HasWarnings()) - suite.Assert().False(suite.validator.HasErrors()) + suite.Assert().Len(suite.validator.Warnings(), 0) + suite.Assert().Len(suite.validator.Errors(), 0) suite.Assert().Equal("cn", suite.config.LDAP.GroupNameAttribute) } @@ -375,8 +415,8 @@ func (suite *LDAPAuthenticationBackendSuite) TestShouldSetDefaultGroupNameAttrib func (suite *LDAPAuthenticationBackendSuite) TestShouldSetDefaultMailAttribute() { ValidateAuthenticationBackend(&suite.config, suite.validator) - suite.Assert().False(suite.validator.HasWarnings()) - suite.Assert().False(suite.validator.HasErrors()) + suite.Assert().Len(suite.validator.Warnings(), 0) + suite.Assert().Len(suite.validator.Errors(), 0) suite.Assert().Equal("mail", suite.config.LDAP.MailAttribute) } @@ -384,8 +424,8 @@ func (suite *LDAPAuthenticationBackendSuite) TestShouldSetDefaultMailAttribute() func (suite *LDAPAuthenticationBackendSuite) TestShouldSetDefaultDisplayNameAttribute() { ValidateAuthenticationBackend(&suite.config, suite.validator) - suite.Assert().False(suite.validator.HasWarnings()) - suite.Assert().False(suite.validator.HasErrors()) + suite.Assert().Len(suite.validator.Warnings(), 0) + suite.Assert().Len(suite.validator.Errors(), 0) suite.Assert().Equal("displayName", suite.config.LDAP.DisplayNameAttribute) } @@ -393,8 +433,8 @@ func (suite *LDAPAuthenticationBackendSuite) TestShouldSetDefaultDisplayNameAttr func (suite *LDAPAuthenticationBackendSuite) TestShouldSetDefaultRefreshInterval() { ValidateAuthenticationBackend(&suite.config, suite.validator) - suite.Assert().False(suite.validator.HasWarnings()) - suite.Assert().False(suite.validator.HasErrors()) + suite.Assert().Len(suite.validator.Warnings(), 0) + suite.Assert().Len(suite.validator.Errors(), 0) suite.Assert().Equal("5m", suite.config.RefreshInterval) } @@ -404,7 +444,7 @@ func (suite *LDAPAuthenticationBackendSuite) TestShouldRaiseWhenUsersFilterDoesN ValidateAuthenticationBackend(&suite.config, suite.validator) - suite.Assert().False(suite.validator.HasWarnings()) + suite.Assert().Len(suite.validator.Warnings(), 0) suite.Require().Len(suite.validator.Errors(), 1) suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: ldap: option 'users_filter' must contain enclosing parenthesis: '{username_attribute}={input}' should probably be '({username_attribute}={input})'") @@ -415,7 +455,7 @@ func (suite *LDAPAuthenticationBackendSuite) TestShouldRaiseWhenGroupsFilterDoes ValidateAuthenticationBackend(&suite.config, suite.validator) - suite.Assert().False(suite.validator.HasWarnings()) + suite.Assert().Len(suite.validator.Warnings(), 0) suite.Require().Len(suite.validator.Errors(), 1) suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: ldap: option 'groups_filter' must contain enclosing parenthesis: 'cn={input}' should probably be '(cn={input})'") @@ -425,7 +465,7 @@ func (suite *LDAPAuthenticationBackendSuite) TestShouldRaiseWhenUsersFilterDoesN suite.config.LDAP.UsersFilter = "(&({mail_attribute}={input})(objectClass=person))" ValidateAuthenticationBackend(&suite.config, suite.validator) - suite.Assert().False(suite.validator.HasWarnings()) + suite.Assert().Len(suite.validator.Warnings(), 0) suite.Require().Len(suite.validator.Errors(), 1) suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: ldap: option 'users_filter' must contain the placeholder '{username_attribute}' but it is required") @@ -436,7 +476,7 @@ func (suite *LDAPAuthenticationBackendSuite) TestShouldHelpDetectNoInputPlacehol ValidateAuthenticationBackend(&suite.config, suite.validator) - suite.Assert().False(suite.validator.HasWarnings()) + suite.Assert().Len(suite.validator.Warnings(), 0) suite.Require().Len(suite.validator.Errors(), 1) suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: ldap: option 'users_filter' must contain the placeholder '{input}' but it is required") @@ -447,8 +487,8 @@ func (suite *LDAPAuthenticationBackendSuite) TestShouldSetDefaultTLSMinimumVersi ValidateAuthenticationBackend(&suite.config, suite.validator) - suite.Assert().False(suite.validator.HasWarnings()) - suite.Assert().False(suite.validator.HasErrors()) + suite.Assert().Len(suite.validator.Warnings(), 0) + suite.Assert().Len(suite.validator.Errors(), 0) suite.Assert().Equal(schema.DefaultLDAPAuthenticationBackendConfiguration.TLS.MinimumVersion, suite.config.LDAP.TLS.MinimumVersion) } @@ -460,7 +500,7 @@ func (suite *LDAPAuthenticationBackendSuite) TestShouldNotAllowInvalidTLSValue() ValidateAuthenticationBackend(&suite.config, suite.validator) - suite.Assert().False(suite.validator.HasWarnings()) + suite.Assert().Len(suite.validator.Warnings(), 0) suite.Require().Len(suite.validator.Errors(), 1) suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: ldap: tls: option 'minimum_tls_version' is invalid: SSL2.0: supplied tls version isn't supported") @@ -491,8 +531,8 @@ func (suite *ActiveDirectoryAuthenticationBackendSuite) SetupTest() { func (suite *ActiveDirectoryAuthenticationBackendSuite) TestShouldSetActiveDirectoryDefaults() { ValidateAuthenticationBackend(&suite.config, suite.validator) - suite.Assert().False(suite.validator.HasWarnings()) - suite.Assert().False(suite.validator.HasErrors()) + suite.Assert().Len(suite.validator.Warnings(), 0) + suite.Assert().Len(suite.validator.Errors(), 0) suite.Assert().Equal( schema.DefaultLDAPAuthenticationBackendConfiguration.Timeout, diff --git a/internal/configuration/validator/const.go b/internal/configuration/validator/const.go index 90019dc1..75a79486 100644 --- a/internal/configuration/validator/const.go +++ b/internal/configuration/validator/const.go @@ -69,6 +69,8 @@ const ( "backend is configured" errFmtAuthBackendRefreshInterval = "authentication_backend: option 'refresh_interval' is configured to '%s' but " + "it must be either a duration notation or one of 'disable', or 'always': %w" + errFmtAuthBackendPasswordResetCustomURLScheme = "authentication_backend: password_reset: option 'custom_url' is" + + " configured to '%s' which has the scheme '%s' but the scheme must be either 'http' or 'https'" errFmtFileAuthBackendPathNotConfigured = "authentication_backend: file: option 'path' is required" errFmtFileAuthBackendPasswordSaltLength = "authentication_backend: file: password: option 'salt_length' " + @@ -424,6 +426,7 @@ var ValidKeys = []string{ // Authentication Backend Keys. "authentication_backend.disable_reset_password", + "authentication_backend.password_reset.custom_url", "authentication_backend.refresh_interval", // LDAP Authentication Backend Keys. diff --git a/internal/configuration/validator/notifier_test.go b/internal/configuration/validator/notifier_test.go index 7e7eb8d9..3316e063 100644 --- a/internal/configuration/validator/notifier_test.go +++ b/internal/configuration/validator/notifier_test.go @@ -34,14 +34,14 @@ func (suite *NotifierSuite) SetupTest() { func (suite *NotifierSuite) TestShouldEnsureAtLeastSMTPOrFilesystemIsProvided() { ValidateNotifier(&suite.config, suite.validator) - suite.Assert().False(suite.validator.HasWarnings()) - suite.Assert().False(suite.validator.HasErrors()) + suite.Assert().Len(suite.validator.Warnings(), 0) + suite.Assert().Len(suite.validator.Errors(), 0) suite.config.SMTP = nil ValidateNotifier(&suite.config, suite.validator) - suite.Assert().False(suite.validator.HasWarnings()) + suite.Assert().Len(suite.validator.Warnings(), 0) suite.Require().True(suite.validator.HasErrors()) suite.Assert().Len(suite.validator.Errors(), 1) @@ -52,7 +52,7 @@ func (suite *NotifierSuite) TestShouldEnsureAtLeastSMTPOrFilesystemIsProvided() func (suite *NotifierSuite) TestShouldEnsureEitherSMTPOrFilesystemIsProvided() { ValidateNotifier(&suite.config, suite.validator) - suite.Assert().False(suite.validator.HasErrors()) + suite.Assert().Len(suite.validator.Errors(), 0) suite.config.FileSystem = &schema.FileSystemNotifierConfiguration{ Filename: "test", @@ -60,7 +60,7 @@ func (suite *NotifierSuite) TestShouldEnsureEitherSMTPOrFilesystemIsProvided() { ValidateNotifier(&suite.config, suite.validator) - suite.Assert().False(suite.validator.HasWarnings()) + suite.Assert().Len(suite.validator.Warnings(), 0) suite.Require().True(suite.validator.HasErrors()) suite.Assert().Len(suite.validator.Errors(), 1) @@ -74,8 +74,8 @@ func (suite *NotifierSuite) TestShouldEnsureEitherSMTPOrFilesystemIsProvided() { func (suite *NotifierSuite) TestSMTPShouldSetTLSDefaults() { ValidateNotifier(&suite.config, suite.validator) - suite.Assert().False(suite.validator.HasWarnings()) - suite.Assert().False(suite.validator.HasErrors()) + suite.Assert().Len(suite.validator.Warnings(), 0) + suite.Assert().Len(suite.validator.Errors(), 0) suite.Assert().Equal("example.com", suite.config.SMTP.TLS.ServerName) suite.Assert().Equal("TLS1.2", suite.config.SMTP.TLS.MinimumVersion) @@ -90,8 +90,8 @@ func (suite *NotifierSuite) TestSMTPShouldDefaultTLSServerNameToHost() { ValidateNotifier(&suite.config, suite.validator) - suite.Assert().False(suite.validator.HasWarnings()) - suite.Assert().False(suite.validator.HasErrors()) + suite.Assert().Len(suite.validator.Warnings(), 0) + suite.Assert().Len(suite.validator.Errors(), 0) suite.Assert().Equal("google.com", suite.config.SMTP.TLS.ServerName) suite.Assert().Equal("TLS1.1", suite.config.SMTP.TLS.MinimumVersion) @@ -102,15 +102,15 @@ func (suite *NotifierSuite) TestSMTPShouldEnsureHostAndPortAreProvided() { suite.config.FileSystem = nil ValidateNotifier(&suite.config, suite.validator) - suite.Assert().False(suite.validator.HasWarnings()) - suite.Assert().False(suite.validator.HasErrors()) + suite.Assert().Len(suite.validator.Warnings(), 0) + suite.Assert().Len(suite.validator.Errors(), 0) suite.config.SMTP.Host = "" suite.config.SMTP.Port = 0 ValidateNotifier(&suite.config, suite.validator) - suite.Assert().False(suite.validator.HasWarnings()) + suite.Assert().Len(suite.validator.Warnings(), 0) suite.Assert().True(suite.validator.HasErrors()) errors := suite.validator.Errors() @@ -126,7 +126,7 @@ func (suite *NotifierSuite) TestSMTPShouldEnsureSenderIsProvided() { ValidateNotifier(&suite.config, suite.validator) - suite.Assert().False(suite.validator.HasWarnings()) + suite.Assert().Len(suite.validator.Warnings(), 0) suite.Require().True(suite.validator.HasErrors()) suite.Assert().Len(suite.validator.Errors(), 1) @@ -144,14 +144,14 @@ func (suite *NotifierSuite) TestFileShouldEnsureFilenameIsProvided() { } ValidateNotifier(&suite.config, suite.validator) - suite.Assert().False(suite.validator.HasWarnings()) - suite.Assert().False(suite.validator.HasErrors()) + suite.Assert().Len(suite.validator.Warnings(), 0) + suite.Assert().Len(suite.validator.Errors(), 0) suite.config.FileSystem.Filename = "" ValidateNotifier(&suite.config, suite.validator) - suite.Assert().False(suite.validator.HasWarnings()) + suite.Assert().Len(suite.validator.Warnings(), 0) suite.Require().True(suite.validator.HasErrors()) suite.Assert().Len(suite.validator.Errors(), 1) diff --git a/internal/configuration/validator/theme_test.go b/internal/configuration/validator/theme_test.go index df279cb8..1b29d0e3 100644 --- a/internal/configuration/validator/theme_test.go +++ b/internal/configuration/validator/theme_test.go @@ -24,8 +24,8 @@ func (suite *Theme) SetupTest() { func (suite *Theme) TestShouldValidateCompleteConfiguration() { ValidateTheme(suite.config, suite.validator) - suite.Assert().False(suite.validator.HasWarnings()) - suite.Assert().False(suite.validator.HasErrors()) + suite.Assert().Len(suite.validator.Warnings(), 0) + suite.Assert().Len(suite.validator.Errors(), 0) } func (suite *Theme) TestShouldRaiseErrorWhenInvalidThemeProvided() { @@ -33,7 +33,7 @@ func (suite *Theme) TestShouldRaiseErrorWhenInvalidThemeProvided() { ValidateTheme(suite.config, suite.validator) - suite.Assert().False(suite.validator.HasWarnings()) + suite.Assert().Len(suite.validator.Warnings(), 0) suite.Require().Len(suite.validator.Errors(), 1) suite.Assert().EqualError(suite.validator.Errors()[0], "option 'theme' must be one of 'light', 'dark', 'grey', 'auto' but it is configured as 'invalid'") diff --git a/internal/server/server.go b/internal/server/server.go index 6c5b19ce..b52c8700 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -24,6 +24,8 @@ func registerRoutes(configuration schema.Configuration, providers middlewares.Pr rememberMe := strconv.FormatBool(configuration.Session.RememberMeDuration != schema.RememberMeDisabled) resetPassword := strconv.FormatBool(!configuration.AuthenticationBackend.DisableResetPassword) + resetPasswordCustomURL := configuration.AuthenticationBackend.PasswordReset.CustomURL.String() + duoSelfEnrollment := f if configuration.DuoAPI != nil { duoSelfEnrollment = strconv.FormatBool(configuration.DuoAPI.EnableSelfEnrollment) @@ -34,9 +36,9 @@ func registerRoutes(configuration schema.Configuration, providers middlewares.Pr https := configuration.Server.TLS.Key != "" && configuration.Server.TLS.Certificate != "" - serveIndexHandler := ServeTemplatedFile(embeddedAssets, indexFile, configuration.Server.AssetPath, duoSelfEnrollment, rememberMe, resetPassword, configuration.Session.Name, configuration.Theme, https) - serveSwaggerHandler := ServeTemplatedFile(swaggerAssets, indexFile, configuration.Server.AssetPath, duoSelfEnrollment, rememberMe, resetPassword, configuration.Session.Name, configuration.Theme, https) - serveSwaggerAPIHandler := ServeTemplatedFile(swaggerAssets, apiFile, configuration.Server.AssetPath, duoSelfEnrollment, rememberMe, resetPassword, configuration.Session.Name, configuration.Theme, https) + serveIndexHandler := ServeTemplatedFile(embeddedAssets, indexFile, configuration.Server.AssetPath, duoSelfEnrollment, rememberMe, resetPassword, resetPasswordCustomURL, configuration.Session.Name, configuration.Theme, https) + serveSwaggerHandler := ServeTemplatedFile(swaggerAssets, indexFile, configuration.Server.AssetPath, duoSelfEnrollment, rememberMe, resetPassword, resetPasswordCustomURL, configuration.Session.Name, configuration.Theme, https) + serveSwaggerAPIHandler := ServeTemplatedFile(swaggerAssets, apiFile, configuration.Server.AssetPath, duoSelfEnrollment, rememberMe, resetPassword, resetPasswordCustomURL, configuration.Session.Name, configuration.Theme, https) r := router.New() r.GET("/", autheliaMiddleware(serveIndexHandler)) @@ -77,7 +79,8 @@ func registerRoutes(configuration schema.Configuration, providers middlewares.Pr r.POST("/api/logout", autheliaMiddleware(handlers.LogoutPost)) // Only register endpoints if forgot password is not disabled. - if !configuration.AuthenticationBackend.DisableResetPassword { + if !configuration.AuthenticationBackend.DisableResetPassword && + configuration.AuthenticationBackend.PasswordReset.CustomURL.String() == "" { // Password reset related endpoints. r.POST("/api/reset-password/identity/start", autheliaMiddleware( handlers.ResetPasswordIdentityStart)) diff --git a/internal/server/template.go b/internal/server/template.go index 9c8d4a5f..b71041bc 100644 --- a/internal/server/template.go +++ b/internal/server/template.go @@ -16,7 +16,7 @@ import ( // ServeTemplatedFile serves a templated version of a specified file, // this is utilised to pass information between the backend and frontend // and generate a nonce to support a restrictive CSP while using material-ui. -func ServeTemplatedFile(publicDir, file, assetPath, duoSelfEnrollment, rememberMe, resetPassword, session, theme string, https bool) middlewares.RequestHandler { +func ServeTemplatedFile(publicDir, file, assetPath, duoSelfEnrollment, rememberMe, resetPassword, resetPasswordCustomURL, session, theme string, https bool) middlewares.RequestHandler { logger := logging.Logger() a, err := assets.Open(publicDir + file) @@ -81,7 +81,7 @@ func ServeTemplatedFile(publicDir, file, assetPath, duoSelfEnrollment, rememberM ctx.Response.Header.Add("Content-Security-Policy", fmt.Sprintf(cspDefaultTemplate, nonce)) } - err := tmpl.Execute(ctx.Response.BodyWriter(), struct{ Base, BaseURL, CSPNonce, DuoSelfEnrollment, LogoOverride, RememberMe, ResetPassword, Session, Theme string }{Base: base, BaseURL: baseURL, CSPNonce: nonce, DuoSelfEnrollment: duoSelfEnrollment, LogoOverride: logoOverride, RememberMe: rememberMe, ResetPassword: resetPassword, Session: session, Theme: theme}) + err := tmpl.Execute(ctx.Response.BodyWriter(), struct{ Base, BaseURL, CSPNonce, DuoSelfEnrollment, LogoOverride, RememberMe, ResetPassword, ResetPasswordCustomURL, Session, Theme string }{Base: base, BaseURL: baseURL, CSPNonce: nonce, DuoSelfEnrollment: duoSelfEnrollment, LogoOverride: logoOverride, RememberMe: rememberMe, ResetPassword: resetPassword, ResetPasswordCustomURL: resetPasswordCustomURL, Session: session, Theme: theme}) if err != nil { ctx.RequestCtx.Error("an error occurred", 503) logger.Errorf("Unable to execute template: %v", err) diff --git a/web/.env.development b/web/.env.development index 6402411e..cc016d96 100644 --- a/web/.env.development +++ b/web/.env.development @@ -4,4 +4,5 @@ VITE_PUBLIC_URL="" VITE_DUO_SELF_ENROLLMENT=true VITE_REMEMBER_ME=true VITE_RESET_PASSWORD=true +VITE_RESET_PASSWORD_CUSTOM_URL="" VITE_THEME=light \ No newline at end of file diff --git a/web/.env.production b/web/.env.production index 788e3f4f..b146b314 100644 --- a/web/.env.production +++ b/web/.env.production @@ -3,4 +3,5 @@ VITE_PUBLIC_URL={{.Base}} VITE_DUO_SELF_ENROLLMENT={{.DuoSelfEnrollment}} VITE_REMEMBER_ME={{.RememberMe}} VITE_RESET_PASSWORD={{.ResetPassword}} +VITE_RESET_PASSWORD_CUSTOM_URL={{.ResetPasswordCustomURL}} VITE_THEME={{.Theme}} \ No newline at end of file diff --git a/web/index.html b/web/index.html index 31f7b90d..70970870 100644 --- a/web/index.html +++ b/web/index.html @@ -1,8 +1,7 @@ - - + @@ -13,10 +12,17 @@ Login - Authelia - +
- - \ No newline at end of file + diff --git a/web/src/App.tsx b/web/src/App.tsx index 635669be..a1b181b8 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -18,7 +18,13 @@ import NotificationsContext from "@hooks/NotificationsContext"; import { Notification } from "@models/Notifications"; import * as themes from "@themes/index"; import { getBasePath } from "@utils/BasePath"; -import { getDuoSelfEnrollment, getRememberMe, getResetPassword, getTheme } from "@utils/Configuration"; +import { + getDuoSelfEnrollment, + getRememberMe, + getResetPassword, + getResetPasswordCustomURL, + getTheme, +} from "@utils/Configuration"; import RegisterOneTimePassword from "@views/DeviceRegistration/RegisterOneTimePassword"; import RegisterWebauthn from "@views/DeviceRegistration/RegisterWebauthn"; import BaseLoadingPage from "@views/LoadingPage/BaseLoadingPage"; @@ -80,6 +86,7 @@ const App: React.FC = () => { duoSelfEnrollment={getDuoSelfEnrollment()} rememberMe={getRememberMe()} resetPassword={getResetPassword()} + resetPasswordCustomURL={getResetPasswordCustomURL()} /> } /> diff --git a/web/src/setupTests.js b/web/src/setupTests.js index 0f20e1c5..1c5931ad 100644 --- a/web/src/setupTests.js +++ b/web/src/setupTests.js @@ -4,4 +4,5 @@ document.body.setAttribute("data-basepath", ""); document.body.setAttribute("data-duoselfenrollment", "true"); document.body.setAttribute("data-rememberme", "true"); document.body.setAttribute("data-resetpassword", "true"); +document.body.setAttribute("data-resetpasswordcustomurl", ""); document.body.setAttribute("data-theme", "light"); diff --git a/web/src/utils/Configuration.ts b/web/src/utils/Configuration.ts index 4b6518b5..25ff419d 100644 --- a/web/src/utils/Configuration.ts +++ b/web/src/utils/Configuration.ts @@ -23,6 +23,10 @@ export function getResetPassword() { return getEmbeddedVariable("resetpassword") === "true"; } +export function getResetPasswordCustomURL() { + return getEmbeddedVariable("resetpasswordcustomurl"); +} + export function getTheme() { return getEmbeddedVariable("theme"); } diff --git a/web/src/views/LoginPortal/FirstFactor/FirstFactorForm.tsx b/web/src/views/LoginPortal/FirstFactor/FirstFactorForm.tsx index cf0d02ca..329c830e 100644 --- a/web/src/views/LoginPortal/FirstFactor/FirstFactorForm.tsx +++ b/web/src/views/LoginPortal/FirstFactor/FirstFactorForm.tsx @@ -16,7 +16,9 @@ import { postFirstFactor } from "@services/FirstFactor"; export interface Props { disabled: boolean; rememberMe: boolean; + resetPassword: boolean; + resetPasswordCustomURL: string; onAuthenticationStart: () => void; onAuthenticationFailure: () => void; @@ -76,7 +78,13 @@ const FirstFactorForm = function (props: Props) { }; const handleResetPasswordClick = () => { - navigate(ResetPasswordStep1Route); + if (props.resetPassword) { + if (props.resetPasswordCustomURL !== "") { + window.open(props.resetPasswordCustomURL); + } else { + navigate(ResetPasswordStep1Route); + } + } }; return ( diff --git a/web/src/views/LoginPortal/LoginPortal.tsx b/web/src/views/LoginPortal/LoginPortal.tsx index ea507c74..ffc512ff 100644 --- a/web/src/views/LoginPortal/LoginPortal.tsx +++ b/web/src/views/LoginPortal/LoginPortal.tsx @@ -28,7 +28,9 @@ import SecondFactorForm from "@views/LoginPortal/SecondFactor/SecondFactorForm"; export interface Props { duoSelfEnrollment: boolean; rememberMe: boolean; + resetPassword: boolean; + resetPasswordCustomURL: string; } const RedirectionErrorMessage = @@ -175,6 +177,7 @@ const LoginPortal = function (props: Props) { disabled={firstFactorDisabled} rememberMe={props.rememberMe} resetPassword={props.resetPassword} + resetPasswordCustomURL={props.resetPasswordCustomURL} onAuthenticationStart={() => setFirstFactorDisabled(true)} onAuthenticationFailure={() => setFirstFactorDisabled(false)} onAuthenticationSuccess={handleAuthSuccess}