[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 thispull/824/head
parent
a6308d72c3
commit
9800421b88
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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.
|
||||
* 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
|
||||
```
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
@ -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"))
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
}))
|
||||
}));
|
|
@ -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} />
|
||||
|
|
Loading…
Reference in New Issue