feat(server): zxcvbn password policy server side (#3151)
This is so the zxcvbn ppolicy is checked on the server.pull/3204/head
parent
c5cb36c526
commit
92aba8eb0b
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
##
|
##
|
||||||
|
|
|
@ -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
3
go.mod
|
@ -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
6
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 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=
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
##
|
##
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -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{
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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"`
|
||||||
|
|
|
@ -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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// PasswordPolicyProvider represents an implementation of a password policy provider.
|
||||||
|
type PasswordPolicyProvider interface {
|
||||||
|
Check(password string) (err error)
|
||||||
|
}
|
||||||
|
|
||||||
// NewPasswordPolicyProvider returns a new password policy provider.
|
// NewPasswordPolicyProvider returns a new password policy provider.
|
||||||
func NewPasswordPolicyProvider(config schema.PasswordPolicyConfiguration) (provider PasswordPolicyProvider) {
|
func NewPasswordPolicyProvider(config schema.PasswordPolicyConfiguration) (provider PasswordPolicyProvider) {
|
||||||
if !config.Standard.Enabled {
|
if !config.Standard.Enabled && !config.ZXCVBN.Enabled {
|
||||||
return provider
|
return &StandardPasswordPolicyProvider{}
|
||||||
}
|
}
|
||||||
|
|
||||||
provider.min, provider.max = config.Standard.MinLength, config.Standard.MaxLength
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.ZXCVBN.Enabled {
|
||||||
|
return &ZXCVBNPasswordPolicyProvider{minScore: config.ZXCVBN.MinScore}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &StandardPasswordPolicyProvider{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// PasswordPolicyProvider handles password policy checking.
|
// ZXCVBNPasswordPolicyProvider handles zxcvbn password policy checking.
|
||||||
type PasswordPolicyProvider struct {
|
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) {
|
||||||
|
|
|
@ -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]+`)}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in New Issue