feat(authentication): password policy (#2723)

Implement a password policy with visual feedback in the web portal.

Co-authored-by: Manuel Nuñez <@mind-ar>
Co-authored-by: James Elliott <james-d-elliott@users.noreply.github.com>
pull/3101/head
Manuel Nuñez 2022-04-02 19:32:57 -03:00 committed by GitHub
parent cd2d88f9f3
commit 8659ba394d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 964 additions and 564 deletions

View File

@ -0,0 +1,110 @@
---
layout: default
title: Password Policy
parent: Configuration
nav_order: 17
---
# Password Policy
_Authelia_ allows administrators to configure an enforced password policy.
## Configuration
```yaml
password_policy:
standard:
enabled: false
min_length: 8
max_length: 0
require_uppercase: true
require_lowercase: true
require_number: true
require_special: true
zxcvbn:
enabled: false
```
## Options
### standard
<div markdown="1">
type: list
{: .label .label-config .label-purple }
required: no
{: .label .label-config .label-green }
</div>
This section allows you to enable standard security policies.
#### enabled
type: bool
{: .label .label-config .label-purple }
required: no
{: .label .label-config .label-green }
</div>
Enables standard password policy
#### min_length
type: integer
{: .label .label-config .label-purple }
required: no
{: .label .label-config .label-green }
</div>
Determines the minimum allowed password length
#### max_length
type: integer
{: .label .label-config .label-purple }
required: no
{: .label .label-config .label-green }
</div>
Determines the maximum allowed password length
#### require_uppercase
type: bool
{: .label .label-config .label-purple }
required: no
{: .label .label-config .label-green }
</div>
Indicates that at least one UPPERCASE letter must be provided as part of the password
#### require_lowercase
type: bool
{: .label .label-config .label-purple }
required: no
{: .label .label-config .label-green }
</div>
Indicates that at least one lowercase letter must be provided as part of the password
#### require_number
type: bool
{: .label .label-config .label-purple }
required: no
{: .label .label-config .label-green }
</div>
Indicates that at least one number must be provided as part of the password
#### require_special
type: bool
{: .label .label-config .label-purple }
required: no
{: .label .label-config .label-green }
</div>
Indicates that at least one special character must be provided as part of the password
### zxcvbn
This password policy enables advanced password strengh metering, using [Dropbox zxcvbn package](https://github.com/dropbox/zxcvbn).
Note that this password policy do not restrict the user's entry, just warns the user that if their password is too weak
#### enabled
type: bool
{: .label .label-config .label-purple }
required: no
{: .label .label-config .label-green }
</div>
Enables standard password policy
Note:
* only one password policy can be applied at a time

View File

@ -0,0 +1,36 @@
---
layout: default
title: Password Policy
parent: Features
nav_order: 8
---
# Password Policy
Password policy enforces the security by requering the users to use strong passwords
Currently, two methods are supported:
## classic
* this mode of operation allows administrators to set the rules that user passwords must comply with
* the available options are:
* Minimum password length
* Require Uppercase
* Require Lowercase
* Require Numbers
* Require Special characters
* when changing the password users must meet these rules
<p align="center">
<img src="../images/password-policy-classic-1.png" width="400">
</p>
## zxcvbn
* this mode uses zxcvbn for password strength checking (see: https://github.com/dropbox/zxcvbn)
* in this mode of operation, the user is not forced to follow any rules. the user is notified if their passwords is weak or strong
<p align="center">
<img src="../images/password-policy-zxcvbn-1.png" width="400">
</p>

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -10,14 +10,16 @@ type Configuration struct {
Log LogConfiguration `koanf:"log"` Log LogConfiguration `koanf:"log"`
IdentityProviders IdentityProvidersConfiguration `koanf:"identity_providers"` IdentityProviders IdentityProvidersConfiguration `koanf:"identity_providers"`
AuthenticationBackend AuthenticationBackendConfiguration `koanf:"authentication_backend"` AuthenticationBackend AuthenticationBackendConfiguration `koanf:"authentication_backend"`
Session SessionConfiguration `koanf:"session"`
TOTP TOTPConfiguration `koanf:"totp"` TOTP TOTPConfiguration `koanf:"totp"`
Webauthn WebauthnConfiguration `koanf:"webauthn"`
DuoAPI *DuoAPIConfiguration `koanf:"duo_api"` DuoAPI *DuoAPIConfiguration `koanf:"duo_api"`
AccessControl AccessControlConfiguration `koanf:"access_control"` AccessControl AccessControlConfiguration `koanf:"access_control"`
NTP NTPConfiguration `koanf:"ntp"`
Regulation RegulationConfiguration `koanf:"regulation"` Regulation RegulationConfiguration `koanf:"regulation"`
Server ServerConfiguration `koanf:"server"`
Session SessionConfiguration `koanf:"session"`
NTP NTPConfiguration `koanf:"ntp"`
Storage StorageConfiguration `koanf:"storage"` Storage StorageConfiguration `koanf:"storage"`
Notifier *NotifierConfiguration `koanf:"notifier"` Notifier *NotifierConfiguration `koanf:"notifier"`
Server ServerConfiguration `koanf:"server"` PasswordPolicy PasswordPolicyConfiguration `koanf:"password_policy"`
Webauthn WebauthnConfiguration `koanf:"webauthn"`
} }

View File

@ -0,0 +1,40 @@
package schema
// PasswordPolicyStandardParams represents the configuration related to standard parameters of password policy.
type PasswordPolicyStandardParams struct {
Enabled bool
MinLength int `koanf:"min_length"`
MaxLength int `koanf:"max_length"`
RequireUppercase bool `koanf:"require_uppercase"`
RequireLowercase bool `koanf:"require_lowercase"`
RequireNumber bool `koanf:"require_number"`
RequireSpecial bool `koanf:"require_special"`
}
// PasswordPolicyZxcvbnParams represents the configuration related to zxcvbn parameters of password policy.
type PasswordPolicyZxcvbnParams struct {
Enabled bool
MinScore int `koanf:"min_score"`
}
// PasswordPolicyConfiguration represents the configuration related to password policy.
type PasswordPolicyConfiguration struct {
Standard PasswordPolicyStandardParams `koanf:"standard"`
Zxcvbn PasswordPolicyZxcvbnParams `koanf:"zxcvbn"`
}
// DefaultPasswordPolicyConfiguration is the default password policy configuration.
var DefaultPasswordPolicyConfiguration = PasswordPolicyConfiguration{
Standard: PasswordPolicyStandardParams{
Enabled: false,
MinLength: 8,
MaxLength: 0,
RequireUppercase: true,
RequireLowercase: true,
RequireNumber: true,
RequireSpecial: true,
},
Zxcvbn: PasswordPolicyZxcvbnParams{
Enabled: false,
},
}

View File

@ -60,4 +60,6 @@ func ValidateConfiguration(config *schema.Configuration, validator *schema.Struc
ValidateIdentityProviders(&config.IdentityProviders, validator) ValidateIdentityProviders(&config.IdentityProviders, validator)
ValidateNTP(config, validator) ValidateNTP(config, validator)
ValidatePasswordPolicy(&config.PasswordPolicy, validator)
} }

View File

@ -482,6 +482,17 @@ var ValidKeys = []string{
"ntp.max_desync", "ntp.max_desync",
"ntp.disable_startup_check", "ntp.disable_startup_check",
"ntp.disable_failure", "ntp.disable_failure",
// Password Policy keys.
"password_policy.standard.enabled",
"password_policy.standard.min_length",
"password_policy.standard.max_length",
"password_policy.standard.require_uppercase",
"password_policy.standard.require_lowercase",
"password_policy.standard.require_number",
"password_policy.standard.require_special",
"password_policy.zxcvbn.enabled",
"password_policy.zxcvbn.min_score",
} }
var replacedKeys = map[string]string{ var replacedKeys = map[string]string{

View File

@ -0,0 +1,33 @@
package validator
import (
"errors"
"github.com/authelia/authelia/v4/internal/configuration/schema"
"github.com/authelia/authelia/v4/internal/utils"
)
// ValidatePasswordPolicy validates and update Password Policy configuration.
func ValidatePasswordPolicy(configuration *schema.PasswordPolicyConfiguration, validator *schema.StructValidator) {
if !utils.IsBoolCountLessThanN(1, true, configuration.Standard.Enabled, configuration.Zxcvbn.Enabled) {
validator.Push(errors.New("password_policy:only one password policy can be enabled at a time"))
}
if configuration.Standard.Enabled {
if configuration.Standard.MinLength == 0 {
configuration.Standard.MinLength = schema.DefaultPasswordPolicyConfiguration.Standard.MinLength
} else if configuration.Standard.MinLength < 0 {
validator.Push(errors.New("password_policy: min_length must be > 0"))
}
if configuration.Standard.MaxLength == 0 {
configuration.Standard.MaxLength = schema.DefaultPasswordPolicyConfiguration.Standard.MaxLength
}
} else if configuration.Zxcvbn.Enabled {
if configuration.Zxcvbn.MinScore == 0 {
configuration.Zxcvbn.MinScore = schema.DefaultPasswordPolicyConfiguration.Zxcvbn.MinScore
} else if configuration.Zxcvbn.MinScore < 0 || configuration.Zxcvbn.MinScore > 4 {
validator.Push(errors.New("min_score must be between 0 and 4"))
}
}
}

View File

@ -44,6 +44,7 @@ const (
messageUnableToRegisterSecurityKey = "Unable to register your security key." messageUnableToRegisterSecurityKey = "Unable to register your security key."
messageUnableToResetPassword = "Unable to reset your password." messageUnableToResetPassword = "Unable to reset your password."
messageMFAValidationFailed = "Authentication failed, please retry later." messageMFAValidationFailed = "Authentication failed, please retry later."
messagePasswordWeak = "Your supplied password does not meet the password policy requirements"
) )
const ( const (

View File

@ -0,0 +1,5 @@
package handlers
import "errors"
var errPasswordPolicyNoMet = errors.New("the supplied password does not met the security policy")

View File

@ -53,7 +53,28 @@ func resetPasswordIdentityFinish(ctx *middlewares.AutheliaCtx, username string)
ctx.Logger.Errorf("Unable to clear password reset flag in session for user %s: %s", userSession.Username, err) ctx.Logger.Errorf("Unable to clear password reset flag in session for user %s: %s", userSession.Username, err)
} }
ctx.ReplyOK() mode := ""
if ctx.Configuration.PasswordPolicy.Standard.Enabled {
mode = "standard"
} else if ctx.Configuration.PasswordPolicy.Zxcvbn.Enabled {
mode = "zxcvbn"
}
policyResponse := PassworPolicyBody{
Mode: mode,
MinLength: ctx.Configuration.PasswordPolicy.Standard.MinLength,
MaxLength: ctx.Configuration.PasswordPolicy.Standard.MaxLength,
RequireLowercase: ctx.Configuration.PasswordPolicy.Standard.RequireLowercase,
RequireUppercase: ctx.Configuration.PasswordPolicy.Standard.RequireUppercase,
RequireNumber: ctx.Configuration.PasswordPolicy.Standard.RequireNumber,
RequireSpecial: ctx.Configuration.PasswordPolicy.Standard.RequireSpecial,
MinScore: ctx.Configuration.PasswordPolicy.Zxcvbn.MinScore,
}
err = ctx.SetJSONBody(policyResponse)
if err != nil {
ctx.Logger.Errorf("Unable to send password Policy: %s", err)
}
} }
// ResetPasswordIdentityFinish the handler for finishing the identity validation. // ResetPasswordIdentityFinish the handler for finishing the identity validation.

