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
parent
cd2d88f9f3
commit
8659ba394d
|
@ -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
|
|
@ -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 |
|
@ -10,14 +10,16 @@ type Configuration struct {
|
|||
Log LogConfiguration `koanf:"log"`
|
||||
IdentityProviders IdentityProvidersConfiguration `koanf:"identity_providers"`
|
||||
AuthenticationBackend AuthenticationBackendConfiguration `koanf:"authentication_backend"`
|
||||
Session SessionConfiguration `koanf:"session"`
|
||||
TOTP TOTPConfiguration `koanf:"totp"`
|
||||
Webauthn WebauthnConfiguration `koanf:"webauthn"`
|
||||
DuoAPI *DuoAPIConfiguration `koanf:"duo_api"`
|
||||
AccessControl AccessControlConfiguration `koanf:"access_control"`
|
||||
NTP NTPConfiguration `koanf:"ntp"`
|
||||
Regulation RegulationConfiguration `koanf:"regulation"`
|
||||
|
||||
Server ServerConfiguration `koanf:"server"`
|
||||
Session SessionConfiguration `koanf:"session"`
|
||||
NTP NTPConfiguration `koanf:"ntp"`
|
||||
Storage StorageConfiguration `koanf:"storage"`
|
||||
Notifier *NotifierConfiguration `koanf:"notifier"`
|
||||
Server ServerConfiguration `koanf:"server"`
|
||||
Webauthn WebauthnConfiguration `koanf:"webauthn"`
|
||||
PasswordPolicy PasswordPolicyConfiguration `koanf:"password_policy"`
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
|
@ -60,4 +60,6 @@ func ValidateConfiguration(config *schema.Configuration, validator *schema.Struc
|
|||
ValidateIdentityProviders(&config.IdentityProviders, validator)
|
||||
|
||||
ValidateNTP(config, validator)
|
||||
|
||||
ValidatePasswordPolicy(&config.PasswordPolicy, validator)
|
||||
}
|
||||
|
|
|
@ -482,6 +482,17 @@ var ValidKeys = []string{
|
|||
"ntp.max_desync",
|
||||
"ntp.disable_startup_check",
|
||||
"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{
|
||||
|
|
|
@ -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"))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -44,6 +44,7 @@ const (
|
|||
messageUnableToRegisterSecurityKey = "Unable to register your security key."
|
||||
messageUnableToResetPassword = "Unable to reset your password."
|
||||
messageMFAValidationFailed = "Authentication failed, please retry later."
|
||||
messagePasswordWeak = "Your supplied password does not meet the password policy requirements"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
package handlers
|
||||
|
||||
import "errors"
|
||||
|
||||
var errPasswordPolicyNoMet = errors.New("the supplied password does not met the security policy")
|
|
@ -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.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.
|
||||
|
|
|
@ -2,6 +2,7 @@ package handlers
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"github.com/authelia/authelia/v4/internal/middlewares"
|
||||
"github.com/authelia/authelia/v4/internal/utils"
|
||||
|
@ -27,6 +28,11 @@ func ResetPasswordPost(ctx *middlewares.AutheliaCtx) {
|
|||
return
|
||||
}
|
||||
|
||||
if err := validatePassword(ctx, requestBody.Password); err != nil {
|
||||
ctx.Error(err, messagePasswordWeak)
|
||||
return
|
||||
}
|
||||
|
||||
err = ctx.Providers.UserProvider.UpdatePassword(*userSession.PasswordResetUsername, requestBody.Password)
|
||||
|
||||
if err != nil {
|
||||
|
@ -54,3 +60,54 @@ func ResetPasswordPost(ctx *middlewares.AutheliaCtx) {
|
|||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -112,3 +112,15 @@ type resetPasswordStep1RequestBody struct {
|
|||
type resetPasswordStep2RequestBody struct {
|
||||
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"`
|
||||
}
|
||||
|
|
|
@ -99,4 +99,21 @@ ntp:
|
|||
max_desync: 3s
|
||||
## You can enable or disable the NTP synchronization check on startup
|
||||
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
|
||||
...
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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...))
|
||||
})
|
||||
}
|
||||
}
|
|
@ -23,7 +23,8 @@
|
|||
"react-i18next": "11.16.2",
|
||||
"react-loading": "2.0.3",
|
||||
"react-otp-input": "2.4.0",
|
||||
"react-router-dom": "6.3.0"
|
||||
"react-router-dom": "6.3.0",
|
||||
"zxcvbn": "^4.4.2"
|
||||
},
|
||||
"scripts": {
|
||||
"prepare": "cd .. && husky install .github",
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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} />);
|
||||
});
|
|
@ -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;
|
|
@ -55,6 +55,13 @@
|
|||
"Access your email addresses": "Access your email addresses",
|
||||
"Accept": "Accept",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -55,6 +55,13 @@
|
|||
"Access your email addresses": "Acceso a su dirección de correo",
|
||||
"Accept": "Aceptar",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
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 { useTranslation } from "react-i18next";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
|
||||
import FixedTextField from "@components/FixedTextField";
|
||||
import PasswordMeter from "@components/PasswordMeter";
|
||||
import { IndexRoute } from "@constants/Routes";
|
||||
import { useNotifications } from "@hooks/NotificationsContext";
|
||||
import LoginLayout from "@layouts/LoginLayout";
|
||||
|
@ -23,6 +25,15 @@ const ResetPasswordStep2 = function () {
|
|||
const { createSuccessNotification, createErrorNotification } = useNotifications();
|
||||
const { t: translate } = useTranslation("Portal");
|
||||
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
|
||||
// the secret for OTP.
|
||||
const processToken = extractIdentityToken(location.search);
|
||||
|
@ -36,7 +47,22 @@ const ResetPasswordStep2 = function () {
|
|||
|
||||
try {
|
||||
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);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
@ -76,9 +102,9 @@ const ResetPasswordStep2 = function () {
|
|||
} catch (err) {
|
||||
console.error(err);
|
||||
if ((err as Error).message.includes("0000052D.")) {
|
||||
createErrorNotification(
|
||||
translate("Your supplied password does not meet the password policy requirements"),
|
||||
);
|
||||
createErrorNotification("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 {
|
||||
createErrorNotification(translate("There was an issue resetting the password"));
|
||||
}
|
||||
|
@ -97,21 +123,44 @@ const ResetPasswordStep2 = function () {
|
|||
id="password1-textfield"
|
||||
label={translate("New password")}
|
||||
variant="outlined"
|
||||
type="password"
|
||||
type={showPassword ? "text" : "password"}
|
||||
value={password1}
|
||||
disabled={formDisabled}
|
||||
onChange={(e) => setPassword1(e.target.value)}
|
||||
error={errorPassword1}
|
||||
className={classnames(style.fullWidth)}
|
||||
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 item xs={12}>
|
||||
<FixedTextField
|
||||
id="password2-textfield"
|
||||
label={translate("Repeat new password")}
|
||||
variant="outlined"
|
||||
type="password"
|
||||
type={showPassword ? "text" : "password"}
|
||||
disabled={formDisabled}
|
||||
value={password2}
|
||||
onChange={(e) => setPassword2(e.target.value)}
|
||||
|
|
Loading…
Reference in New Issue