diff --git a/api/openapi.yml b/api/openapi.yml index 62c8ee3b4..10ec36b9d 100644 --- a/api/openapi.yml +++ b/api/openapi.yml @@ -730,6 +730,9 @@ components: max_length: type: integer description: The maximum password length when using the standard mode. + min_score: + type: integer + description: The minimum password score when using the zxcvbn mode. require_uppercase: type: boolean description: If uppercase characters are required when using the standard mode. diff --git a/config.template.yml b/config.template.yml index 1cc564f53..1739cec81 100644 --- a/config.template.yml +++ b/config.template.yml @@ -365,6 +365,9 @@ password_policy: zxcvbn: enabled: false + ## Configures the minimum score allowed. + min_score: 3 + ## ## Access Control Configuration ## diff --git a/docs/configuration/password_policy.md b/docs/configuration/password_policy.md index 0661d7222..71f483a5f 100644 --- a/docs/configuration/password_policy.md +++ b/docs/configuration/password_policy.md @@ -23,6 +23,7 @@ password_policy: require_special: false zxcvbn: enabled: false + min_score: 3 ``` ## Options @@ -39,8 +40,10 @@ This section allows you to enable standard security policies. #### enabled
-type: bool +type: boolean {: .label .label-config .label-purple } +default: false +{: .label .label-config .label-blue } required: no {: .label .label-config .label-green }
@@ -73,8 +76,10 @@ Determines the maximum allowed password length. #### require_uppercase
-type: bool +type: boolean {: .label .label-config .label-purple } +default: false +{: .label .label-config .label-blue } required: no {: .label .label-config .label-green }
@@ -83,8 +88,10 @@ Indicates that at least one UPPERCASE letter must be provided as part of the pas #### require_lowercase
-type: bool +type: boolean {: .label .label-config .label-purple } +default: false +{: .label .label-config .label-blue } required: no {: .label .label-config .label-green }
@@ -93,8 +100,10 @@ Indicates that at least one lowercase letter must be provided as part of the pas #### require_number
-type: bool +type: boolean {: .label .label-config .label-purple } +default: false +{: .label .label-config .label-blue } required: no {: .label .label-config .label-green }
@@ -103,8 +112,10 @@ Indicates that at least one number must be provided as part of the password. #### require_special
-type: bool +type: boolean {: .label .label-config .label-purple } +default: false +{: .label .label-config .label-blue } required: no {: .label .label-config .label-green }
@@ -115,13 +126,12 @@ Indicates that at least one special character must be provided as part of the pa This password policy enables advanced password strength metering, using [zxcvbn](https://github.com/dropbox/zxcvbn). -Note that this password policy do not restrict the user's entry it just gives the user feedback as to how strong their -password is. - #### enabled
-type: bool +type: boolean {: .label .label-config .label-purple } +default: false +{: .label .label-config .label-blue } required: no {: .label .label-config .label-green }
@@ -130,4 +140,22 @@ _**Important Note:** only one password policy can be applied at a time._ Enables zxcvbn password policy. +#### min_score +
+type: integer +{: .label .label-config .label-purple } +default: 3 +{: .label .label-config .label-blue } +required: no +{: .label .label-config .label-green } +
+Configures the minimum zxcvbn score allowed for new passwords. There are 5 levels in the zxcvbn score system (taken from [github.com/dropbox/zxcvbn](https://github.com/dropbox/zxcvbn#usage)): + +- score 0: too guessable: risky password (guesses < 10^3) +- score 1: very guessable: protection from throttled online attacks (guesses < 10^6) +- score 2: somewhat guessable: protection from unthrottled online attacks. (guesses < 10^8) +- score 3: safely unguessable: moderate protection from offline slow-hash scenario. (guesses < 10^10) +- score 4: very unguessable: strong protection from offline slow-hash scenario. (guesses >= 10^10) + +We do not allow score 0, if you set the `min_score` value to 0 instead the default will be chosen. \ No newline at end of file diff --git a/go.mod b/go.mod index 97ef1e098..50cda5e9e 100644 --- a/go.mod +++ b/go.mod @@ -31,6 +31,7 @@ require ( github.com/spf13/cobra v1.4.0 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.7.1 + github.com/trustelem/zxcvbn v1.0.1 github.com/valyala/fasthttp v1.35.0 golang.org/x/text v0.3.7 gopkg.in/square/go-jose.v2 v2.6.0 @@ -45,6 +46,7 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/dgraph-io/ristretto v0.1.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/dlclark/regexp2 v1.4.0 // indirect github.com/dustin/go-humanize v1.0.0 // indirect github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect github.com/fsnotify/fsnotify v1.5.1 // indirect @@ -85,6 +87,7 @@ require ( github.com/spf13/cast v1.4.1 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/subosito/gotenv v1.2.0 // indirect + github.com/test-go/testify v1.1.4 // indirect github.com/tinylib/msgp v1.1.6 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/x448/float16 v0.8.4 // indirect diff --git a/go.sum b/go.sum index 9caf9f462..5d3add6d6 100644 --- a/go.sum +++ b/go.sum @@ -160,6 +160,8 @@ github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUn github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E= +github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v17.12.0-ce-rc1.0.20201201034508-7d75c1d40d88+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= @@ -1243,6 +1245,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/subosito/gotenv v1.1.1/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/test-go/testify v1.1.4 h1:Tf9lntrKUMHiXQ07qBScBTSA0dhYQlu83hswqelv1iE= +github.com/test-go/testify v1.1.4/go.mod h1:rH7cfJo/47vWGdi4GPj16x3/t1xGOj2YxzmNQzk2ghU= github.com/tidwall/gjson v1.11.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= @@ -1255,6 +1259,8 @@ github.com/tinylib/msgp v1.1.6 h1:i+SbKraHhnrf9M5MYmvQhFnbLhAXSDWF8WWsuyRdocw= github.com/tinylib/msgp v1.1.6/go.mod h1:75BAfg2hauQhs3qedfdDZmWAPcFMAvJE5b9rGOMufyw= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/trustelem/zxcvbn v1.0.1 h1:mp4JFtzdDYGj9WYSD3KQSkwwUumWNFzXaAjckaTYpsc= +github.com/trustelem/zxcvbn v1.0.1/go.mod h1:zonUyKeh7sw6psPf/e3DtRqkRyZvAbOfjNz/aO7YQ5s= github.com/uber-go/atomic v1.3.2/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g= github.com/uber/jaeger-client-go v2.15.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= github.com/uber/jaeger-client-go v2.22.1+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= diff --git a/internal/commands/helpers.go b/internal/commands/helpers.go index d5c3990bb..178dde8ef 100644 --- a/internal/commands/helpers.go +++ b/internal/commands/helpers.go @@ -71,7 +71,7 @@ func getProviders() (providers middlewares.Providers, warnings []error, errors [ totpProvider := totp.NewTimeBasedProvider(config.TOTP) - passwordPolicyProvider := middlewares.NewPasswordPolicyProvider(config.PasswordPolicy) + ppolicyProvider := middlewares.NewPasswordPolicyProvider(config.PasswordPolicy) return middlewares.Providers{ Authorizer: authorizer, @@ -83,6 +83,6 @@ func getProviders() (providers middlewares.Providers, warnings []error, errors [ Notifier: notifier, SessionProvider: sessionProvider, TOTP: totpProvider, - PasswordPolicy: passwordPolicyProvider, + PasswordPolicy: ppolicyProvider, }, warnings, errors } diff --git a/internal/configuration/config.template.yml b/internal/configuration/config.template.yml index 1cc564f53..1739cec81 100644 --- a/internal/configuration/config.template.yml +++ b/internal/configuration/config.template.yml @@ -365,6 +365,9 @@ password_policy: zxcvbn: enabled: false + ## Configures the minimum score allowed. + min_score: 3 + ## ## Access Control Configuration ## diff --git a/internal/configuration/schema/password_policy.go b/internal/configuration/schema/password_policy.go index 7b247725a..303618fa9 100644 --- a/internal/configuration/schema/password_policy.go +++ b/internal/configuration/schema/password_policy.go @@ -13,7 +13,8 @@ type PasswordPolicyStandardParams struct { // PasswordPolicyZXCVBNParams represents the configuration related to ZXCVBN parameters of password policy. type PasswordPolicyZXCVBNParams struct { - Enabled bool `koanf:"enabled"` + Enabled bool `koanf:"enabled"` + MinScore int `koanf:"min_score"` } // PasswordPolicyConfiguration represents the configuration related to password policy. @@ -30,6 +31,7 @@ var DefaultPasswordPolicyConfiguration = PasswordPolicyConfiguration{ MaxLength: 0, }, ZXCVBN: PasswordPolicyZXCVBNParams{ - Enabled: false, + Enabled: false, + MinScore: 3, }, } diff --git a/internal/configuration/validator/const.go b/internal/configuration/validator/const.go index 02f48f875..9eb02cac0 100644 --- a/internal/configuration/validator/const.go +++ b/internal/configuration/validator/const.go @@ -244,8 +244,9 @@ const ( ) const ( - errFmtPasswordPolicyMinLengthNotGreaterThanZero = "password_policy: standard: option 'min_length' must be greater than 0 but is configured as %d" - errPasswordPolicyMultipleDefined = "password_policy: only a single password policy mechanism can be specified" + errPasswordPolicyMultipleDefined = "password_policy: only a single password policy mechanism can be specified" + errFmtPasswordPolicyStandardMinLengthNotGreaterThanZero = "password_policy: standard: option 'min_length' must be greater than 0 but is configured as %d" + errFmtPasswordPolicyZXCVBNMinScoreInvalid = "password_policy: zxcvbn: option 'min_score' is invalid: must be between 1 and 4 but it's configured as %d" ) // Error constants. @@ -524,6 +525,7 @@ var ValidKeys = []string{ "password_policy.standard.require_number", "password_policy.standard.require_special", "password_policy.zxcvbn.enabled", + "password_policy.zxcvbn.min_score", } var replacedKeys = map[string]string{ diff --git a/internal/configuration/validator/keys_test.go b/internal/configuration/validator/keys_test.go index 57fd0e06b..23153666b 100644 --- a/internal/configuration/validator/keys_test.go +++ b/internal/configuration/validator/keys_test.go @@ -54,7 +54,7 @@ func TestAllSpecificErrorKeys(t *testing.T) { var uniqueValues []string - // Setup configKeys and uniqueValues expectedErrs. + // Setup configKeys and uniqueValues expected. for key, value := range specificErrorKeys { configKeys = append(configKeys, key) diff --git a/internal/configuration/validator/password_policy.go b/internal/configuration/validator/password_policy.go index 0e7408f6b..0951c4bf5 100644 --- a/internal/configuration/validator/password_policy.go +++ b/internal/configuration/validator/password_policy.go @@ -17,11 +17,20 @@ func ValidatePasswordPolicy(config *schema.PasswordPolicyConfiguration, validato if config.Standard.MinLength == 0 { config.Standard.MinLength = schema.DefaultPasswordPolicyConfiguration.Standard.MinLength } else if config.Standard.MinLength < 0 { - validator.Push(fmt.Errorf(errFmtPasswordPolicyMinLengthNotGreaterThanZero, config.Standard.MinLength)) + validator.Push(fmt.Errorf(errFmtPasswordPolicyStandardMinLengthNotGreaterThanZero, config.Standard.MinLength)) } if config.Standard.MaxLength == 0 { config.Standard.MaxLength = schema.DefaultPasswordPolicyConfiguration.Standard.MaxLength } } + + if config.ZXCVBN.Enabled { + switch { + case config.ZXCVBN.MinScore == 0: + config.ZXCVBN.MinScore = schema.DefaultPasswordPolicyConfiguration.ZXCVBN.MinScore + case config.ZXCVBN.MinScore < 0, config.ZXCVBN.MinScore > 4: + validator.Push(fmt.Errorf(errFmtPasswordPolicyZXCVBNMinScoreInvalid, config.ZXCVBN.MinScore)) + } + } } diff --git a/internal/configuration/validator/password_policy_test.go b/internal/configuration/validator/password_policy_test.go index 5f341672d..3d27f08d6 100644 --- a/internal/configuration/validator/password_policy_test.go +++ b/internal/configuration/validator/password_policy_test.go @@ -33,7 +33,8 @@ func TestValidatePasswordPolicy(t *testing.T) { MinLength: -1, }, ZXCVBN: schema.PasswordPolicyZXCVBNParams{ - Enabled: true, + Enabled: true, + MinScore: 3, }, }, expectedErrs: []string{ @@ -65,7 +66,8 @@ func TestValidatePasswordPolicy(t *testing.T) { }, expected: &schema.PasswordPolicyConfiguration{ ZXCVBN: schema.PasswordPolicyZXCVBNParams{ - Enabled: true, + Enabled: true, + MinScore: 3, }, }, }, @@ -84,6 +86,42 @@ func TestValidatePasswordPolicy(t *testing.T) { }, }, }, + { + desc: "ShouldRaiseErrorsZXCVBNTooLow", + have: &schema.PasswordPolicyConfiguration{ + ZXCVBN: schema.PasswordPolicyZXCVBNParams{ + Enabled: true, + MinScore: -1, + }, + }, + expected: &schema.PasswordPolicyConfiguration{ + ZXCVBN: schema.PasswordPolicyZXCVBNParams{ + Enabled: true, + MinScore: -1, + }, + }, + expectedErrs: []string{ + "password_policy: zxcvbn: option 'min_score' is invalid: must be between 1 and 4 but it's configured as -1", + }, + }, + { + desc: "ShouldRaiseErrorsZXCVBNTooHigh", + have: &schema.PasswordPolicyConfiguration{ + ZXCVBN: schema.PasswordPolicyZXCVBNParams{ + Enabled: true, + MinScore: 5, + }, + }, + expected: &schema.PasswordPolicyConfiguration{ + ZXCVBN: schema.PasswordPolicyZXCVBNParams{ + Enabled: true, + MinScore: 5, + }, + }, + expectedErrs: []string{ + "password_policy: zxcvbn: option 'min_score' is invalid: must be between 1 and 4 but it's configured as 5", + }, + }, } for _, tc := range testCases { @@ -99,6 +137,7 @@ func TestValidatePasswordPolicy(t *testing.T) { assert.Equal(t, tc.expected.Standard.RequireSpecial, tc.have.Standard.RequireSpecial) assert.Equal(t, tc.expected.Standard.RequireUppercase, tc.have.Standard.RequireUppercase) assert.Equal(t, tc.expected.Standard.RequireLowercase, tc.have.Standard.RequireLowercase) + assert.Equal(t, tc.expected.ZXCVBN.MinScore, tc.have.ZXCVBN.MinScore) errs := validator.Errors() require.Len(t, errs, len(tc.expectedErrs)) diff --git a/internal/handlers/types.go b/internal/handlers/types.go index 6b7a4d8da..33e096c25 100644 --- a/internal/handlers/types.go +++ b/internal/handlers/types.go @@ -120,6 +120,7 @@ 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"` diff --git a/internal/middlewares/password_policy.go b/internal/middlewares/password_policy.go index f2894361c..7a3aba798 100644 --- a/internal/middlewares/password_policy.go +++ b/internal/middlewares/password_policy.go @@ -3,44 +3,77 @@ package middlewares import ( "regexp" + "github.com/trustelem/zxcvbn" + "github.com/authelia/authelia/v4/internal/configuration/schema" ) -// NewPasswordPolicyProvider returns a new password policy provider. -func NewPasswordPolicyProvider(config schema.PasswordPolicyConfiguration) (provider PasswordPolicyProvider) { - if !config.Standard.Enabled { - return provider - } - - provider.min, provider.max = config.Standard.MinLength, config.Standard.MaxLength - - if config.Standard.RequireLowercase { - provider.patterns = append(provider.patterns, *regexp.MustCompile(`[a-z]+`)) - } - - if config.Standard.RequireUppercase { - provider.patterns = append(provider.patterns, *regexp.MustCompile(`[A-Z]+`)) - } - - if config.Standard.RequireNumber { - provider.patterns = append(provider.patterns, *regexp.MustCompile(`[0-9]+`)) - } - - if config.Standard.RequireSpecial { - provider.patterns = append(provider.patterns, *regexp.MustCompile(`[^a-zA-Z0-9]+`)) - } - - return provider +// PasswordPolicyProvider represents an implementation of a password policy provider. +type PasswordPolicyProvider interface { + Check(password string) (err error) } -// PasswordPolicyProvider handles password policy checking. -type PasswordPolicyProvider struct { +// NewPasswordPolicyProvider returns a new password policy provider. +func NewPasswordPolicyProvider(config schema.PasswordPolicyConfiguration) (provider PasswordPolicyProvider) { + if !config.Standard.Enabled && !config.ZXCVBN.Enabled { + return &StandardPasswordPolicyProvider{} + } + + if config.Standard.Enabled { + p := &StandardPasswordPolicyProvider{} + + p.min, p.max = config.Standard.MinLength, config.Standard.MaxLength + + if config.Standard.RequireLowercase { + p.patterns = append(p.patterns, *regexp.MustCompile(`[a-z]+`)) + } + + if config.Standard.RequireUppercase { + p.patterns = append(p.patterns, *regexp.MustCompile(`[A-Z]+`)) + } + + if config.Standard.RequireNumber { + p.patterns = append(p.patterns, *regexp.MustCompile(`[0-9]+`)) + } + + if config.Standard.RequireSpecial { + p.patterns = append(p.patterns, *regexp.MustCompile(`[^a-zA-Z0-9]+`)) + } + + return p + } + + if config.ZXCVBN.Enabled { + return &ZXCVBNPasswordPolicyProvider{minScore: config.ZXCVBN.MinScore} + } + + return &StandardPasswordPolicyProvider{} +} + +// ZXCVBNPasswordPolicyProvider handles zxcvbn password policy checking. +type ZXCVBNPasswordPolicyProvider struct { + minScore int +} + +// Check checks the password against the policy. +func (p ZXCVBNPasswordPolicyProvider) Check(password string) (err error) { + result := zxcvbn.PasswordStrength(password, nil) + + if result.Score < p.minScore { + return errPasswordPolicyNoMet + } + + return nil +} + +// StandardPasswordPolicyProvider handles standard password policy checking. +type StandardPasswordPolicyProvider struct { patterns []regexp.Regexp min, max int } // Check checks the password against the policy. -func (p PasswordPolicyProvider) Check(password string) (err error) { +func (p StandardPasswordPolicyProvider) Check(password string) (err error) { patterns := len(p.patterns) if (p.min > 0 && len(password) < p.min) || (p.max > 0 && len(password) > p.max) { diff --git a/internal/middlewares/password_policy_test.go b/internal/middlewares/password_policy_test.go index 8828f63a9..0ce9ff899 100644 --- a/internal/middlewares/password_policy_test.go +++ b/internal/middlewares/password_policy_test.go @@ -19,42 +19,42 @@ func TestNewPasswordPolicyProvider(t *testing.T) { { desc: "ShouldReturnUnconfiguredProvider", have: schema.PasswordPolicyConfiguration{}, - expected: PasswordPolicyProvider{}, + expected: &StandardPasswordPolicyProvider{}, }, { - desc: "ShouldReturnUnconfiguredProviderWhenZxcvbn", - have: schema.PasswordPolicyConfiguration{ZXCVBN: schema.PasswordPolicyZXCVBNParams{Enabled: true}}, - expected: PasswordPolicyProvider{}, + desc: "ShouldReturnProviderWhenZxcvbn", + have: schema.PasswordPolicyConfiguration{ZXCVBN: schema.PasswordPolicyZXCVBNParams{Enabled: true, MinScore: 10}}, + expected: &ZXCVBNPasswordPolicyProvider{minScore: 10}, }, { desc: "ShouldReturnConfiguredProviderWithMin", have: schema.PasswordPolicyConfiguration{Standard: schema.PasswordPolicyStandardParams{Enabled: true, MinLength: 8}}, - expected: PasswordPolicyProvider{min: 8}, + expected: &StandardPasswordPolicyProvider{min: 8}, }, { desc: "ShouldReturnConfiguredProviderWitHMinMax", have: schema.PasswordPolicyConfiguration{Standard: schema.PasswordPolicyStandardParams{Enabled: true, MinLength: 8, MaxLength: 100}}, - expected: PasswordPolicyProvider{min: 8, max: 100}, + expected: &StandardPasswordPolicyProvider{min: 8, max: 100}, }, { desc: "ShouldReturnConfiguredProviderWithMinLowercase", have: schema.PasswordPolicyConfiguration{Standard: schema.PasswordPolicyStandardParams{Enabled: true, MinLength: 8, RequireLowercase: true}}, - expected: PasswordPolicyProvider{min: 8, patterns: []regexp.Regexp{*regexp.MustCompile(`[a-z]+`)}}, + expected: &StandardPasswordPolicyProvider{min: 8, patterns: []regexp.Regexp{*regexp.MustCompile(`[a-z]+`)}}, }, { desc: "ShouldReturnConfiguredProviderWithMinLowercaseUppercase", have: schema.PasswordPolicyConfiguration{Standard: schema.PasswordPolicyStandardParams{Enabled: true, MinLength: 8, RequireLowercase: true, RequireUppercase: true}}, - expected: PasswordPolicyProvider{min: 8, patterns: []regexp.Regexp{*regexp.MustCompile(`[a-z]+`), *regexp.MustCompile(`[A-Z]+`)}}, + expected: &StandardPasswordPolicyProvider{min: 8, patterns: []regexp.Regexp{*regexp.MustCompile(`[a-z]+`), *regexp.MustCompile(`[A-Z]+`)}}, }, { desc: "ShouldReturnConfiguredProviderWithMinLowercaseUppercaseNumber", have: schema.PasswordPolicyConfiguration{Standard: schema.PasswordPolicyStandardParams{Enabled: true, MinLength: 8, RequireLowercase: true, RequireUppercase: true, RequireNumber: true}}, - expected: PasswordPolicyProvider{min: 8, patterns: []regexp.Regexp{*regexp.MustCompile(`[a-z]+`), *regexp.MustCompile(`[A-Z]+`), *regexp.MustCompile(`[0-9]+`)}}, + expected: &StandardPasswordPolicyProvider{min: 8, patterns: []regexp.Regexp{*regexp.MustCompile(`[a-z]+`), *regexp.MustCompile(`[A-Z]+`), *regexp.MustCompile(`[0-9]+`)}}, }, { desc: "ShouldReturnConfiguredProviderWithMinLowercaseUppercaseSpecial", have: schema.PasswordPolicyConfiguration{Standard: schema.PasswordPolicyStandardParams{Enabled: true, MinLength: 8, RequireLowercase: true, RequireUppercase: true, RequireSpecial: true}}, - expected: PasswordPolicyProvider{min: 8, patterns: []regexp.Regexp{*regexp.MustCompile(`[a-z]+`), *regexp.MustCompile(`[A-Z]+`), *regexp.MustCompile(`[^a-zA-Z0-9]+`)}}, + expected: &StandardPasswordPolicyProvider{min: 8, patterns: []regexp.Regexp{*regexp.MustCompile(`[a-z]+`), *regexp.MustCompile(`[A-Z]+`), *regexp.MustCompile(`[^a-zA-Z0-9]+`)}}, }, } diff --git a/web/src/components/PasswordMeter.test.tsx b/web/src/components/PasswordMeter.test.tsx index cc77ab77e..790ddd6f3 100644 --- a/web/src/components/PasswordMeter.test.tsx +++ b/web/src/components/PasswordMeter.test.tsx @@ -12,6 +12,7 @@ it("renders without crashing", () => { policy={{ max_length: 0, min_length: 4, + min_score: 0, require_lowercase: false, require_number: false, require_special: false, @@ -29,6 +30,7 @@ it("renders adjusted height without crashing", () => { policy={{ max_length: 0, min_length: 4, + min_score: 0, require_lowercase: false, require_number: false, require_special: false, diff --git a/web/src/models/PasswordPolicy.ts b/web/src/models/PasswordPolicy.ts index 454a90ffa..32bec62f3 100644 --- a/web/src/models/PasswordPolicy.ts +++ b/web/src/models/PasswordPolicy.ts @@ -8,6 +8,7 @@ export interface PasswordPolicyConfiguration { mode: PasswordPolicyMode; min_length: number; max_length: number; + min_score: number; require_uppercase: boolean; require_lowercase: boolean; require_number: boolean; diff --git a/web/src/services/PasswordPolicyConfiguration.ts b/web/src/services/PasswordPolicyConfiguration.ts index a816d8b93..6b979c74d 100644 --- a/web/src/services/PasswordPolicyConfiguration.ts +++ b/web/src/services/PasswordPolicyConfiguration.ts @@ -6,6 +6,7 @@ interface PasswordPolicyConfigurationPayload { mode: ModePasswordPolicy; min_length: number; max_length: number; + min_score: number; require_uppercase: boolean; require_lowercase: boolean; require_number: boolean; diff --git a/web/src/views/ResetPassword/ResetPasswordStep2.tsx b/web/src/views/ResetPassword/ResetPasswordStep2.tsx index 809938a3d..bf1e61463 100644 --- a/web/src/views/ResetPassword/ResetPasswordStep2.tsx +++ b/web/src/views/ResetPassword/ResetPasswordStep2.tsx @@ -32,6 +32,7 @@ const ResetPasswordStep2 = function () { const [pPolicy, setPPolicy] = useState({ max_length: 0, min_length: 8, + min_score: 0, require_lowercase: false, require_number: false, require_special: false,