Skip to content

Commit

Permalink
Update password complexity to be a hash instead of individual keys
Browse files Browse the repository at this point in the history
  • Loading branch information
kykyi committed Dec 10, 2024
1 parent ea11968 commit d463f9a
Show file tree
Hide file tree
Showing 6 changed files with 62 additions and 79 deletions.
2 changes: 1 addition & 1 deletion config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"

28 changes: 9 additions & 19 deletions lib/devise.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 13 additions & 13 deletions lib/devise/models/validatable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 }
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
22 changes: 8 additions & 14 deletions lib/generators/templates/devise.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
39 changes: 22 additions & 17 deletions test/models/validatable_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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?

Expand All @@ -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
24 changes: 9 additions & 15 deletions test/rails_app/config/initializers/devise.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit d463f9a

Please sign in to comment.