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"`
|
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"`
|
||||||
Storage StorageConfiguration `koanf:"storage"`
|
|
||||||
Notifier *NotifierConfiguration `koanf:"notifier"`
|
Server ServerConfiguration `koanf:"server"`
|
||||||
Server ServerConfiguration `koanf:"server"`
|
Session SessionConfiguration `koanf:"session"`
|
||||||
Webauthn WebauthnConfiguration `koanf:"webauthn"`
|
NTP NTPConfiguration `koanf:"ntp"`
|
||||||
|
Storage StorageConfiguration `koanf:"storage"`
|
||||||
|
Notifier *NotifierConfiguration `koanf:"notifier"`
|
||||||
|
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)
|
ValidateIdentityProviders(&config.IdentityProviders, validator)
|
||||||
|
|
||||||
ValidateNTP(config, validator)
|
ValidateNTP(config, validator)
|
||||||
|
|
||||||
|
ValidatePasswordPolicy(&config.PasswordPolicy, validator)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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{
|
||||||
|
|
|
@ -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."
|
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 (
|
||||||
|
|
|
@ -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.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.
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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"`
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
...
|
...
|
||||||
|
|
|
@ -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-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
|
@ -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",
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)}
|
||||||
|
|
Loading…
Reference in New Issue