View File

@ -2,6 +2,7 @@ package handlers
import ( import (
"fmt" "fmt"
"regexp"
"github.com/authelia/authelia/v4/internal/middlewares" "github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/utils" "github.com/authelia/authelia/v4/internal/utils"
@ -27,6 +28,11 @@ func ResetPasswordPost(ctx *middlewares.AutheliaCtx) {
return return
} }
if err := validatePassword(ctx, requestBody.Password); err != nil {
ctx.Error(err, messagePasswordWeak)
return
}
err = ctx.Providers.UserProvider.UpdatePassword(*userSession.PasswordResetUsername, requestBody.Password) err = ctx.Providers.UserProvider.UpdatePassword(*userSession.PasswordResetUsername, requestBody.Password)
if err != nil { if err != nil {
@ -54,3 +60,54 @@ func ResetPasswordPost(ctx *middlewares.AutheliaCtx) {
ctx.ReplyOK() ctx.ReplyOK()
} }
// validatePassword validates if the password met the password policy rules.
func validatePassword(ctx *middlewares.AutheliaCtx, password string) error {
// password validation applies only to standard passwor policy.
if !ctx.Configuration.PasswordPolicy.Standard.Enabled {
return nil
}
requireLowercase := ctx.Configuration.PasswordPolicy.Standard.RequireLowercase
requireUppercase := ctx.Configuration.PasswordPolicy.Standard.RequireUppercase
requireNumber := ctx.Configuration.PasswordPolicy.Standard.RequireNumber
requireSpecial := ctx.Configuration.PasswordPolicy.Standard.RequireSpecial
minLength := ctx.Configuration.PasswordPolicy.Standard.MinLength
maxlength := ctx.Configuration.PasswordPolicy.Standard.MaxLength
var patterns []string
if (minLength > 0 && len(password) < minLength) || (maxlength > 0 && len(password) > maxlength) {
return errPasswordPolicyNoMet
}
if requireLowercase {
patterns = append(patterns, "[a-z]+")
}
if requireUppercase {
patterns = append(patterns, "[A-Z]+")
}
if requireNumber {
patterns = append(patterns, "[0-9]+")
}
if requireSpecial {
patterns = append(patterns, "[^a-zA-Z0-9]+")
}
for _, pattern := range patterns {
re, err := regexp.Compile(pattern)
if err != nil {
return err
}
if found := re.MatchString(password); !found {
return errPasswordPolicyNoMet
}
}
return nil
}

View File

@ -112,3 +112,15 @@ type resetPasswordStep1RequestBody struct {
type resetPasswordStep2RequestBody struct { type resetPasswordStep2RequestBody struct {
Password string `json:"password"` Password string `json:"password"`
} }
// PassworPolicyBody represents the response sent by the password reset step 2.
type PassworPolicyBody struct {
Mode string `json:"mode"`
MinLength int `json:"min_length"`
MaxLength int `json:"max_length"`
MinScore int `json:"min_score"`
RequireUppercase bool `json:"require_uppercase"`
RequireLowercase bool `json:"require_lowercase"`
RequireNumber bool `json:"require_number"`
RequireSpecial bool `json:"require_special"`
}

View File

@ -99,4 +99,21 @@ ntp:
max_desync: 3s max_desync: 3s
## You can enable or disable the NTP synchronization check on startup ## You can enable or disable the NTP synchronization check on startup
disable_startup_check: false disable_startup_check: false
password_policy:
standard:
# Enables standard password Policy
enabled: false
min_length: 8
max_length: 0
require_uppercase: true
require_lowercase: true
require_number: true
require_special: true
zxcvbn:
## zxcvbn: uses zxcvbn for password strength checking (see: https://github.com/dropbox/zxcvbn)
## Note that the zxcvbn option does not prohibit the user from using a weak password,
## it only offers feedback about the strength of the password they are entering.
## if you need to enforce password rules, you should use `mode=classic`
enabled: false
... ...

View File

@ -0,0 +1,32 @@
package utils
// IsBoolCountLessThanN takes an int (n), bool (v), and then a variadic slice of bool (vals). If the number of bools in vals with
// the value v is more than n, it returns false, otherwise it returns true.
func IsBoolCountLessThanN(n int, v bool, vals ...bool) bool {
lvals := len(vals)
// If lvals (len of vals) is less than n it can't possibly have more than n so we can short circuit here.
if lvals < n {
return true
}
j := 0
for i, val := range vals {
if val == v {
j++
}
// If lvals (len of vals) minus the current index (the remainder) plus the number of positives
// is less than n we can short circuit here.
if lvals-i+j < n {
return true
}
if j > n {
return false
}
}
return true
}

View File

@ -0,0 +1,114 @@
package utils
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
func TestIsBoolCountLessThanN(t *testing.T) {
type h struct {
n int
v bool
vals []bool
}
testCases := []struct {
have h
want bool
}{
{
have: h{
n: 1,
v: true,
vals: []bool{true, false, false},
},
want: true,
},
{
have: h{
n: 1,
v: true,
vals: []bool{true, true, false},
},
want: false,
},
{
have: h{
n: 2,
v: true,
vals: []bool{true, true, false},
},
want: true,
},
{
have: h{
n: 2,
v: true,
vals: []bool{true, true, true},
},
want: false,
},
{
have: h{
n: 300,
v: true,
vals: []bool{true, true, true},
},
want: true,
},
{
have: h{
n: 300,
v: false,
vals: []bool{true, true, true},
},
want: true,
},
{
have: h{
n: 2,
v: false,
vals: []bool{false, false, false},
},
want: false,
},
{
have: h{
n: 1,
v: false,
vals: []bool{false, true, true},
},
want: true,
},
{
have: h{
n: 20,
v: false,
vals: []bool{true, true, true, true, true, true, true, true, true, true, true, true, true, true, true,
true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true,
true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true,
true, true, true, true, true},
},
want: true,
},
}
for _, tc := range testCases {
countTrue := 0
countFalse := 0
for _, v := range tc.have.vals {
if v {
countTrue++
} else {
countFalse++
}
}
t.Run(fmt.Sprintf("%d %t true(%d)-false(%d)/should be %t", tc.have.n, tc.have.v, countTrue, countFalse, tc.want), func(t *testing.T) {
assert.Equal(t, tc.want, IsBoolCountLessThanN(tc.have.n, tc.have.v, tc.have.vals...))
})
}
}

View File

@ -23,7 +23,8 @@
"react-i18next": "11.16.2", "react-i18next": "11.16.2",
"react-loading": "2.0.3", "react-loading": "2.0.3",
"react-otp-input": "2.4.0", "react-otp-input": "2.4.0",
"react-router-dom": "6.3.0" "react-router-dom": "6.3.0",
"zxcvbn": "^4.4.2"
}, },
"scripts": { "scripts": {
"prepare": "cd .. && husky install .github", "prepare": "cd .. && husky install .github",

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,13 @@
import React from "react";
import { render } from "@testing-library/react";
import PasswordMeter from "@components/PasswordMeter";
it("renders without crashing", () => {
render(<PasswordMeter value={""} minLength={4} />);
});
it("renders adjusted height without crashing", () => {
render(<PasswordMeter value={"Passw0rd!"} minLength={4} />);
});

View File

@ -0,0 +1,138 @@
import React, { useState, useEffect } from "react";
import { makeStyles } from "@material-ui/core";
import classnames from "classnames";
import { useTranslation } from "react-i18next";
import zxcvbn from "zxcvbn";
export interface Props {
value: string;
/**
* mode password meter mode
* classic: classic mode (checks lowercase, uppercase, specials and numbers)
* zxcvbn: uses zxcvbn package to get the password strength
**/
mode: string;
minLength: number;
maxLength: number;
requireLowerCase: boolean;
requireUpperCase: boolean;
requireNumber: boolean;
requireSpecial: boolean;
}
const PasswordMeter = function (props: Props) {
const [progressColor] = useState(["#D32F2F", "#FF5722", "#FFEB3B", "#AFB42B", "#62D32F"]);
const [passwordScore, setPasswordScore] = useState(0);
const [maxScores, setMaxScores] = useState(0);
const [feedback, setFeedback] = useState("");
const { t: translate } = useTranslation("Portal");
const style = makeStyles((theme) => ({
progressBar: {
height: "5px",
marginTop: "2px",
backgroundColor: "red",
width: "50%",
transition: "width .5s linear",
},
}))();
useEffect(() => {
const password = props.value;
if (props.mode === "standard") {
//use mode mode
setMaxScores(4);
if (password.length < props.minLength) {
setPasswordScore(0);
setFeedback(translate("Must be at least {{len}} characters in length", { len: props.minLength }));
return;
}
if (password.length > props.maxLength) {
setPasswordScore(0);
setFeedback(translate("Must not be more than {{len}} characters in length", { len: props.maxLength }));
return;
}
setFeedback("");
let score = 1;
let required = 0;
let hits = 0;
let warning = "";
if (props.requireLowerCase) {
required++;
const hasLowercase = /[a-z]/.test(password);
if (hasLowercase) {
hits++;
} else {
warning += "* " + translate("Must have at least one lowercase letter") + "\n";
}
}
if (props.requireUpperCase) {
required++;
const hasUppercase = /[A-Z]/.test(password);
if (hasUppercase) {
hits++;
} else {
warning += "* " + translate("Must have at least one UPPERCASE letter") + "\n";
}
}
if (props.requireNumber) {
required++;
const hasNumber = /[0-9]/.test(password);
if (hasNumber) {
hits++;
} else {
warning += "* " + translate("Must have at least one number") + "\n";
}
}
if (props.requireSpecial) {
required++;
const hasSpecial = /[^0-9\w]/i.test(password);
if (hasSpecial) {
hits++;
} else {
warning += "* " + translate("Must have at least one special character") + "\n";
}
}
score += hits > 0 ? 1 : 0;
score += required === hits ? 1 : 0;
if (warning !== "") {
setFeedback(translate("The password does not meet the password policy") + ":\n" + warning);
}
setPasswordScore(score);
} else if (props.mode === "zxcvbn") {
//use zxcvbn mode
setMaxScores(5);
const { score, feedback } = zxcvbn(password);
setFeedback(feedback.warning);
setPasswordScore(score);
}
}, [props, translate]);
if (props.mode === "" || props.mode === "none") return <span></span>;
return (
<div
style={{
width: "100%",
}}
>
<div
title={feedback}
className={classnames(style.progressBar)}
style={{
width: `${(passwordScore + 1) * (100 / maxScores)}%`,
backgroundColor: progressColor[passwordScore],
}}
></div>
</div>
);
};
PasswordMeter.defaultProps = {
minLength: 0,
};
export default PasswordMeter;

View File

@ -55,6 +55,13 @@
"Access your email addresses": "Access your email addresses", "Access your email addresses": "Access your email addresses",
"Accept": "Accept", "Accept": "Accept",
"Deny": "Deny", "Deny": "Deny",
"The above application is requesting the following permissions": "The above application is requesting the following permissions" "The above application is requesting the following permissions": "The above application is requesting the following permissions",
"The password does not meet the password policy": "The password does not meet the password policy",
"Must have at least one lowercase letter": "Must have at least one lowercase letter",
"Must have at least one UPPERCASE letter": "Must have at least one UPPERCASE letter",
"Must have at least one number": "Must have at least one number",
"Must have at least one special character": "Must have at least one special character",
"Must be at least {{len}} characters in length": "Must be at least {{len}} characters in length",
"Must not be more than {{len}} characters in length": "Must not be more than {{len}} characters in length"
} }
} }

