Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(web): custom reset password link #2854

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions config.template.yml
Expand Up @@ -160,6 +160,12 @@ authentication_backend:
## Disable both the HTML element and the API for reset password functionality.
disable_reset_password: false

## Enable the use of external reset password link.
enable_external_reset_password: false

## External reset password url that redirects the user to an external reset portal.
external_reset_password_url: ""
james-d-elliott marked this conversation as resolved.
Show resolved Hide resolved

## 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.
Expand Down
26 changes: 26 additions & 0 deletions docs/configuration/authentication/index.md
Expand Up @@ -18,6 +18,8 @@ There are two ways to store the users along with their password:
```yaml
authentication_backend:
disable_reset_password: false
enable_external_reset_password: false
external_reset_password_url: ""
james-d-elliott marked this conversation as resolved.
Show resolved Hide resolved
file: {}
ldap: {}
```
Expand All @@ -36,6 +38,30 @@ required: no

This setting controls if users can reset their password from the web frontend or not.

### enable_external_reset_password
<div markdown="1">
type: boolean
{: .label .label-config .label-purple }
default: false
{: .label .label-config .label-blue }
required: no
{: .label .label-config .label-green }
</div>

This setting controls if users can reset their password from an external reset password portal.

### external_reset_password_url
<div markdown="1">
type: string
{: .label .label-config .label-purple }
default: ""
{: .label .label-config .label-blue }
required: no (yes if enable_external_reset_password is true)
{: .label .label-config .label-green }
</div>

The custom reset password URL, which redirects users to the custom reset password portal.
james-d-elliott marked this conversation as resolved.
Show resolved Hide resolved

### file

The [file](file.md) authentication provider.
Expand Down
6 changes: 6 additions & 0 deletions internal/configuration/config.template.yml
Expand Up @@ -160,6 +160,12 @@ authentication_backend:
## Disable both the HTML element and the API for reset password functionality.
disable_reset_password: false

## Enable the use of external reset password link.
enable_external_reset_password: false

## External reset password url that redirects the user to an external reset portal.
external_reset_password_url: ""

james-d-elliott marked this conversation as resolved.
Show resolved Hide resolved
## 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.
Expand Down
10 changes: 6 additions & 4 deletions internal/configuration/schema/authentication.go
Expand Up @@ -45,10 +45,12 @@ 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"`
DisableResetPassword bool `koanf:"disable_reset_password"`
EnableExternalResetPassword bool `koanf:"enable_external_reset_password"`
ExternalResetPasswordURL string `koanf:"external_reset_password_url"`
RefreshInterval string `koanf:"refresh_interval"`
LDAP *LDAPAuthenticationBackendConfiguration `koanf:"ldap"`
File *FileAuthenticationBackendConfiguration `koanf:"file"`
}
james-d-elliott marked this conversation as resolved.
Show resolved Hide resolved

// DefaultPasswordConfiguration represents the default configuration related to Argon2id hashing.
Expand Down
11 changes: 11 additions & 0 deletions internal/configuration/validator/authentication.go
Expand Up @@ -34,6 +34,17 @@ func ValidateAuthenticationBackend(configuration *schema.AuthenticationBackendCo
validator.Push(fmt.Errorf("Auth Backend `refresh_interval` is configured to '%s' but it must be either a duration notation or one of 'disable', or 'always'. Error from parser: %s", configuration.RefreshInterval, err))
}
}

if configuration.EnableExternalResetPassword && !configuration.DisableResetPassword {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [golangci] reported by reviewdog 🐶
undeclared name: configuration (typecheck)

validator.Push(errors.New("You cannot enable both `internal reset password` and `external reset password` processes in `authentication_backend`"))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [golangci] reported by reviewdog 🐶
undeclared name: errors (typecheck)

}

if configuration.EnableExternalResetPassword {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [golangci] reported by reviewdog 🐶
undeclared name: configuration (typecheck)

err := utils.IsStringAbsURL(configuration.ExternalResetPasswordURL)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [golangci] reported by reviewdog 🐶
undeclared name: configuration (typecheck)

if err != nil {
validator.Push(fmt.Errorf("Provided reset url is invalid. Error from parser: %s", err))
}
}
james-d-elliott marked this conversation as resolved.
Show resolved Hide resolved
}

