[FEATURE] Disable Reset Password (#823)

* [FEATURE] Disable Reset Password
* add configuration key to authentication_backend called disable_reset_password
* disable_reset_password prevents the API handler for the functionality and the UI element
* disable_reset_password is a boolean
* adjust RememberMeEnabled to be RememberMe instead as it's just unnecessary
* add docs for security measures and in the authentication docs
* updated config.template.yml
* add flexEnd style to align reset password when remember me disabled
* add todo items for ldap user/password validation relating to this
pull/824/head
James Elliott 2020-04-05 09:28:09 +10:00 committed by GitHub
parent a6308d72c3
commit 9800421b88
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 107 additions and 45 deletions

View File

@ -67,6 +67,9 @@ duo_api:
# #
# There are two supported backends: 'ldap' and 'file'. # There are two supported backends: 'ldap' and 'file'.
authentication_backend: authentication_backend:
# Disable both the HTML element and the API for reset password functionality
disable_reset_password: false
# LDAP backend configuration. # LDAP backend configuration.
# #
# This backend allows Authelia to be scaled to more # This backend allows Authelia to be scaled to more

View File

@ -16,6 +16,7 @@ Configuring Authelia to use a file is done by specifying the path to the
file in the configuration file. file in the configuration file.
authentication_backend: authentication_backend:
disable_reset_password: false
file: file:
path: /var/lib/authelia/users.yml path: /var/lib/authelia/users.yml
password_hashing: password_hashing:

View File

@ -13,3 +13,17 @@ There are two ways to store the users along with their password:
* LDAP: users are stored in remote servers like OpenLDAP, OpenAM or Microsoft Active Directory. * LDAP: users are stored in remote servers like OpenLDAP, OpenAM or Microsoft Active Directory.
* File: users are stored in YAML file with a hashed version of their password. * File: users are stored in YAML file with a hashed version of their password.
## Disabling Reset Password
You can disable the reset password functionality for additional security as per this configuration:
```yaml
# The authentication backend to use for verifying user passwords
# and retrieve information such as email address and groups
# users belong to.
#
# There are two supported backends: 'ldap' and 'file'.
authentication_backend:
# Disable both the HTML element and the API for reset password functionality
disable_reset_password: true
```

View File

@ -16,6 +16,7 @@ Configuration of the LDAP backend is done as follows
```yaml ```yaml
authentication_backend: authentication_backend:
disable_reset_password: false
ldap: ldap:
# The url to the ldap server. Scheme can be ldap:// or ldaps:// # The url to the ldap server. Scheme can be ldap:// or ldaps://
url: ldap://127.0.0.1 url: ldap://127.0.0.1

View File

@ -97,6 +97,12 @@ an attacker to intercept a link used to setup 2FA; which reduces security
## Additional security ## Additional security
### Reset Password
It's possible to disable the reset password functionality and is recommended for anyone
wanting to increase security. See the [configuration](../configuration/authentication/index.md)
for information.
### Session security ### Session security
We have a few options to configure the security of a session. The main and most important We have a few options to configure the security of a session. The main and most important

View File

@ -60,6 +60,7 @@ var DefaultPasswordOptionsSHA512Configuration = PasswordHashingConfiguration{
// AuthenticationBackendConfiguration represents the configuration related to the authentication backend. // AuthenticationBackendConfiguration represents the configuration related to the authentication backend.
type AuthenticationBackendConfiguration struct { type AuthenticationBackendConfiguration struct {
DisableResetPassword bool `mapstructure:"disable_reset_password"`
Ldap *LDAPAuthenticationBackendConfiguration `mapstructure:"ldap"` Ldap *LDAPAuthenticationBackendConfiguration `mapstructure:"ldap"`
File *FileAuthenticationBackendConfiguration `mapstructure:"file"` File *FileAuthenticationBackendConfiguration `mapstructure:"file"`
} }

View File

@ -108,10 +108,12 @@ func validateLdapAuthenticationBackend(configuration *schema.LDAPAuthenticationB
configuration.URL = validateLdapURL(configuration.URL, validator) configuration.URL = validateLdapURL(configuration.URL, validator)
} }
// TODO: see if it's possible to disable this check if disable_reset_password is set and when anonymous/user binding is supported (#101 and #387)
if configuration.User == "" { if configuration.User == "" {
validator.Push(errors.New("Please provide a user name to connect to the LDAP server")) validator.Push(errors.New("Please provide a user name to connect to the LDAP server"))
} }
// TODO: see if it's possible to disable this check if disable_reset_password is set and when anonymous/user binding is supported (#101 and #387)
if configuration.Password == "" { if configuration.Password == "" {
validator.Push(errors.New("Please provide a password to connect to the LDAP server")) validator.Push(errors.New("Please provide a password to connect to the LDAP server"))
} }

View File

@ -4,13 +4,15 @@ import "github.com/authelia/authelia/internal/middlewares"
type ConfigurationBody struct { type ConfigurationBody struct {
GoogleAnalyticsTrackingID string `json:"ga_tracking_id,omitempty"` GoogleAnalyticsTrackingID string `json:"ga_tracking_id,omitempty"`
RememberMeEnabled bool `json:"remember_me_enabled"` // whether remember me is enabled or not RememberMe bool `json:"remember_me"` // whether remember me is enabled or not
ResetPassword bool `json:"reset_password"`
} }
func ConfigurationGet(ctx *middlewares.AutheliaCtx) { func ConfigurationGet(ctx *middlewares.AutheliaCtx) {
body := ConfigurationBody{ body := ConfigurationBody{
GoogleAnalyticsTrackingID: ctx.Configuration.GoogleAnalyticsTrackingID, GoogleAnalyticsTrackingID: ctx.Configuration.GoogleAnalyticsTrackingID,
RememberMeEnabled: ctx.Providers.SessionProvider.RememberMe != 0, RememberMe: ctx.Providers.SessionProvider.RememberMe != 0,
ResetPassword: !ctx.Configuration.AuthenticationBackend.DisableResetPassword,
} }
ctx.SetJSONBody(body) ctx.SetJSONBody(body)
} }

View File

@ -29,7 +29,8 @@ func (s *ConfigurationSuite) TestShouldReturnConfiguredGATrackingID() {
expectedBody := ConfigurationBody{ expectedBody := ConfigurationBody{
GoogleAnalyticsTrackingID: GATrackingID, GoogleAnalyticsTrackingID: GATrackingID,
RememberMeEnabled: true, RememberMe: true,
ResetPassword: true,
} }
ConfigurationGet(s.mock.Ctx) ConfigurationGet(s.mock.Ctx)
@ -44,7 +45,22 @@ func (s *ConfigurationSuite) TestShouldDisableRememberMe() {
s.mock.Ctx.Configuration.Session) s.mock.Ctx.Configuration.Session)
expectedBody := ConfigurationBody{ expectedBody := ConfigurationBody{
GoogleAnalyticsTrackingID: GATrackingID, GoogleAnalyticsTrackingID: GATrackingID,
RememberMeEnabled: false, RememberMe: false,
ResetPassword: true,
}
ConfigurationGet(s.mock.Ctx)
s.mock.Assert200OK(s.T(), expectedBody)
}
func (s *ConfigurationSuite) TestShouldDisableResetPassword() {
GATrackingID := "ABC"
s.mock.Ctx.Configuration.GoogleAnalyticsTrackingID = GATrackingID
s.mock.Ctx.Configuration.AuthenticationBackend.DisableResetPassword = true
expectedBody := ConfigurationBody{
GoogleAnalyticsTrackingID: GATrackingID,
RememberMe: true,
ResetPassword: false,
} }
ConfigurationGet(s.mock.Ctx) ConfigurationGet(s.mock.Ctx)

View File

@ -41,6 +41,8 @@ func StartServer(configuration schema.Configuration, providers middlewares.Provi
router.POST("/api/firstfactor", autheliaMiddleware(handlers.FirstFactorPost)) router.POST("/api/firstfactor", autheliaMiddleware(handlers.FirstFactorPost))
router.POST("/api/logout", autheliaMiddleware(handlers.LogoutPost)) router.POST("/api/logout", autheliaMiddleware(handlers.LogoutPost))
// only register endpoints if forgot password is not disabled
if !configuration.AuthenticationBackend.DisableResetPassword {
// Password reset related endpoints. // Password reset related endpoints.
router.POST("/api/reset-password/identity/start", autheliaMiddleware( router.POST("/api/reset-password/identity/start", autheliaMiddleware(
handlers.ResetPasswordIdentityStart)) handlers.ResetPasswordIdentityStart))
@ -48,6 +50,7 @@ func StartServer(configuration schema.Configuration, providers middlewares.Provi
handlers.ResetPasswordIdentityFinish)) handlers.ResetPasswordIdentityFinish))
router.POST("/api/reset-password", autheliaMiddleware( router.POST("/api/reset-password", autheliaMiddleware(
handlers.ResetPasswordPost)) handlers.ResetPasswordPost))
}
// Information about the user // Information about the user
router.GET("/api/user/info", autheliaMiddleware( router.GET("/api/user/info", autheliaMiddleware(

View File

@ -56,7 +56,9 @@ const App: React.FC = () => {
<SignOut /> <SignOut />
</Route> </Route>
<Route path={FirstFactorRoute}> <Route path={FirstFactorRoute}>
<LoginPortal rememberMe={configuration?.remember_me_enabled === true}/> <LoginPortal
rememberMe={configuration?.remember_me === true}
resetPassword={configuration?.reset_password === true} />
</Route> </Route>
<Route path="/"> <Route path="/">
<Redirect to={FirstFactorRoute}></Redirect> <Redirect to={FirstFactorRoute}></Redirect>

View File

@ -2,7 +2,8 @@ import { SecondFactorMethod } from "./Methods";
export interface Configuration { export interface Configuration {
ga_tracking_id: string; ga_tracking_id: string;
remember_me_enabled: boolean; remember_me: boolean;
reset_password: boolean;
} }
export interface ExtendedConfiguration { export interface ExtendedConfiguration {

View File

@ -12,6 +12,7 @@ import FixedTextField from "../../../components/FixedTextField";
export interface Props { export interface Props {
disabled: boolean; disabled: boolean;
rememberMe: boolean; rememberMe: boolean;
resetPassword: boolean;
onAuthenticationStart: () => void; onAuthenticationStart: () => void;
onAuthenticationFailure: () => void; onAuthenticationFailure: () => void;
@ -41,7 +42,7 @@ export default function (props: Props) {
const handleRememberMeChange = () => { const handleRememberMeChange = () => {
setRememberMe(!rememberMe); setRememberMe(!rememberMe);
} };
const handleSignIn = async () => { const handleSignIn = async () => {
if (username === "" || password === "") { if (username === "" || password === "") {
@ -67,11 +68,11 @@ export default function (props: Props) {
setPassword(""); setPassword("");
passwordRef.current.focus(); passwordRef.current.focus();
} }
} };
const handleResetPasswordClick = () => { const handleResetPasswordClick = () => {
history.push(ResetPasswordStep1Route); history.push(ResetPasswordStep1Route);
} };
return ( return (
<LoginLayout <LoginLayout
@ -121,7 +122,10 @@ export default function (props: Props) {
} }
}} /> }} />
</Grid> </Grid>
<Grid item xs={12} className={classnames(style.leftAlign, style.actionRow)}> {props.rememberMe || props.resetPassword ?
<Grid item xs={12} className={props.rememberMe
? classnames(style.leftAlign, style.actionRow)
: classnames(style.leftAlign, style.flexEnd, style.actionRow)}>
{props.rememberMe ? {props.rememberMe ?
<FormControlLabel <FormControlLabel
control={ control={
@ -136,14 +140,15 @@ export default function (props: Props) {
className={style.rememberMe} className={style.rememberMe}
label="Remember me" label="Remember me"
/> : null} /> : null}
{props.resetPassword ?
<Link <Link
id="reset-password-button" id="reset-password-button"
component="button" component="button"
onClick={handleResetPasswordClick} onClick={handleResetPasswordClick}
className={style.resetLink}> className={style.resetLink}>
Reset password? Reset password?
</Link> </Link> : null}
</Grid> </Grid> : null}
<Grid item xs={12}> <Grid item xs={12}>
<Button <Button
id="sign-in-button" id="sign-in-button"
@ -179,6 +184,9 @@ const useStyles = makeStyles(theme => ({
rememberMe: { rememberMe: {
flexGrow: 1, flexGrow: 1,
}, },
flexEnd: {
justifyContent: "flex-end",
},
leftAlign: { leftAlign: {
textAlign: "left", textAlign: "left",
}, },
@ -186,4 +194,4 @@ const useStyles = makeStyles(theme => ({
textAlign: "right", textAlign: "right",
verticalAlign: "bottom", verticalAlign: "bottom",
}, },
})) }));

View File

@ -18,6 +18,7 @@ import AuthenticatedView from "./AuthenticatedView/AuthenticatedView";
export interface Props { export interface Props {
rememberMe: boolean; rememberMe: boolean;
resetPassword: boolean;
} }
export default function (props: Props) { export default function (props: Props) {
@ -119,6 +120,7 @@ export default function (props: Props) {
<FirstFactorForm <FirstFactorForm
disabled={firstFactorDisabled} disabled={firstFactorDisabled}
rememberMe={props.rememberMe} rememberMe={props.rememberMe}
resetPassword={props.resetPassword}
onAuthenticationStart={() => setFirstFactorDisabled(true)} onAuthenticationStart={() => setFirstFactorDisabled(true)}
onAuthenticationFailure={() => setFirstFactorDisabled(false)} onAuthenticationFailure={() => setFirstFactorDisabled(false)}
onAuthenticationSuccess={handleAuthSuccess} /> onAuthenticationSuccess={handleAuthSuccess} />