View File

@ -55,6 +55,13 @@
"Access your email addresses": "Acceso a su dirección de correo", "Access your email addresses": "Acceso a su dirección de correo",
"Accept": "Aceptar", "Accept": "Aceptar",
"Deny": "Denegar", "Deny": "Denegar",
"The above application is requesting the following permissions": "La aplicación solicita los siguientes permisos" "The above application is requesting the following permissions": "La aplicación solicita los siguientes permisos",
"The password does not meet the password policy": "La contraseña no cumple con la política de contraseñas",
"Must have at least one lowercase letter": "Debe contener al menos una letra minúscula",
"Must have at least one UPPERCASE letter": "Debe contener al menos una letra MAYUSCULA",
"Must have at least one number": "Debe contener al menos un número",
"Must have at least one special character": "Debe contener al menos un caracter especial",
"Must be at least {{len}} characters in length": "La longitud mínima es de {{len}} caracteres",
"Must not be more than {{len}} characters in length": "La longitud máxima es de {{len}} caracteres"
} }
} }

View File

@ -1,11 +1,13 @@
import React, { useState, useCallback, useEffect } from "react"; import React, { useState, useCallback, useEffect } from "react";
import { Grid, Button, makeStyles } from "@material-ui/core"; import { Grid, Button, makeStyles, InputAdornment, IconButton } from "@material-ui/core";
import { Visibility, VisibilityOff } from "@material-ui/icons";
import classnames from "classnames"; import classnames from "classnames";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useLocation, useNavigate } from "react-router-dom"; import { useLocation, useNavigate } from "react-router-dom";
import FixedTextField from "@components/FixedTextField"; import FixedTextField from "@components/FixedTextField";
import PasswordMeter from "@components/PasswordMeter";
import { IndexRoute } from "@constants/Routes"; import { IndexRoute } from "@constants/Routes";
import { useNotifications } from "@hooks/NotificationsContext"; import { useNotifications } from "@hooks/NotificationsContext";
import LoginLayout from "@layouts/LoginLayout"; import LoginLayout from "@layouts/LoginLayout";
@ -23,6 +25,15 @@ const ResetPasswordStep2 = function () {
const { createSuccessNotification, createErrorNotification } = useNotifications(); const { createSuccessNotification, createErrorNotification } = useNotifications();
const { t: translate } = useTranslation("Portal"); const { t: translate } = useTranslation("Portal");
const navigate = useNavigate(); const navigate = useNavigate();
const [showPassword, setShowPassword] = useState(false);
const [pPolicyMode, setPPolicyMode] = useState("none");
const [pPolicyMinLength, setPPolicyMinLength] = useState(0);
const [pPolicyMaxLength, setPPolicyMaxLength] = useState(0);
const [pPolicyRequireUpperCase, setPPolicyRequireUpperCase] = useState(false);
const [pPolicyRequireLowerCase, setPPolicyRequireLowerCase] = useState(false);
const [pPolicyRequireNumber, setPPolicyRequireNumber] = useState(false);
const [pPolicyRequireSpecial, setPPolicyRequireSpecial] = useState(false);
// Get the token from the query param to give it back to the API when requesting // Get the token from the query param to give it back to the API when requesting
// the secret for OTP. // the secret for OTP.
const processToken = extractIdentityToken(location.search); const processToken = extractIdentityToken(location.search);
@ -36,7 +47,22 @@ const ResetPasswordStep2 = function () {
try { try {
setFormDisabled(true); setFormDisabled(true);
await completeResetPasswordProcess(processToken); const {
mode,
min_length,
max_length,
require_uppercase,
require_lowercase,
require_number,
require_special,
} = await completeResetPasswordProcess(processToken);
setPPolicyMode(mode);
setPPolicyMinLength(min_length);
setPPolicyMaxLength(max_length);
setPPolicyRequireLowerCase(require_lowercase);
setPPolicyRequireUpperCase(require_uppercase);
setPPolicyRequireNumber(require_number);
setPPolicyRequireSpecial(require_special);
setFormDisabled(false); setFormDisabled(false);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
@ -76,9 +102,9 @@ const ResetPasswordStep2 = function () {
} catch (err) { } catch (err) {
console.error(err); console.error(err);
if ((err as Error).message.includes("0000052D.")) { if ((err as Error).message.includes("0000052D.")) {
createErrorNotification( createErrorNotification("Your supplied password does not meet the password policy requirements.");
translate("Your supplied password does not meet the password policy requirements"), } else if ((err as Error).message.includes("policy")) {
); createErrorNotification("Your supplied password does not meet the password policy requirements.");
} else { } else {
createErrorNotification(translate("There was an issue resetting the password")); createErrorNotification(translate("There was an issue resetting the password"));
} }
@ -97,21 +123,44 @@ const ResetPasswordStep2 = function () {
id="password1-textfield" id="password1-textfield"
label={translate("New password")} label={translate("New password")}
variant="outlined" variant="outlined"
type="password" type={showPassword ? "text" : "password"}
value={password1} value={password1}
disabled={formDisabled} disabled={formDisabled}
onChange={(e) => setPassword1(e.target.value)} onChange={(e) => setPassword1(e.target.value)}
error={errorPassword1} error={errorPassword1}
className={classnames(style.fullWidth)} className={classnames(style.fullWidth)}
autoComplete="new-password" autoComplete="new-password"
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label="toggle password visibility"
onClick={(e) => setShowPassword(!showPassword)}
edge="end"
>
{showPassword ? <VisibilityOff></VisibilityOff> : <Visibility></Visibility>}
</IconButton>
</InputAdornment>
),
}}
/> />
<PasswordMeter
value={password1}
mode={pPolicyMode}
minLength={pPolicyMinLength}
maxLength={pPolicyMaxLength}
requireLowerCase={pPolicyRequireLowerCase}
requireUpperCase={pPolicyRequireUpperCase}
requireNumber={pPolicyRequireNumber}
requireSpecial={pPolicyRequireSpecial}
></PasswordMeter>
</Grid> </Grid>
<Grid item xs={12}> <Grid item xs={12}>
<FixedTextField <FixedTextField
id="password2-textfield" id="password2-textfield"
label={translate("Repeat new password")} label={translate("Repeat new password")}
variant="outlined" variant="outlined"
type="password" type={showPassword ? "text" : "password"}
disabled={formDisabled} disabled={formDisabled}
value={password2} value={password2}
onChange={(e) => setPassword2(e.target.value)} onChange={(e) => setPassword2(e.target.value)}