feat(server): zxcvbn password policy server side (#3151)

This is so the zxcvbn ppolicy is checked on the server.
pull/3204/head
James Elliott 2022-04-15 19:30:51 +10:00 committed by GitHub
parent c5cb36c526
commit 92aba8eb0b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 194 additions and 57 deletions

View File

@ -730,6 +730,9 @@ components:
max_length: max_length:
type: integer type: integer
description: The maximum password length when using the standard mode. 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: require_uppercase:
type: boolean type: boolean
description: If uppercase characters are required when using the standard mode. description: If uppercase characters are required when using the standard mode.

View File

@ -365,6 +365,9 @@ password_policy:
zxcvbn: zxcvbn:
enabled: false enabled: false
## Configures the minimum score allowed.
min_score: 3
## ##
## Access Control Configuration ## Access Control Configuration
## ##

View File

@ -23,6 +23,7 @@ password_policy:
require_special: false require_special: false
zxcvbn: zxcvbn:
enabled: false enabled: false
min_score: 3
``` ```
## Options ## Options
@ -39,8 +40,10 @@ This section allows you to enable standard security policies.
#### enabled #### enabled
<div markdown="1"> <div markdown="1">
type: bool type: boolean
{: .label .label-config .label-purple } {: .label .label-config .label-purple }
default: false
{: .label .label-config .label-blue }
required: no required: no
{: .label .label-config .label-green } {: .label .label-config .label-green }
</div> </div>
@ -73,8 +76,10 @@ Determines the maximum allowed password length.
#### require_uppercase #### require_uppercase
<div markdown="1"> <div markdown="1">
type: bool type: boolean
{: .label .label-config .label-purple } {: .label .label-config .label-purple }
default: false
{: .label .label-config .label-blue }
required: no required: no
{: .label .label-config .label-green } {: .label .label-config .label-green }
</div> </div>
@ -83,8 +88,10 @@ Indicates that at least one UPPERCASE letter must be provided as part of the pas
#### require_lowercase #### require_lowercase
<div markdown="1"> <div markdown="1">
type: bool type: boolean
{: .label .label-config .label-purple } {: .label .label-config .label-purple }
default: false
{: .label .label-config .label-blue }
required: no required: no
{: .label .label-config .label-green } {: .label .label-config .label-green }
</div> </div>
@ -93,8 +100,10 @@ Indicates that at least one lowercase letter must be provided as part of the pas
#### require_number #### require_number
<div markdown="1"> <div markdown="1">
type: bool type: boolean
{: .label .label-config .label-purple } {: .label .label-config .label-purple }
default: false
{: .label .label-config .label-blue }
required: no required: no
{: .label .label-config .label-green } {: .label .label-config .label-green }
</div> </div>
@ -103,8 +112,10 @@ Indicates that at least one number must be provided as part of the password.
#### require_special #### require_special
<div markdown="1"> <div markdown="1">
type: bool type: boolean
{: .label .label-config .label-purple } {: .label .label-config .label-purple }
default: false
{: .label .label-config .label-blue }
required: no required: no
{: .label .label-config .label-green } {: .label .label-config .label-green }
</div> </div>
@ -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). 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 #### enabled
<div markdown="1"> <div markdown="1">
type: bool type: boolean
{: .label .label-config .label-purple } {: .label .label-config .label-purple }
default: false
{: .label .label-config .label-blue }
required: no required: no
{: .label .label-config .label-green } {: .label .label-config .label-green }
</div> </div>
@ -130,4 +140,22 @@ _**Important Note:** only one password policy can be applied at a time._
Enables zxcvbn password policy. Enables zxcvbn password policy.
#### min_score
<div markdown="1">
type: integer
{: .label .label-config .label-purple }
default: 3
{: .label .label-config .label-blue }
required: no
{: .label .label-config .label-green }
</div>
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.

3
go.mod
View File

