[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'.
|
# 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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
```
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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",
|
||||||
},
|
},
|
||||||
}))
|
}));
|
|
@ -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} />
|
||||||
|
|
Loading…
Reference in New Issue