From 6a529497241c0628f40f01a11a19b8cd2998173c Mon Sep 17 00:00:00 2001 From: mikolajmeller Date: Fri, 23 Feb 2024 13:24:47 +0100 Subject: [PATCH] feat: add max length password policy --- .schemastore/config.schema.json | 7 +++++++ driver/config/config.go | 3 +++ driver/config/config_test.go | 2 +- embedx/config.schema.json | 7 +++++++ selfservice/strategy/password/login.go | 6 ++++++ selfservice/strategy/password/validator.go | 4 ++++ test/e2e/cypress/support/config.d.ts | 5 +++++ 7 files changed, 33 insertions(+), 1 deletion(-) diff --git a/.schemastore/config.schema.json b/.schemastore/config.schema.json index d03c22b37246..8f46aeb3bd8c 100644 --- a/.schemastore/config.schema.json +++ b/.schemastore/config.schema.json @@ -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.", diff --git a/driver/config/config.go b/driver/config/config.go index 6af7e7c17c5c..e143ae6ac48c 100644 --- a/driver/config/config.go +++ b/driver/config/config.go @@ -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" @@ -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 @@ -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), } } diff --git a/driver/config/config_test.go b/driver/config/config_test.go index 38e3a01fd055..6623e5e98e3a 100644 --- a/driver/config/config_test.go +++ b/driver/config/config_test.go @@ -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"}`}, } { diff --git a/embedx/config.schema.json b/embedx/config.schema.json index a836c21a7af5..f2ed390cc249 100644 --- a/embedx/config.schema.json +++ b/embedx/config.schema.json @@ -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.", diff --git a/selfservice/strategy/password/login.go b/selfservice/strategy/password/login.go index 8c91d7e6c4f9..a60f202a9cf6 100644 --- a/selfservice/strategy/password/login.go +++ b/selfservice/strategy/password/login.go @@ -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)) diff --git a/selfservice/strategy/password/validator.go b/selfservice/strategy/password/validator.go index be1aac3c3497..3925cf8a1fad 100644 --- a/selfservice/strategy/password/validator.go +++ b/selfservice/strategy/password/validator.go @@ -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) diff --git a/test/e2e/cypress/support/config.d.ts b/test/e2e/cypress/support/config.d.ts index c7e9742aed39..9e285f7928d5 100644 --- a/test/e2e/cypress/support/config.d.ts +++ b/test/e2e/cypress/support/config.d.ts @@ -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. */ @@ -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 {