@ -31,6 +31,7 @@ require (
github.com/spf13/cobra v1.4.0 github.com/spf13/cobra v1.4.0
github.com/spf13/pflag v1.0.5 github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.7.1 github.com/stretchr/testify v1.7.1
github.com/trustelem/zxcvbn v1.0.1
github.com/valyala/fasthttp v1.35.0 github.com/valyala/fasthttp v1.35.0
golang.org/x/text v0.3.7 golang.org/x/text v0.3.7
gopkg.in/square/go-jose.v2 v2.6.0 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/davecgh/go-spew v1.1.1 // indirect
github.com/dgraph-io/ristretto v0.1.0 // indirect github.com/dgraph-io/ristretto v0.1.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // 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/dustin/go-humanize v1.0.0 // indirect
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect
github.com/fsnotify/fsnotify v1.5.1 // 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/cast v1.4.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/subosito/gotenv v1.2.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/tinylib/msgp v1.1.6 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/x448/float16 v0.8.4 // indirect github.com/x448/float16 v0.8.4 // indirect

6
go.sum
View File

@ -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 h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 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/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/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/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= 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.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 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= 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/gjson v1.11.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= 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/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-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/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-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.15.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk=
github.com/uber/jaeger-client-go v2.22.1+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= github.com/uber/jaeger-client-go v2.22.1+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk=

View File

@ -71,7 +71,7 @@ func getProviders() (providers middlewares.Providers, warnings []error, errors [
totpProvider := totp.NewTimeBasedProvider(config.TOTP) totpProvider := totp.NewTimeBasedProvider(config.TOTP)
passwordPolicyProvider := middlewares.NewPasswordPolicyProvider(config.PasswordPolicy) ppolicyProvider := middlewares.NewPasswordPolicyProvider(config.PasswordPolicy)
return middlewares.Providers{ return middlewares.Providers{
Authorizer: authorizer, Authorizer: authorizer,
@ -83,6 +83,6 @@ func getProviders() (providers middlewares.Providers, warnings []error, errors [
Notifier: notifier, Notifier: notifier,
SessionProvider: sessionProvider, SessionProvider: sessionProvider,
TOTP: totpProvider, TOTP: totpProvider,
PasswordPolicy: passwordPolicyProvider, PasswordPolicy: ppolicyProvider,
}, warnings, errors }, warnings, errors
} }

View File

@ -365,6 +365,9 @@ password_policy:
zxcvbn: zxcvbn:
enabled: false enabled: false
## Configures the minimum score allowed.
min_score: 3
## ##
## Access Control Configuration ## Access Control Configuration
## ##

View File

@ -14,6 +14,7 @@ type PasswordPolicyStandardParams struct {
// PasswordPolicyZXCVBNParams represents the configuration related to ZXCVBN parameters of password policy. // PasswordPolicyZXCVBNParams represents the configuration related to ZXCVBN parameters of password policy.
type PasswordPolicyZXCVBNParams struct { type PasswordPolicyZXCVBNParams struct {
Enabled bool `koanf:"enabled"` Enabled bool `koanf:"enabled"`
MinScore int `koanf:"min_score"`
} }
// PasswordPolicyConfiguration represents the configuration related to password policy. // PasswordPolicyConfiguration represents the configuration related to password policy.
@ -31,5 +32,6 @@ var DefaultPasswordPolicyConfiguration = PasswordPolicyConfiguration{
}, },
ZXCVBN: PasswordPolicyZXCVBNParams{ ZXCVBN: PasswordPolicyZXCVBNParams{
Enabled: false, Enabled: false,
MinScore: 3,
}, },
} }

View File

@ -244,8 +244,9 @@ const (
) )
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. // Error constants.
@ -524,6 +525,7 @@ var ValidKeys = []string{
"password_policy.standard.require_number", "password_policy.standard.require_number",
"password_policy.standard.require_special", "password_policy.standard.require_special",
"password_policy.zxcvbn.enabled", "password_policy.zxcvbn.enabled",
"password_policy.zxcvbn.min_score",
} }
var replacedKeys = map[string]string{ var replacedKeys = map[string]string{

View File

@ -54,7 +54,7 @@ func TestAllSpecificErrorKeys(t *testing.T) {
var uniqueValues []string var uniqueValues []string
// Setup configKeys and uniqueValues expectedErrs. // Setup configKeys and uniqueValues expected.
for key, value := range specificErrorKeys { for key, value := range specificErrorKeys {
configKeys = append(configKeys, key) configKeys = append(configKeys, key)

View File

@ -17,11 +17,20 @@ func ValidatePasswordPolicy(config *schema.PasswordPolicyConfiguration, validato
if config.Standard.MinLength == 0 { if config.Standard.MinLength == 0 {
config.Standard.MinLength = schema.DefaultPasswordPolicyConfiguration.Standard.MinLength config.Standard.MinLength = schema.DefaultPasswordPolicyConfiguration.Standard.MinLength
} else if config.Standard.MinLength < 0 { } 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 { if config.Standard.MaxLength == 0 {
config.Standard.MaxLength = schema.DefaultPasswordPolicyConfiguration.Standard.MaxLength 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))
}
}
} }

View File

@ -34,6 +34,7 @@ func TestValidatePasswordPolicy(t *testing.T) {
}, },
ZXCVBN: schema.PasswordPolicyZXCVBNParams{ ZXCVBN: schema.PasswordPolicyZXCVBNParams{
Enabled: true, Enabled: true,
MinScore: 3,
}, },
}, },
expectedErrs: []string{ expectedErrs: []string{
@ -66,6 +67,7 @@ func TestValidatePasswordPolicy(t *testing.T) {
expected: &schema.PasswordPolicyConfiguration{ expected: &schema.PasswordPolicyConfiguration{
ZXCVBN: schema.PasswordPolicyZXCVBNParams{ 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 { 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.RequireSpecial, tc.have.Standard.RequireSpecial)
assert.Equal(t, tc.expected.Standard.RequireUppercase, tc.have.Standard.RequireUppercase) 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.Standard.RequireLowercase, tc.have.Standard.RequireLowercase)
assert.Equal(t, tc.expected.ZXCVBN.MinScore, tc.have.ZXCVBN.MinScore)
errs := validator.Errors() errs := validator.Errors()
require.Len(t, errs, len(tc.expectedErrs)) require.Len(t, errs, len(tc.expectedErrs))

View File

@ -120,6 +120,7 @@ type PassworPolicyBody struct {
Mode string `json:"mode"` Mode string `json:"mode"`
MinLength int `json:"min_length"` MinLength int `json:"min_length"`
MaxLength int `json:"max_length"` MaxLength int `json:"max_length"`
MinScore int `json:"min_score"`
RequireUppercase bool `json:"require_uppercase"` RequireUppercase bool `json:"require_uppercase"`
RequireLowercase bool `json:"require_lowercase"` RequireLowercase bool `json:"require_lowercase"`
RequireNumber bool `json:"require_number"` RequireNumber bool `json:"require_number"`

View File

@ -3,44 +3,77 @@ package middlewares
import ( import (
"regexp" "regexp"
"github.com/trustelem/zxcvbn"
"github.com/authelia/authelia/v4/internal/configuration/schema" "github.com/authelia/authelia/v4/internal/configuration/schema"
) )
// NewPasswordPolicyProvider returns a new password policy provider. // PasswordPolicyProvider represents an implementation of a password policy provider.
func NewPasswordPolicyProvider(config schema.PasswordPolicyConfiguration) (provider PasswordPolicyProvider) { type PasswordPolicyProvider interface {
if !config.Standard.Enabled { Check(password string) (err error)
return provider
} }
provider.min, provider.max = config.Standard.MinLength, config.Standard.MaxLength // 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 { if config.Standard.RequireLowercase {
provider.patterns = append(provider.patterns, *regexp.MustCompile(`[a-z]+`)) p.patterns = append(p.patterns, *regexp.MustCompile(`[a-z]+`))
} }
if config.Standard.RequireUppercase { if config.Standard.RequireUppercase {
provider.patterns = append(provider.patterns, *regexp.MustCompile(`[A-Z]+`)) p.patterns = append(p.patterns, *regexp.MustCompile(`[A-Z]+`))
} }
if config.Standard.RequireNumber { if config.Standard.RequireNumber {
provider.patterns = append(provider.patterns, *regexp.MustCompile(`[0-9]+`)) p.patterns = append(p.patterns, *regexp.MustCompile(`[0-9]+`))
} }
if config.Standard.RequireSpecial { if config.Standard.RequireSpecial {
provider.patterns = append(provider.patterns, *regexp.MustCompile(`[^a-zA-Z0-9]+`)) p.patterns = append(p.patterns, *regexp.MustCompile(`[^a-zA-Z0-9]+`))
} }
return provider return p
} }
// PasswordPolicyProvider handles password policy checking. if config.ZXCVBN.Enabled {
type PasswordPolicyProvider struct { 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 patterns []regexp.Regexp
min, max int min, max int
} }
// Check checks the password against the policy. // 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) patterns := len(p.patterns)
if (p.min > 0 && len(password) < p.min) || (p.max > 0 && len(password) > p.max) { if (p.min > 0 && len(password) < p.min) || (p.max > 0 && len(password) > p.max) {

View File

@ -19,42 +19,42 @@ func TestNewPasswordPolicyProvider(t *testing.T) {
{ {
desc: "ShouldReturnUnconfiguredProvider", desc: "ShouldReturnUnconfiguredProvider",
have: schema.PasswordPolicyConfiguration{}, have: schema.PasswordPolicyConfiguration{},
expected: PasswordPolicyProvider{}, expected: &StandardPasswordPolicyProvider{},
}, },
{ {
desc: "ShouldReturnUnconfiguredProviderWhenZxcvbn", desc: "ShouldReturnProviderWhenZxcvbn",
have: schema.PasswordPolicyConfiguration{ZXCVBN: schema.PasswordPolicyZXCVBNParams{Enabled: true}}, have: schema.PasswordPolicyConfiguration{ZXCVBN: schema.PasswordPolicyZXCVBNParams{Enabled: true, MinScore: 10}},
expected: PasswordPolicyProvider{}, expected: &ZXCVBNPasswordPolicyProvider{minScore: 10},
}, },
{ {
desc: "ShouldReturnConfiguredProviderWithMin", desc: "ShouldReturnConfiguredProviderWithMin",
have: schema.PasswordPolicyConfiguration{Standard: schema.PasswordPolicyStandardParams{Enabled: true, MinLength: 8}}, have: schema.PasswordPolicyConfiguration{Standard: schema.PasswordPolicyStandardParams{Enabled: true, MinLength: 8}},
expected: PasswordPolicyProvider{min: 8}, expected: &StandardPasswordPolicyProvider{min: 8},
}, },
{ {
desc: "ShouldReturnConfiguredProviderWitHMinMax", desc: "ShouldReturnConfiguredProviderWitHMinMax",
have: schema.PasswordPolicyConfiguration{Standard: schema.PasswordPolicyStandardParams{Enabled: true, MinLength: 8, MaxLength: 100}}, 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", desc: "ShouldReturnConfiguredProviderWithMinLowercase",
have: schema.PasswordPolicyConfiguration{Standard: schema.PasswordPolicyStandardParams{Enabled: true, MinLength: 8, RequireLowercase: true}}, 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", desc: "ShouldReturnConfiguredProviderWithMinLowercaseUppercase",
have: schema.PasswordPolicyConfiguration{Standard: schema.PasswordPolicyStandardParams{Enabled: true, MinLength: 8, RequireLowercase: true, RequireUppercase: true}}, 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", desc: "ShouldReturnConfiguredProviderWithMinLowercaseUppercaseNumber",
have: schema.PasswordPolicyConfiguration{Standard: schema.PasswordPolicyStandardParams{Enabled: true, MinLength: 8, RequireLowercase: true, RequireUppercase: true, RequireNumber: true}}, 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", desc: "ShouldReturnConfiguredProviderWithMinLowercaseUppercaseSpecial",
have: schema.PasswordPolicyConfiguration{Standard: schema.PasswordPolicyStandardParams{Enabled: true, MinLength: 8, RequireLowercase: true, RequireUppercase: true, RequireSpecial: true}}, 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]+`)}},
}, },
} }

View File

@ -12,6 +12,7 @@ it("renders without crashing", () => {
policy={{ policy={{
max_length: 0, max_length: 0,
min_length: 4, min_length: 4,
min_score: 0,
require_lowercase: false, require_lowercase: false,
require_number: false, require_number: false,
require_special: false, require_special: false,
@ -29,6 +30,7 @@ it("renders adjusted height without crashing", () => {
policy={{ policy={{
max_length: 0, max_length: 0,
min_length: 4, min_length: 4,
min_score: 0,
require_lowercase: false, require_lowercase: false,
require_number: false, require_number: false,
require_special: false, require_special: false,

View File

@ -8,6 +8,7 @@ export interface PasswordPolicyConfiguration {
mode: PasswordPolicyMode; mode: PasswordPolicyMode;
min_length: number; min_length: number;
max_length: number; max_length: number;
min_score: number;
require_uppercase: boolean; require_uppercase: boolean;
require_lowercase: boolean; require_lowercase: boolean;
require_number: boolean; require_number: boolean;

View File

@ -6,6 +6,7 @@ interface PasswordPolicyConfigurationPayload {
mode: ModePasswordPolicy; mode: ModePasswordPolicy;
min_length: number; min_length: number;
max_length: number; max_length: number;
min_score: number;
require_uppercase: boolean; require_uppercase: boolean;
require_lowercase: boolean; require_lowercase: boolean;
require_number: boolean; require_number: boolean;

View File

@ -32,6 +32,7 @@ const ResetPasswordStep2 = function () {
const [pPolicy, setPPolicy] = useState<PasswordPolicyConfiguration>({ const [pPolicy, setPPolicy] = useState<PasswordPolicyConfiguration>({
max_length: 0, max_length: 0,
min_length: 8, min_length: 8,
min_score: 0,
require_lowercase: false, require_lowercase: false,
require_number: false, require_number: false,
require_special: false, require_special: false,