Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: limit maximum password length #3781

Open
wants to merge 18 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .schemastore/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1524,6 +1524,13 @@
"default": 8,
"minimum": 6
},
"max_password_length": {
"title": "Maximum Password Length",
"description": "Defines the maximum length of the password.",
"type": "integer",
"default": 1024,
"minimum": 20
Comment on lines +1531 to +1532
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: discuss default values
Should it be limited to max 72 characters to account for bcrypt hasher limitations?

// Bcrypt truncates the password to the first 72 bytes, following the OpenBSD implementation,
// so if password is longer than 72 bytes, function returns an error
// See https://en.wikipedia.org/wiki/Bcrypt#User_input
if len(password) > 72 {
return schema.NewPasswordPolicyViolationError(
"#/password",
text.NewErrorValidationPasswordMaxLength(72, len(password)),
)
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: update Breaking changes section with new value

},
"identifier_similarity_check_enabled": {
"title": "Enable password-identifier similarity check",
"description": "If set to false the password validation does not check for similarity between the password and the user identifier.",
Expand Down
3 changes: 3 additions & 0 deletions driver/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ const (
ViperKeyPasswordHaveIBeenPwnedEnabled = "selfservice.methods.password.config.haveibeenpwned_enabled"
ViperKeyPasswordMaxBreaches = "selfservice.methods.password.config.max_breaches"
ViperKeyPasswordMinLength = "selfservice.methods.password.config.min_password_length"
ViperKeyPasswordMaxLength = "selfservice.methods.password.config.max_password_length"
ViperKeyPasswordIdentifierSimilarityCheckEnabled = "selfservice.methods.password.config.identifier_similarity_check_enabled"
ViperKeyIgnoreNetworkErrors = "selfservice.methods.password.config.ignore_network_errors"
ViperKeyTOTPIssuer = "selfservice.methods.totp.config.issuer"
Expand Down Expand Up @@ -249,6 +250,7 @@ type (
MaxBreaches uint `json:"max_breaches"`
IgnoreNetworkErrors bool `json:"ignore_network_errors"`
MinPasswordLength uint `json:"min_password_length"`
MaxPasswordLength uint `json:"max_password_length"`
IdentifierSimilarityCheckEnabled bool `json:"identifier_similarity_check_enabled"`
}
Schemas []Schema
Expand Down Expand Up @@ -1425,6 +1427,7 @@ func (p *Config) PasswordPolicyConfig(ctx context.Context) *PasswordPolicy {
MaxBreaches: uint(p.GetProvider(ctx).Int(ViperKeyPasswordMaxBreaches)),
IgnoreNetworkErrors: p.GetProvider(ctx).BoolF(ViperKeyIgnoreNetworkErrors, true),
MinPasswordLength: uint(p.GetProvider(ctx).IntF(ViperKeyPasswordMinLength, 8)),
MaxPasswordLength: uint(p.GetProvider(ctx).IntF(ViperKeyPasswordMaxLength, 20)),
IdentifierSimilarityCheckEnabled: p.GetProvider(ctx).BoolF(ViperKeyPasswordIdentifierSimilarityCheckEnabled, true),
}
}
Expand Down
2 changes: 1 addition & 1 deletion driver/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ func TestViperProvider(t *testing.T) {
config string
enabled bool
}{
{id: "password", enabled: true, config: `{"haveibeenpwned_host":"api.pwnedpasswords.com","haveibeenpwned_enabled":true,"ignore_network_errors":true,"max_breaches":0,"min_password_length":8,"identifier_similarity_check_enabled":true}`},
{id: "password", enabled: true, config: `{"haveibeenpwned_host":"api.pwnedpasswords.com","haveibeenpwned_enabled":true,"ignore_network_errors":true,"max_breaches":0,"min_password_length":8,"max_password_length":1024,"identifier_similarity_check_enabled":true}`},
{id: "oidc", enabled: true, config: `{"providers":[{"client_id":"a","client_secret":"b","id":"github","provider":"github","mapper_url":"http://test.kratos.ory.sh/default-identity.schema.json"}]}`},
{id: "totp", enabled: true, config: `{"issuer":"issuer.ory.sh"}`},
} {
Expand Down
7 changes: 7 additions & 0 deletions embedx/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1524,6 +1524,13 @@
"default": 8,
"minimum": 6
},
"max_password_length": {
"title": "Maximum Password Length",
"description": "Defines the maximum length of the password.",
"type": "integer",
"default": 1024,
"minimum": 20
},
"identifier_similarity_check_enabled": {
"title": "Enable password-identifier similarity check",
"description": "If set to false the password validation does not check for similarity between the password and the user identifier.",
Expand Down
1 change: 1 addition & 0 deletions internal/client-go/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
Expand Down
4 changes: 4 additions & 0 deletions internal/client-go/model_ui_node_input_attributes.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions internal/httpclient/model_ui_node_input_attributes.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@
{
"attributes": {
"autocomplete": "new-password",
"minlength": 8,
"maxlength": 1024,
"disabled": false,
"name": "password",
"node_type": "input",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@
{
"attributes": {
"autocomplete": "new-password",
"minlength": 8,
"maxlength": 1024,
"disabled": false,
"name": "password",
"node_type": "input",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@
"type": "password",
"required": true,
"autocomplete": "new-password",
"minlength": 8,
"maxlength": 1024,
"disabled": false,
"node_type": "input"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@
{
"attributes": {
"autocomplete": "new-password",
"minlength": 8,
"maxlength": 1024,
"disabled": false,
"name": "password",
"node_type": "input",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@
"type": "password",
"required": true,
"autocomplete": "new-password",
"minlength": 8,
"maxlength": 1024,
"disabled": false,
"node_type": "input"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@
"type": "password",
"required": true,
"autocomplete": "new-password",
"minlength": 8,
"maxlength": 1024,
"disabled": false,
"node_type": "input"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@
"type": "password",
"required": true,
"autocomplete": "new-password",
"minlength": 8,
"maxlength": 1024,
"disabled": false,
"node_type": "input"
},
Expand Down
8 changes: 7 additions & 1 deletion selfservice/strategy/password/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,12 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow,
return nil, s.handleLoginError(w, r, f, &p, err)
}

// Reduce possibility of DDoS attacks with long password
if len([]byte(p.Password)) > int(s.d.Config().PasswordPolicyConfig(r.Context()).MaxPasswordLength) {
time.Sleep(x.RandomDelay(s.d.Config().HasherArgon2(r.Context()).ExpectedDuration, s.d.Config().HasherArgon2(r.Context()).ExpectedDeviation))
return nil, s.handleLoginError(w, r, f, &p, errors.WithStack(schema.NewInvalidCredentialsError()))
}

i, c, err := s.d.PrivilegedIdentityPool().FindByCredentialsIdentifier(r.Context(), s.ID(), stringsx.Coalesce(p.Identifier, p.LegacyIdentifier))
if err != nil {
time.Sleep(x.RandomDelay(s.d.Config().HasherArgon2(r.Context()).ExpectedDuration, s.d.Config().HasherArgon2(r.Context()).ExpectedDeviation))
Expand Down Expand Up @@ -161,7 +167,7 @@ func (s *Strategy) PopulateLoginMethod(r *http.Request, requestedAAL identity.Au
}

sr.UI.SetCSRF(s.d.GenerateCSRFToken(r))
sr.UI.SetNode(NewPasswordNode("password", node.InputAttributeAutocompleteCurrentPassword))
sr.UI.SetNode(NewPasswordNode("password", node.InputAttributeAutocompleteCurrentPassword, s.d.Config().PasswordPolicyConfig(r.Context())))
sr.UI.GetNodes().Append(node.NewInputField("method", "password", node.PasswordGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoLogin()))

return nil
Expand Down
5 changes: 4 additions & 1 deletion selfservice/strategy/password/nodes.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,18 @@
package password

import (
"github.com/ory/kratos/driver/config"
"github.com/ory/kratos/text"
"github.com/ory/kratos/ui/node"
)

func NewPasswordNode(name string, autocomplete node.UiNodeInputAttributeAutocomplete) *node.Node {
func NewPasswordNode(name string, autocomplete node.UiNodeInputAttributeAutocomplete, passwordPolicy *config.PasswordPolicy) *node.Node {
return node.NewInputField(name, nil, node.PasswordGroup,
node.InputAttributeTypePassword,
node.WithRequiredInputAttribute,
node.WithInputAttributes(func(a *node.InputAttributes) {
a.MinLength = passwordPolicy.MinPasswordLength
a.MaxLength = passwordPolicy.MaxPasswordLength
a.Autocomplete = autocomplete
})).
WithMetaLabel(text.NewInfoNodeInputPassword())
Expand Down
2 changes: 1 addition & 1 deletion selfservice/strategy/password/registration.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ func (s *Strategy) PopulateRegistrationMethod(r *http.Request, f *registration.F
}

f.UI.SetCSRF(s.d.GenerateCSRFToken(r))
f.UI.Nodes.Upsert(NewPasswordNode("password", node.InputAttributeAutocompleteNewPassword))
f.UI.Nodes.Upsert(NewPasswordNode("password", node.InputAttributeAutocompleteNewPassword, s.d.Config().PasswordPolicyConfig(r.Context())))
f.UI.Nodes.Append(node.NewInputField("method", "password", node.PasswordGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoRegistration()))

return nil
Expand Down
2 changes: 1 addition & 1 deletion selfservice/strategy/password/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ func (s *Strategy) continueSettingsFlow(

func (s *Strategy) PopulateSettingsMethod(r *http.Request, _ *identity.Identity, f *settings.Flow) error {
f.UI.SetCSRF(s.d.GenerateCSRFToken(r))
f.UI.Nodes.Upsert(NewPasswordNode("password", node.InputAttributeAutocompleteNewPassword).WithMetaLabel(text.NewInfoNodeInputPassword()))
f.UI.Nodes.Upsert(NewPasswordNode("password", node.InputAttributeAutocompleteNewPassword, s.d.Config().PasswordPolicyConfig(r.Context())).WithMetaLabel(text.NewInfoNodeInputPassword()))
f.UI.Nodes.Append(node.NewInputField("method", "password", node.PasswordGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoNodeLabelSave()))

return nil
Expand Down
4 changes: 4 additions & 0 deletions selfservice/strategy/password/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,10 @@ func (s *DefaultPasswordValidator) validate(ctx context.Context, identifier, pas
return text.NewErrorValidationPasswordMinLength(int(passwordPolicyConfig.MinPasswordLength), len(password))
}

if len(password) > int(passwordPolicyConfig.MaxPasswordLength) {
return errors.Errorf("password length cannot exceed %d characters", passwordPolicyConfig.MaxPasswordLength)
}

if passwordPolicyConfig.IdentifierSimilarityCheckEnabled && len(identifier) > 0 {
compIdentifier, compPassword := strings.ToLower(identifier), strings.ToLower(password)
dist := levenshtein.Distance(compIdentifier, compPassword)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@
{
"attributes": {
"autocomplete": "new-password",
"minlength": 8,
"maxlength": 1024,
"disabled": false,
"name": "password",
"node_type": "input",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@
{
"attributes": {
"autocomplete": "new-password",
"minlength": 8,
"maxlength": 1024,
"disabled": false,
"name": "password",
"node_type": "input",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@
{
"attributes": {
"autocomplete": "new-password",
"minlength": 8,
"maxlength": 1024,
"disabled": false,
"name": "password",
"node_type": "input",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
{
"attributes": {
"autocomplete": "new-password",
"minlength": 8,
"maxlength": 1024,
"disabled": false,
"name": "password",
"node_type": "input",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
{
"attributes": {
"autocomplete": "new-password",
"minlength": 8,
"maxlength": 1024,
"disabled": false,
"name": "password",
"node_type": "input",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@
{
"attributes": {
"autocomplete": "new-password",
"minlength": 8,
"maxlength": 1024,
"disabled": false,
"name": "password",
"node_type": "input",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@
{
"attributes": {
"autocomplete": "new-password",
"minlength": 8,
"maxlength": 1024,
"disabled": false,
"name": "password",
"node_type": "input",
Expand Down
5 changes: 5 additions & 0 deletions test/e2e/cypress/support/config.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,10 @@ export type IgnoreLookupNetworkErrors = boolean
* Defines the minimum length of the password.
*/
export type MinimumPasswordLength = number
/**
* Defines the maximum length of the password.
*/
export type MaximumPasswordLength = number
/**
* If set to false the password validation does not check for similarity between the password and the user identifier.
*/
Expand Down Expand Up @@ -875,6 +879,7 @@ export interface PasswordConfiguration {
max_breaches?: AllowPasswordBreaches
ignore_network_errors?: IgnoreLookupNetworkErrors
min_password_length?: MinimumPasswordLength
max_password_length?: MaximumPasswordLength
identifier_similarity_check_enabled?: EnablePasswordIdentifierSimilarityCheck
}
export interface TOTPConfiguration {
Expand Down
6 changes: 6 additions & 0 deletions ui/node/attributes.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@ type InputAttributes struct {
// The autocomplete attribute for the input.
Autocomplete UiNodeInputAttributeAutocomplete `json:"autocomplete,omitempty"`

// The minlength attribute for the input.
MinLength uint `json:"minlength,omitempty"`

// The maxlength attribute for the input.
MaxLength uint `json:"maxlength,omitempty"`

// The input's label text.
Label *text.Message `json:"label,omitempty"`

Expand Down
2 changes: 2 additions & 0 deletions ui/node/fixtures/sort/expected/1.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@
"attributes": {
"name": "password",
"type": "password",
"minlength": 8,
"maxlength": 1024,
"required": true,
"disabled": false,
"node_type": "input"
Expand Down
2 changes: 2 additions & 0 deletions ui/node/fixtures/sort/expected/2.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@
"attributes": {
"name": "password",
"type": "password",
"minlength": 8,
"maxlength": 1024,
"required": true,
"disabled": false,
"node_type": "input"
Expand Down
2 changes: 2 additions & 0 deletions ui/node/fixtures/sort/expected/4.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@
"attributes": {
"name": "password",
"type": "password",
"minlength": 8,
"maxlength": 1024,
"required": true,
"disabled": false,
"node_type": "input"
Expand Down
2 changes: 2 additions & 0 deletions ui/node/fixtures/sort/input/1.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@
"attributes": {
"name": "password",
"type": "password",
"minlength": 8,
"maxlength": 1024,
"required": true,
"disabled": false
},
Expand Down
2 changes: 2 additions & 0 deletions ui/node/fixtures/sort/input/2.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
"attributes": {
"name": "password",
"type": "password",
"minlength": 8,
"maxlength": 1024,
"required": true,
"disabled": false
},
Expand Down
2 changes: 2 additions & 0 deletions ui/node/fixtures/sort/input/4.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@
"attributes": {
"disabled": false,
"name": "password",
"minlength": 8,
"maxlength": 1024,
"required": true,
"type": "password"
},
Expand Down
Loading