// validateFileAuthenticationBackend validates and updates the file authentication backend configuration.
Expand Down
13 changes: 13 additions & 0 deletions internal/configuration/validator/authentication_test.go
Expand Up @@ -230,6 +230,19 @@ func (suite *LDAPAuthenticationBackendSuite) TestShouldValidateCompleteConfigura
suite.Assert().False(suite.validator.HasErrors())
}

func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenResetURLIsInvalid() {
suite.configuration.EnableExternalResetPassword = true
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [golangci] reported by reviewdog 🐶
suite.configuration undefined (type *FileBasedAuthenticationBackend has no field or method configuration) (typecheck)

suite.configuration.DisableResetPassword = true
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [golangci] reported by reviewdog 🐶
suite.configuration undefined (type *FileBasedAuthenticationBackend has no field or method configuration) (typecheck)

suite.configuration.ExternalResetPasswordURL = "example.com" //nolint:goconst
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [golangci] reported by reviewdog 🐶
suite.configuration undefined (type *FileBasedAuthenticationBackend has no field or method configuration) (typecheck)


ValidateAuthenticationBackend(&suite.configuration, suite.validator)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [golangci] reported by reviewdog 🐶
suite.configuration undefined (type *FileBasedAuthenticationBackend has no field or method configuration) (typecheck)


suite.Assert().False(suite.validator.HasWarnings())
suite.Require().Len(suite.validator.Errors(), 1)

suite.Assert().EqualError(suite.validator.Errors()[0], "Provided reset url is invalid. Error from parser: the url 'example.com' is not absolute because it doesn't start with a scheme like 'http://' or 'https://'")
}

