From a6301cc80a1880eb1ae81f54781b988abc41458a Mon Sep 17 00:00:00 2001 From: Baden Ashford Date: Fri, 22 Nov 2024 00:08:14 +0000 Subject: [PATCH 1/5] Introduce config to allow for password complexity to be validated in :validatable with lower case, upper case, numbers, and configurable special character presence to be validated on. --- config/locales/en.yml | 5 ++ lib/devise.rb | 20 +++++ lib/devise/models/validatable.rb | 57 +++++++++++++- lib/generators/templates/devise.rb | 15 ++++ test/models/validatable_test.rb | 81 +++++++++++++++++++- test/rails_app/config/initializers/devise.rb | 15 ++++ 6 files changed, 187 insertions(+), 6 deletions(-) diff --git a/config/locales/en.yml b/config/locales/en.yml index 260e1c4ba6..bd0f60910b 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -63,3 +63,8 @@ en: not_saved: one: "1 error prohibited this %{resource} from being saved:" other: "%{count} errors prohibited this %{resource} from being saved:" + must_contain_lowercase: "must include at least one lowercase letter" + must_contain_uppercase: "must include at least one uppercase letter" + must_contain_number: "must include at least one number" + must_contain_special_character: "must include at least one special character" + diff --git a/lib/devise.rb b/lib/devise.rb index 3d9dc78867..e050ed6362 100644 --- a/lib/devise.rb +++ b/lib/devise.rb @@ -120,6 +120,26 @@ module Test mattr_accessor :password_length @@password_length = 6..128 + # Validate presence of lower case letter in password + mattr_accessor :password_requires_lowercase + @@password_requires_lowercase = false + + # Validate presence of upper case letter in password + mattr_accessor :password_requires_uppercase + @@password_requires_uppercase = false + + # Validate presence of special character in password + mattr_accessor :password_requires_special_character + @@password_requires_special_character = false + + # Special character options + mattr_accessor :password_special_characters + @@password_special_characters = "!?@#$%^&*()_+-=[]{}|:;<>,./" + + # Validate presence of a number in password + mattr_accessor :password_requires_number + @@password_requires_number = false + # The time the user will be remembered without asking for credentials again. mattr_accessor :remember_for @@remember_for = 2.weeks diff --git a/lib/devise/models/validatable.rb b/lib/devise/models/validatable.rb index 1c22fb5fec..cbc161a272 100644 --- a/lib/devise/models/validatable.rb +++ b/lib/devise/models/validatable.rb @@ -13,11 +13,16 @@ module Models # # * +email_regexp+: the regular expression used to validate e-mails; # * +password_length+: a range expressing password length. Defaults to 6..128. - # + # * +password_requires_lowercase+: a boolean to require a lower case letter in the password. Defaults to false. + # * +password_requires_uppercase+: a boolean to require an upper case letter in the password. Defaults to false. + # * +password_requires_special_character+: a boolean to require a special character in the password. Defaults to false. + # * +password_requires_number+: a boolean to require a number in the password. Defaults to false. + # * +password_special_characters+: a string with the special characters that are allowed in the password. Defaults to nil. + module Validatable # All validations used by this module. VALIDATIONS = [:validates_presence_of, :validates_uniqueness_of, :validates_format_of, - :validates_confirmation_of, :validates_length_of].freeze + :validates_confirmation_of, :validates_length_of, :validate].freeze def self.required_fields(klass) [] @@ -35,6 +40,13 @@ def self.included(base) validates_presence_of :password, if: :password_required? validates_confirmation_of :password, if: :password_required? validates_length_of :password, within: password_length, allow_blank: true + + validates_format_of :password, with: /\p{Lower}/, if: -> { password_requires_lowercase }, message: :must_contain_lowercase + validates_format_of :password, with: /\p{Upper}/, if: -> { password_requires_uppercase }, message: :must_contain_uppercase + validates_format_of :password, with: /\d/, if: -> { password_requires_number }, message: :must_contain_number + + # Run as special character check as a custom validation to ensure password_special_characters is evaluated at runtime + validate :password_must_contain_special_character, if: -> { password_requires_special_character } end end @@ -60,8 +72,47 @@ def email_required? true end + # Make these instance methods so the default Devise.password_requires_<> + #can be overridden + def password_requires_lowercase + self.class.password_requires_lowercase + end + + def password_requires_uppercase + self.class.password_requires_uppercase + end + + def password_requires_number + self.class.password_requires_number + end + + def password_requires_special_character + self.class.password_requires_special_character + end + + def password_special_characters + self.class.password_special_characters + end + + def password_must_contain_special_character + special_character_regex = /[#{Regexp.escape(password_special_characters)}]/ + + unless password =~ special_character_regex + errors.add(:password, :must_contain_special_character) + end + end + module ClassMethods - Devise::Models.config(self, :email_regexp, :password_length) + Devise::Models.config( + self, + :email_regexp, + :password_length, + :password_requires_lowercase, + :password_requires_uppercase, + :password_requires_special_character, + :password_requires_number, + :password_special_characters + ) end end end diff --git a/lib/generators/templates/devise.rb b/lib/generators/templates/devise.rb index 9e6744bd7d..07c57d9faa 100644 --- a/lib/generators/templates/devise.rb +++ b/lib/generators/templates/devise.rb @@ -185,6 +185,21 @@ # to give user feedback and not to assert the e-mail validity. config.email_regexp = /\A[^@\s]+@[^@\s]+\z/ + # Password requires a lower case letter + # config.password_requires_lowercase = false + + # Password requires an upper case letter + # config.password_requires_uppercase = false + + # Password requires a number + # config.password_requires_number = false + + # Password requires a special character + # config.password_requires_special_character = false + + # Special characters that are allowed in the password + # config.password_special_characters = "!?@#$%^&*()_+-=[]{}|:;<>,./" + # ==> Configuration for :timeoutable # The time you want to timeout the user session without activity. After this # time the user will be asked for credentials again. Default is 30 minutes. diff --git a/test/models/validatable_test.rb b/test/models/validatable_test.rb index e8858de7e3..65cdabac5d 100644 --- a/test/models/validatable_test.rb +++ b/test/models/validatable_test.rb @@ -1,10 +1,9 @@ # encoding: UTF-8 # frozen_string_literal: true - -require 'test_helper' +require "test_helper" class ValidatableTest < ActiveSupport::TestCase - test 'should require email to be set' do + test 'should require email to be set' do user = new_user(email: nil) assert user.invalid? assert user.errors[:email] @@ -121,4 +120,80 @@ class ValidatableTest < ActiveSupport::TestCase test 'required_fields should be an empty array' do assert_equal [], Devise::Models::Validatable.required_fields(User) end + + + test "password must require a lower case letter if password_requires_lowercase_letter is true" do + with_password_requirement(:password_requires_lowercase, false) do + user = new_user(password: 'PASSWORD', password_confirmation: 'PASSWORD') + assert user.valid? + end + + with_password_requirement(:password_requires_lowercase, true) do + user = new_user(password: 'PASSWORD', password_confirmation: 'PASSWORD') + assert user.invalid? + assert_equal 'must include at least one lowercase letter', user.errors[:password].join + end + end + + test "password must require an upper case letter if password_requires_uppercase_letter is true" do + with_password_requirement(:password_requires_uppercase, false) do + user = new_user(password: 'password', password_confirmation: 'password') + assert user.valid? + end + + with_password_requirement(:password_requires_uppercase, true) do + user = new_user(password: 'password', password_confirmation: 'password') + assert user.invalid? + assert_equal 'must include at least one uppercase letter', user.errors[:password].join + end + end + + test "password must require an upper case letter if password_requires_number is true" do + with_password_requirement(:password_requires_number, false) do + user = new_user(password: 'password', password_confirmation: 'password') + assert user.valid? + end + + with_password_requirement(:password_requires_number, true) do + user = new_user(password: 'password', password_confirmation: 'password') + assert user.invalid? + assert_equal 'must include at least one number', user.errors[:password].join + end + end + + test "password must require special character if password_requires_special_character is true" do + with_password_requirement(:password_requires_special_character, false) do + user = new_user(password: 'password', password_confirmation: 'password') + assert user.valid? + end + + with_password_requirement(:password_requires_special_character, true) do + user = new_user(password: 'password', password_confirmation: 'password') + assert user.invalid? + assert_equal 'must include at least one special character', user.errors[:password].join + end + end + + + test "special character must be within defined special character set if it is custom" do + with_password_requirement(:password_requires_special_character, true) do + with_password_requirement(:password_special_characters, '!') do + user = new_user(password: 'password!', password_confirmation: 'password!') + assert user.valid? + + user = new_user(password: 'password?', password_confirmation: 'password?') + assert user.invalid? + assert_equal 'must include at least one special character', user.errors[:password].join + end + end + end + + def with_password_requirement(requirement, value) + # Change the password requirement and restore it after the block is executed + original_value = User.public_send(requirement) + User.public_send("#{requirement}=", value) + yield + ensure + User.public_send("#{requirement}=", original_value) + end end diff --git a/test/rails_app/config/initializers/devise.rb b/test/rails_app/config/initializers/devise.rb index a3a339edc1..284e73db0d 100644 --- a/test/rails_app/config/initializers/devise.rb +++ b/test/rails_app/config/initializers/devise.rb @@ -101,6 +101,21 @@ # Regex to use to validate the email address # config.email_regexp = /^([\w\.%\+\-]+)@([\w\-]+\.)+([\w]{2,})$/i + # Password requires a lower case letter + # config.password_requires_lowercase = false + + # Password requires an upper case letter + # config.password_requires_uppercase = false + + # Password requires a number + # config.password_requires_number = false + + # Password requires a special character + # config.password_requires_special_character = false + + # Special characters that are allowed in the password + # config.password_special_characters = "!?@#$%^&*()_+-=[]{}|:;<>,./" + # ==> Configuration for :timeoutable # The time you want to timeout the user session without activity. After this # time the user will be asked for credentials again. Default is 30 minutes. From d463f9a6eb5cc99f32de9ce7ffe6f8d9d6492f25 Mon Sep 17 00:00:00 2001 From: Baden Ashford Date: Tue, 10 Dec 2024 00:31:18 +0000 Subject: [PATCH 2/5] Update password complexity to be a hash instead of individual keys --- config/locales/en.yml | 2 +- lib/devise.rb | 28 +++++--------- lib/devise/models/validatable.rb | 26 ++++++------- lib/generators/templates/devise.rb | 22 ++++------- test/models/validatable_test.rb | 39 +++++++++++--------- test/rails_app/config/initializers/devise.rb | 24 +++++------- 6 files changed, 62 insertions(+), 79 deletions(-) diff --git a/config/locales/en.yml b/config/locales/en.yml index bd0f60910b..27a20d6cc5 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -65,6 +65,6 @@ en: other: "%{count} errors prohibited this %{resource} from being saved:" must_contain_lowercase: "must include at least one lowercase letter" must_contain_uppercase: "must include at least one uppercase letter" - must_contain_number: "must include at least one number" + must_contain_digit: "must include at least one number" must_contain_special_character: "must include at least one special character" diff --git a/lib/devise.rb b/lib/devise.rb index 49f76e9b0f..bc42aa3481 100644 --- a/lib/devise.rb +++ b/lib/devise.rb @@ -120,25 +120,15 @@ module Test mattr_accessor :password_length @@password_length = 6..128 - # Validate presence of lower case letter in password - mattr_accessor :password_requires_lowercase - @@password_requires_lowercase = false - - # Validate presence of upper case letter in password - mattr_accessor :password_requires_uppercase - @@password_requires_uppercase = false - - # Validate presence of special character in password - mattr_accessor :password_requires_special_character - @@password_requires_special_character = false - - # Special character options - mattr_accessor :password_special_characters - @@password_special_characters = "!?@#$%^&*()_+-=[]{}|:;<>,./" - - # Validate presence of a number in password - mattr_accessor :password_requires_number - @@password_requires_number = false + # Password complexity configuration + mattr_accessor :password_complexity + @@password_complexity = { + require_upper: false, + require_lower: false, + require_digit: false, + require_special: false, + special_characters: "!?@#$%^&*()_+-=[]{}|:;<>,./" + } # The time the user will be remembered without asking for credentials again. mattr_accessor :remember_for diff --git a/lib/devise/models/validatable.rb b/lib/devise/models/validatable.rb index 43c0138abb..a5fc66d03c 100644 --- a/lib/devise/models/validatable.rb +++ b/lib/devise/models/validatable.rb @@ -16,7 +16,7 @@ module Models # * +password_requires_lowercase+: a boolean to require a lower case letter in the password. Defaults to false. # * +password_requires_uppercase+: a boolean to require an upper case letter in the password. Defaults to false. # * +password_requires_special_character+: a boolean to require a special character in the password. Defaults to false. - # * +password_requires_number+: a boolean to require a number in the password. Defaults to false. + # * +password_requires_digit+: a boolean to require a digit in the password. Defaults to false. # * +password_special_characters+: a string with the special characters that are allowed in the password. Defaults to nil. # # Since +password_length+ is applied in a proc within `validates_length_of` it can be overridden @@ -45,7 +45,7 @@ def self.included(base) validates_format_of :password, with: /\p{Lower}/, if: -> { password_requires_lowercase }, message: :must_contain_lowercase validates_format_of :password, with: /\p{Upper}/, if: -> { password_requires_uppercase }, message: :must_contain_uppercase - validates_format_of :password, with: /\d/, if: -> { password_requires_number }, message: :must_contain_number + validates_format_of :password, with: /\d/, if: -> { password_requires_digit }, message: :must_contain_digit # Run as special character check as a custom validation to ensure password_special_characters is evaluated at runtime validate :password_must_contain_special_character, if: -> { password_requires_special_character } @@ -76,24 +76,28 @@ def email_required? # Make these instance methods so the default Devise.password_requires_<> #can be overridden + def password_complexity + self.class.password_complexity + end + def password_requires_lowercase - self.class.password_requires_lowercase + password_complexity[:require_lower] end def password_requires_uppercase - self.class.password_requires_uppercase + password_complexity[:require_upper] end - def password_requires_number - self.class.password_requires_number + def password_requires_digit + password_complexity[:require_digit] end def password_requires_special_character - self.class.password_requires_special_character + password_complexity[:require_special] end def password_special_characters - self.class.password_special_characters + password_complexity[:special_characters] end def password_must_contain_special_character @@ -109,11 +113,7 @@ module ClassMethods self, :email_regexp, :password_length, - :password_requires_lowercase, - :password_requires_uppercase, - :password_requires_special_character, - :password_requires_number, - :password_special_characters + :password_complexity ) end end diff --git a/lib/generators/templates/devise.rb b/lib/generators/templates/devise.rb index 07c57d9faa..9bae572d38 100644 --- a/lib/generators/templates/devise.rb +++ b/lib/generators/templates/devise.rb @@ -185,20 +185,14 @@ # to give user feedback and not to assert the e-mail validity. config.email_regexp = /\A[^@\s]+@[^@\s]+\z/ - # Password requires a lower case letter - # config.password_requires_lowercase = false - - # Password requires an upper case letter - # config.password_requires_uppercase = false - - # Password requires a number - # config.password_requires_number = false - - # Password requires a special character - # config.password_requires_special_character = false - - # Special characters that are allowed in the password - # config.password_special_characters = "!?@#$%^&*()_+-=[]{}|:;<>,./" + # Password complexity configuration + # config.password_complexity = { + # require_upper: false, + # require_lower: false, + # require_digit: false, + # require_special: false, + # special_characters: "!?@#$%^&*()_+-=[]{}|:;<>,./" + # } # ==> Configuration for :timeoutable # The time you want to timeout the user session without activity. After this diff --git a/test/models/validatable_test.rb b/test/models/validatable_test.rb index 65cdabac5d..b0b7891cce 100644 --- a/test/models/validatable_test.rb +++ b/test/models/validatable_test.rb @@ -122,52 +122,52 @@ class ValidatableTest < ActiveSupport::TestCase end - test "password must require a lower case letter if password_requires_lowercase_letter is true" do - with_password_requirement(:password_requires_lowercase, false) do + test "password must require a lower case letter if require_lower is true" do + with_password_requirement(:require_lower, false) do user = new_user(password: 'PASSWORD', password_confirmation: 'PASSWORD') assert user.valid? end - with_password_requirement(:password_requires_lowercase, true) do + with_password_requirement(:require_lower, true) do user = new_user(password: 'PASSWORD', password_confirmation: 'PASSWORD') assert user.invalid? assert_equal 'must include at least one lowercase letter', user.errors[:password].join end end - test "password must require an upper case letter if password_requires_uppercase_letter is true" do - with_password_requirement(:password_requires_uppercase, false) do + test "password must require an upper case letter if require_upper is true" do + with_password_requirement(:require_upper, false) do user = new_user(password: 'password', password_confirmation: 'password') assert user.valid? end - with_password_requirement(:password_requires_uppercase, true) do + with_password_requirement(:require_upper, true) do user = new_user(password: 'password', password_confirmation: 'password') assert user.invalid? assert_equal 'must include at least one uppercase letter', user.errors[:password].join end end - test "password must require an upper case letter if password_requires_number is true" do - with_password_requirement(:password_requires_number, false) do + test "password must require an upper case letter if require_digit is true" do + with_password_requirement(:require_digit, false) do user = new_user(password: 'password', password_confirmation: 'password') assert user.valid? end - with_password_requirement(:password_requires_number, true) do + with_password_requirement(:require_digit, true) do user = new_user(password: 'password', password_confirmation: 'password') assert user.invalid? assert_equal 'must include at least one number', user.errors[:password].join end end - test "password must require special character if password_requires_special_character is true" do - with_password_requirement(:password_requires_special_character, false) do + test "password must require special character if require_special is true" do + with_password_requirement(:require_special, false) do user = new_user(password: 'password', password_confirmation: 'password') assert user.valid? end - with_password_requirement(:password_requires_special_character, true) do + with_password_requirement(:require_special, true) do user = new_user(password: 'password', password_confirmation: 'password') assert user.invalid? assert_equal 'must include at least one special character', user.errors[:password].join @@ -176,8 +176,8 @@ class ValidatableTest < ActiveSupport::TestCase test "special character must be within defined special character set if it is custom" do - with_password_requirement(:password_requires_special_character, true) do - with_password_requirement(:password_special_characters, '!') do + with_password_requirement(:require_special, true) do + with_password_requirement(:special_characters, '!') do user = new_user(password: 'password!', password_confirmation: 'password!') assert user.valid? @@ -190,10 +190,15 @@ class ValidatableTest < ActiveSupport::TestCase def with_password_requirement(requirement, value) # Change the password requirement and restore it after the block is executed - original_value = User.public_send(requirement) - User.public_send("#{requirement}=", value) + original_password_complexity= User.public_send("password_complexity") + original_value = original_password_complexity[requirement] + + updated_password_complexity = original_password_complexity.dup + updated_password_complexity[requirement] = value + + User.public_send("password_complexity=", updated_password_complexity) yield ensure - User.public_send("#{requirement}=", original_value) + User.public_send("password_complexity=", original_password_complexity) end end diff --git a/test/rails_app/config/initializers/devise.rb b/test/rails_app/config/initializers/devise.rb index 284e73db0d..d7e7ec6e1c 100644 --- a/test/rails_app/config/initializers/devise.rb +++ b/test/rails_app/config/initializers/devise.rb @@ -100,21 +100,15 @@ # Regex to use to validate the email address # config.email_regexp = /^([\w\.%\+\-]+)@([\w\-]+\.)+([\w]{2,})$/i - - # Password requires a lower case letter - # config.password_requires_lowercase = false - - # Password requires an upper case letter - # config.password_requires_uppercase = false - - # Password requires a number - # config.password_requires_number = false - - # Password requires a special character - # config.password_requires_special_character = false - - # Special characters that are allowed in the password - # config.password_special_characters = "!?@#$%^&*()_+-=[]{}|:;<>,./" + + # Password complexity configuration + # config.password_complexity = { + # require_upper: false, + # require_lower: false, + # require_digit: false, + # require_special: false, + # special_characters: "!?@#$%^&*()_+-=[]{}|:;<>,./" + # } # ==> Configuration for :timeoutable # The time you want to timeout the user session without activity. After this From a57aac3c1dccc6ceb8ce831dbd9d4162182dbd3b Mon Sep 17 00:00:00 2001 From: Baden Ashford Date: Tue, 10 Dec 2024 00:34:52 +0000 Subject: [PATCH 3/5] Update docs on Devise::Models --- lib/devise/models/validatable.rb | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/devise/models/validatable.rb b/lib/devise/models/validatable.rb index a5fc66d03c..bfbd4ca518 100644 --- a/lib/devise/models/validatable.rb +++ b/lib/devise/models/validatable.rb @@ -13,11 +13,12 @@ module Models # # * +email_regexp+: the regular expression used to validate e-mails; # * +password_length+: a range expressing password length. Defaults to 6..128. - # * +password_requires_lowercase+: a boolean to require a lower case letter in the password. Defaults to false. - # * +password_requires_uppercase+: a boolean to require an upper case letter in the password. Defaults to false. - # * +password_requires_special_character+: a boolean to require a special character in the password. Defaults to false. - # * +password_requires_digit+: a boolean to require a digit in the password. Defaults to false. - # * +password_special_characters+: a string with the special characters that are allowed in the password. Defaults to nil. + # * +password_complexity+: a hash with the password complexity requirements, with the following keys: + # - +require_lower+: a boolean to require a lower case letter in the password. Defaults to false. + # - +require_upper+: a boolean to require an upper case letter in the password. Defaults to false. + # - +require_special+: a boolean to require a special character in the password. Defaults to false. + # - +require_digit+: a boolean to require a digit in the password. Defaults to false. + # - +special_characters+: a string with the special characters that are allowed in the password. Defaults to nil. # # Since +password_length+ is applied in a proc within `validates_length_of` it can be overridden # at runtime. From 776a657e0b2fe5f8674dc634506d813be7679de3 Mon Sep 17 00:00:00 2001 From: Baden Ashford Date: Mon, 16 Dec 2024 09:33:32 +1100 Subject: [PATCH 4/5] Update from PR review --- test/models/validatable_test.rb | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/test/models/validatable_test.rb b/test/models/validatable_test.rb index b0b7891cce..141d5299d2 100644 --- a/test/models/validatable_test.rb +++ b/test/models/validatable_test.rb @@ -1,9 +1,9 @@ # encoding: UTF-8 # frozen_string_literal: true -require "test_helper" +require 'test_helper' class ValidatableTest < ActiveSupport::TestCase - test 'should require email to be set' do + test 'should require email to be set' do user = new_user(email: nil) assert user.invalid? assert user.errors[:email] @@ -176,7 +176,7 @@ class ValidatableTest < ActiveSupport::TestCase test "special character must be within defined special character set if it is custom" do - with_password_requirement(:require_special, true) do + with_password_requirement(:require_special, true) do with_password_requirement(:special_characters, '!') do user = new_user(password: 'password!', password_confirmation: 'password!') assert user.valid? @@ -191,7 +191,6 @@ class ValidatableTest < ActiveSupport::TestCase def with_password_requirement(requirement, value) # Change the password requirement and restore it after the block is executed original_password_complexity= User.public_send("password_complexity") - original_value = original_password_complexity[requirement] updated_password_complexity = original_password_complexity.dup updated_password_complexity[requirement] = value From 44dca10d24e677f3b6974f04eea2b99b42344686 Mon Sep 17 00:00:00 2001 From: Baden Ashford Date: Tue, 21 Jan 2025 14:35:40 +1100 Subject: [PATCH 5/5] Remove require_special to make the presence of special_characters imply a special character is required --- lib/devise.rb | 3 +-- lib/devise/models/validatable.rb | 11 +++----- lib/generators/templates/devise.rb | 1 - test/models/validatable_test.rb | 28 +++++--------------- test/rails_app/config/initializers/devise.rb | 1 - 5 files changed, 10 insertions(+), 34 deletions(-) diff --git a/lib/devise.rb b/lib/devise.rb index bc42aa3481..b4217ef4ae 100644 --- a/lib/devise.rb +++ b/lib/devise.rb @@ -126,8 +126,7 @@ module Test require_upper: false, require_lower: false, require_digit: false, - require_special: false, - special_characters: "!?@#$%^&*()_+-=[]{}|:;<>,./" + special_characters: "" } # The time the user will be remembered without asking for credentials again. diff --git a/lib/devise/models/validatable.rb b/lib/devise/models/validatable.rb index bfbd4ca518..19c4b5dd70 100644 --- a/lib/devise/models/validatable.rb +++ b/lib/devise/models/validatable.rb @@ -16,9 +16,8 @@ module Models # * +password_complexity+: a hash with the password complexity requirements, with the following keys: # - +require_lower+: a boolean to require a lower case letter in the password. Defaults to false. # - +require_upper+: a boolean to require an upper case letter in the password. Defaults to false. - # - +require_special+: a boolean to require a special character in the password. Defaults to false. # - +require_digit+: a boolean to require a digit in the password. Defaults to false. - # - +special_characters+: a string with the special characters that are allowed in the password. Defaults to nil. + # - +special_characters+: a string with the special characters that are allowed in the password. Defaults to empty string. # # Since +password_length+ is applied in a proc within `validates_length_of` it can be overridden # at runtime. @@ -49,7 +48,7 @@ def self.included(base) validates_format_of :password, with: /\d/, if: -> { password_requires_digit }, message: :must_contain_digit # Run as special character check as a custom validation to ensure password_special_characters is evaluated at runtime - validate :password_must_contain_special_character, if: -> { password_requires_special_character } + validate :password_must_contain_special_character, if: -> { password_special_characters.present? } end end @@ -92,16 +91,12 @@ def password_requires_uppercase def password_requires_digit password_complexity[:require_digit] end - - def password_requires_special_character - password_complexity[:require_special] - end def password_special_characters password_complexity[:special_characters] end - def password_must_contain_special_character + def password_must_contain_special_character special_character_regex = /[#{Regexp.escape(password_special_characters)}]/ unless password =~ special_character_regex diff --git a/lib/generators/templates/devise.rb b/lib/generators/templates/devise.rb index 9bae572d38..65a594080c 100644 --- a/lib/generators/templates/devise.rb +++ b/lib/generators/templates/devise.rb @@ -190,7 +190,6 @@ # require_upper: false, # require_lower: false, # require_digit: false, - # require_special: false, # special_characters: "!?@#$%^&*()_+-=[]{}|:;<>,./" # } diff --git a/test/models/validatable_test.rb b/test/models/validatable_test.rb index 141d5299d2..68912df899 100644 --- a/test/models/validatable_test.rb +++ b/test/models/validatable_test.rb @@ -161,31 +161,15 @@ class ValidatableTest < ActiveSupport::TestCase end end - test "password must require special character if require_special is true" do - with_password_requirement(:require_special, false) do - user = new_user(password: 'password', password_confirmation: 'password') + test "special character must be within defined special character set if it is custom" do + with_password_requirement(:special_characters, '!') do + user = new_user(password: 'password!', password_confirmation: 'password!') assert user.valid? - end - - with_password_requirement(:require_special, true) do - user = new_user(password: 'password', password_confirmation: 'password') - assert user.invalid? - assert_equal 'must include at least one special character', user.errors[:password].join - end - end - - test "special character must be within defined special character set if it is custom" do - with_password_requirement(:require_special, true) do - with_password_requirement(:special_characters, '!') do - user = new_user(password: 'password!', password_confirmation: 'password!') - assert user.valid? - - user = new_user(password: 'password?', password_confirmation: 'password?') - assert user.invalid? - assert_equal 'must include at least one special character', user.errors[:password].join + user = new_user(password: 'password?', password_confirmation: 'password?') + assert user.invalid? + assert_equal 'must include at least one special character', user.errors[:password].join end - end end def with_password_requirement(requirement, value) diff --git a/test/rails_app/config/initializers/devise.rb b/test/rails_app/config/initializers/devise.rb index d7e7ec6e1c..c803c1b790 100644 --- a/test/rails_app/config/initializers/devise.rb +++ b/test/rails_app/config/initializers/devise.rb @@ -106,7 +106,6 @@ # require_upper: false, # require_lower: false, # require_digit: false, - # require_special: false, # special_characters: "!?@#$%^&*()_+-=[]{}|:;<>,./" # }