[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'.
authentication_backend:
# Disable both the HTML element and the API for reset password functionality
disable_reset_password: false
# LDAP backend configuration.
#
# 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.
authentication_backend:
disable_reset_password: false
file:
path: /var/lib/authelia/users.yml
password_hashing:

View File

@ -12,4 +12,18 @@ 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.
* 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
authentication_backend:
disable_reset_password: false
ldap:
# The url to the ldap server. Scheme can be ldap:// or ldaps://
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
### 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
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.
type AuthenticationBackendConfiguration struct {
Ldap *LDAPAuthenticationBackendConfiguration `mapstructure:"ldap"`
File *FileAuthenticationBackendConfiguration `mapstructure:"file"`
DisableResetPassword bool `mapstructure:"disable_reset_password"`
Ldap *LDAPAuthenticationBackendConfiguration `mapstructure:"ldap"`
File *FileAuthenticationBackendConfiguration `mapstructure:"file"`
}

View File

@ -108,10 +108,12 @@ func validateLdapAuthenticationBackend(configuration *schema.LDAPAuthenticationB
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 == "" {
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 == "" {
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 {
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) {
body := ConfigurationBody{
GoogleAnalyticsTrackingID: ctx.Configuration.GoogleAnalyticsTrackingID,
RememberMeEnabled: ctx.Providers.SessionProvider.RememberMe != 0,
RememberMe: ctx.Providers.SessionProvider.RememberMe != 0,
ResetPassword: !ctx.Configuration.AuthenticationBackend.DisableResetPassword,
}
ctx.SetJSONBody(body)
}

View File

@ -29,7 +29,8 @@ func (s *ConfigurationSuite) TestShouldReturnConfiguredGATrackingID() {
expectedBody := ConfigurationBody{
GoogleAnalyticsTrackingID: GATrackingID,
RememberMeEnabled: true,
RememberMe: true,
ResetPassword: true,
}
ConfigurationGet(s.mock.Ctx)
@ -44,7 +45,22 @@ func (s *ConfigurationSuite) TestShouldDisableRememberMe() {
s.mock.Ctx.Configuration.Session)
expectedBody := ConfigurationBody{
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)

View File

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

View File

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

View File

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

View File

@ -12,6 +12,7 @@ import FixedTextField from "../../../components/FixedTextField";
export interface Props {
disabled: boolean;
rememberMe: boolean;
resetPassword: boolean;
onAuthenticationStart: () => void;
onAuthenticationFailure: () => void;
@ -41,7 +42,7 @@ export default function (props: Props) {
const handleRememberMeChange = () => {
setRememberMe(!rememberMe);
}
};
const handleSignIn = async () => {
if (username === "" || password === "") {
@ -67,11 +68,11 @@ export default function (props: Props) {
setPassword("");
passwordRef.current.focus();
}
}
};
const handleResetPasswordClick = () => {
history.push(ResetPasswordStep1Route);
}
};
return (
<LoginLayout
@ -81,7 +82,7 @@ export default function (props: Props) {
<Grid container spacing={2} className={style.root}>
<Grid item xs={12}>
<FixedTextField
// TODO (PR: #806, Issue: #511) potentially refactor
// TODO (PR: #806, Issue: #511) potentially refactor
inputRef={usernameRef}
id="username-textfield"
label="Username"
@ -101,7 +102,7 @@ export default function (props: Props) {
</Grid>
<Grid item xs={12}>
<FixedTextField
// TODO (PR: #806, Issue: #511) potentially refactor
// TODO (PR: #806, Issue: #511) potentially refactor
inputRef={passwordRef}
id="password-textfield"
label="Password"
@ -121,29 +122,33 @@ export default function (props: Props) {
}
}} />
</Grid>
<Grid item xs={12} className={classnames(style.leftAlign, style.actionRow)}>
{props.rememberMe ?
<FormControlLabel
control={
<Checkbox
id="remember-checkbox"
disabled={disabled}
checked={rememberMe}
onChange={handleRememberMeChange}
value="rememberMe"
color="primary"/>
}
className={style.rememberMe}
label="Remember me"
/> : null}
<Link
id="reset-password-button"
component="button"
onClick={handleResetPasswordClick}
className={style.resetLink}>
Reset password?
</Link>
</Grid>
{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 ?
<FormControlLabel
control={
<Checkbox
id="remember-checkbox"
disabled={disabled}
checked={rememberMe}
onChange={handleRememberMeChange}
value="rememberMe"
color="primary"/>
}
className={style.rememberMe}
label="Remember me"
/> : null}
{props.resetPassword ?
<Link
id="reset-password-button"
component="button"
onClick={handleResetPasswordClick}
className={style.resetLink}>
Reset password?
</Link> : null}
</Grid> : null}
<Grid item xs={12}>
<Button
id="sign-in-button"
@ -179,6 +184,9 @@ const useStyles = makeStyles(theme => ({
rememberMe: {
flexGrow: 1,
},
flexEnd: {
justifyContent: "flex-end",
},
leftAlign: {
textAlign: "left",
},
@ -186,4 +194,4 @@ const useStyles = makeStyles(theme => ({
textAlign: "right",
verticalAlign: "bottom",
},
}))
}));

View File

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