func (suite *LDAPAuthenticationBackendSuite) TestShouldValidateDefaultImplementationAndUsernameAttribute() {
suite.configuration.LDAP.Implementation = ""
suite.configuration.LDAP.UsernameAttribute = ""
Expand Down
2 changes: 2 additions & 0 deletions internal/configuration/validator/const.go
Expand Up @@ -282,6 +282,8 @@ var ValidKeys = []string{

// Authentication Backend Keys.
"authentication_backend.disable_reset_password",
"authentication_backend.enable_external_reset_password",
"authentication_backend.external_reset_password_url",
"authentication_backend.refresh_interval",

// LDAP Authentication Backend Keys.
Expand Down
12 changes: 9 additions & 3 deletions internal/server/server.go
Expand Up @@ -30,6 +30,12 @@ func registerRoutes(configuration schema.Configuration, providers middlewares.Pr
autheliaMiddleware := middlewares.AutheliaMiddleware(configuration, providers)
rememberMe := strconv.FormatBool(configuration.Session.RememberMeDuration != "0")
resetPassword := strconv.FormatBool(!configuration.AuthenticationBackend.DisableResetPassword)
externalResetPassword := strconv.FormatBool(configuration.AuthenticationBackend.EnableExternalResetPassword)

externalResetURL := ""
if configuration.AuthenticationBackend.EnableExternalResetPassword {
externalResetURL = configuration.AuthenticationBackend.ExternalResetPasswordURL
}
james-d-elliott marked this conversation as resolved.
Show resolved Hide resolved

duoSelfEnrollment := f
if configuration.DuoAPI != nil {
Expand All @@ -41,9 +47,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, externalResetPassword, externalResetURL, configuration.Session.Name, configuration.Theme, https)
serveSwaggerHandler := ServeTemplatedFile(swaggerAssets, indexFile, configuration.Server.AssetPath, duoSelfEnrollment, rememberMe, resetPassword, externalResetPassword, externalResetURL, configuration.Session.Name, configuration.Theme, https)
serveSwaggerAPIHandler := ServeTemplatedFile(swaggerAssets, apiFile, configuration.Server.AssetPath, duoSelfEnrollment, rememberMe, resetPassword, externalResetPassword, externalResetURL, configuration.Session.Name, configuration.Theme, https)
james-d-elliott marked this conversation as resolved.
Show resolved Hide resolved

r := router.New()
r.GET("/", autheliaMiddleware(serveIndexHandler))
Expand Down
4 changes: 2 additions & 2 deletions internal/server/template.go
Expand Up @@ -15,7 +15,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, externalResetPassword, externalResetURL, session, theme string, https bool) middlewares.RequestHandler {
james-d-elliott marked this conversation as resolved.
Show resolved Hide resolved
logger := logging.Logger()

a, err := assets.Open(publicDir + file)
Expand Down Expand Up @@ -78,7 +78,7 @@ func ServeTemplatedFile(publicDir, file, assetPath, duoSelfEnrollment, rememberM
ctx.Response.Header.Add("Content-Security-Policy", fmt.Sprintf("default-src 'self' ; object-src 'none'; style-src 'self' 'nonce-%s'", 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, ExternalResetPassword, ExternalResetURL, Session, Theme string }{Base: base, BaseURL: baseURL, CSPNonce: nonce, DuoSelfEnrollment: duoSelfEnrollment, LogoOverride: logoOverride, RememberMe: rememberMe, ResetPassword: resetPassword, ExternalResetPassword: externalResetPassword, ExternalResetURL: externalResetURL, Session: session, Theme: theme})
james-d-elliott marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
ctx.RequestCtx.Error("an error occurred", 503)
logger.Errorf("Unable to execute template: %v", err)
Expand Down
2 changes: 2 additions & 0 deletions web/.env.development
Expand Up @@ -4,4 +4,6 @@ VITE_PUBLIC_URL=""
VITE_DUO_SELF_ENROLLMENT=true
VITE_REMEMBER_ME=true
VITE_RESET_PASSWORD=true
VITE_EXTERNAL_RESET_PASSWORD=false
VITE_EXTERNAL_RESET_URL=""
james-d-elliott marked this conversation as resolved.
Show resolved Hide resolved
VITE_THEME=light
2 changes: 2 additions & 0 deletions web/.env.production
Expand Up @@ -3,4 +3,6 @@ VITE_PUBLIC_URL={{.Base}}
VITE_DUO_SELF_ENROLLMENT={{.DuoSelfEnrollment}}
VITE_REMEMBER_ME={{.RememberMe}}
VITE_RESET_PASSWORD={{.ResetPassword}}
VITE_EXTERNAL_RESET_PASSWORD={{.ExternalResetPassword}}
VITE_EXTERNAL_RESET_URL={{.ExternalResetURL}}
james-d-elliott marked this conversation as resolved.
Show resolved Hide resolved
VITE_THEME={{.Theme}}
45 changes: 26 additions & 19 deletions web/index.html
@@ -1,22 +1,29 @@
<!DOCTYPE html>
<html lang="en">
<head>
<base href="{{.BaseURL}}" />
<meta property="csp-nonce" content="{{.CSPNonce}}" />
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="Authelia login portal for your apps" />
<link rel="manifest" href="/manifest.json" />
<link rel="icon" href="/favicon.ico" />
<title>Login - Authelia</title>
</head>

<head>
<base href="{{.BaseURL}}">
<meta property="csp-nonce" content="{{.CSPNonce}}" />
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="Authelia login portal for your apps" />
<link rel="manifest" href="/manifest.json" />
<link rel="icon" href="/favicon.ico" />
<title>Login - Authelia</title>
</head>

<body data-basepath="%VITE_PUBLIC_URL%" data-duoselfenrollment="%VITE_DUO_SELF_ENROLLMENT%" data-logooverride="%VITE_LOGO_OVERRIDE%" data-rememberme="%VITE_REMEMBER_ME%" data-resetpassword="%VITE_RESET_PASSWORD%" data-theme="%VITE_THEME%">
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>

</html>
<body
data-basepath="%VITE_PUBLIC_URL%"
data-duoselfenrollment="%VITE_DUO_SELF_ENROLLMENT%"
data-logooverride="%VITE_LOGO_OVERRIDE%"
data-rememberme="%VITE_REMEMBER_ME%"
data-resetpassword="%VITE_RESET_PASSWORD%"
data-externalresetpassword="%VITE_EXTERNAL_RESET_PASSWORD%"
data-externalreseturl="%VITE_EXTERNAL_RESET_URL%"
james-d-elliott marked this conversation as resolved.
Show resolved Hide resolved
data-theme="%VITE_THEME%"
>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>
11 changes: 10 additions & 1 deletion web/src/App.tsx
Expand Up @@ -18,7 +18,14 @@ 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,
getExternalResetPassword,
getExternalResetUrl,
getTheme,
} from "@utils/Configuration";
import RegisterOneTimePassword from "@views/DeviceRegistration/RegisterOneTimePassword";
import RegisterSecurityKey from "@views/DeviceRegistration/RegisterSecurityKey";
import ConsentView from "@views/LoginPortal/ConsentView/ConsentView";
Expand Down Expand Up @@ -78,6 +85,8 @@ const App: React.FC = () => {
duoSelfEnrollment={getDuoSelfEnrollment()}
rememberMe={getRememberMe()}
resetPassword={getResetPassword()}
externalResetPassword={getExternalResetPassword()}
externalResetUrl={getExternalResetUrl()}
/>
}
/>
Expand Down
2 changes: 2 additions & 0 deletions web/src/setupTests.js
Expand Up @@ -4,4 +4,6 @@ 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-externalresetpassword", "false");
document.body.setAttribute("data-externalreseturl", "");
document.body.setAttribute("data-theme", "light");
8 changes: 8 additions & 0 deletions web/src/utils/Configuration.ts
Expand Up @@ -23,6 +23,14 @@ export function getResetPassword() {
return getEmbeddedVariable("resetpassword") === "true";
}

export function getExternalResetPassword() {
return getEmbeddedVariable("externalresetpassword") === "true";
}

export function getExternalResetUrl() {
return getEmbeddedVariable("externalreseturl");
}
james-d-elliott marked this conversation as resolved.
Show resolved Hide resolved

export function getTheme() {
return getEmbeddedVariable("theme");
}
19 changes: 18 additions & 1 deletion web/src/views/LoginPortal/FirstFactor/FirstFactorForm.tsx
Expand Up @@ -17,6 +17,8 @@ export interface Props {
disabled: boolean;
rememberMe: boolean;
resetPassword: boolean;
externalResetPassword: boolean;
externalResetUrl: string;

onAuthenticationStart: () => void;
onAuthenticationFailure: () => void;
Expand Down Expand Up @@ -76,7 +78,11 @@ const FirstFactorForm = function (props: Props) {
};

const handleResetPasswordClick = () => {
navigate(ResetPasswordStep1Route);
if (props.resetPassword) {
navigate(ResetPasswordStep1Route);
} else if (props.externalResetPassword) {
window.open(props.externalResetUrl);
}
};

return (
Expand Down Expand Up @@ -192,6 +198,17 @@ const FirstFactorForm = function (props: Props) {
{translate("Reset password?")}
</Link>
</Grid>
) : props.externalResetPassword ? (
<Grid item xs={12} className={classnames(style.actionRow, style.flexEnd)}>
<Link
id="reset-password-button"
component="button"
onClick={handleResetPasswordClick}
className={style.resetLink}
>
{translate("Reset password?")}
</Link>
</Grid>
) : null}
</Grid>
</LoginLayout>
Expand Down
4 changes: 4 additions & 0 deletions web/src/views/LoginPortal/LoginPortal.tsx
Expand Up @@ -29,6 +29,8 @@ export interface Props {
duoSelfEnrollment: boolean;
rememberMe: boolean;
resetPassword: boolean;
externalResetPassword: boolean;
externalResetUrl: string;
}

const RedirectionErrorMessage =
Expand Down Expand Up @@ -175,6 +177,8 @@ const LoginPortal = function (props: Props) {
disabled={firstFactorDisabled}
rememberMe={props.rememberMe}
resetPassword={props.resetPassword}
externalResetPassword={props.externalResetPassword}
externalResetUrl={props.externalResetUrl}
onAuthenticationStart={() => setFirstFactorDisabled(true)}
onAuthenticationFailure={() => setFirstFactorDisabled(false)}
onAuthenticationSuccess={handleAuthSuccess}
Expand Down