Skip to content

Commit 74264f4

Browse files
committed
Improve password length validation in ActiveModel::SecurePassword for BCrypt compatibility
- Validate password length in both characters and bytes - Provide user-friendly error message for character length - Add byte size validation due to BCrypt's 72-byte limit Co-authored-by: ChatGPT [Fix rails#47600]
1 parent 02c1b7a commit 74264f4

File tree

4 files changed

+46
-2
lines changed

4 files changed

+46
-2
lines changed

activemodel/CHANGELOG.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,22 @@
1+
* Improve password length validation in ActiveModel::SecurePassword to consider byte size for BCrypt compatibility.
2+
3+
The previous password length validation only considered the character count, which may not
4+
accurately reflect the 72-byte size limit imposed by BCrypt. This change updates the validation
5+
to consider both character count and byte size while keeping the character length validation in place.
6+
7+
```ruby
8+
user = User.new(password: "𝕒" * 73) # 73 characters
9+
user.valid? # => false
10+
user.errors[:password] # => ["is too long (maximum is 72 characters)"]
11+
12+
13+
user = User.new(password: "𝕒" * 25) # 25 characters, 75 bytes
14+
user.valid? # => false
15+
user.errors[:password] # => ["is too long (maximum is 72 bytes)"]
16+
```
17+
18+
*ChatGPT*, *Guillermo Iguaran*
19+
120
* `has_secure_password` now generates an `#{attribute}_salt` method that returns the salt
221
used to compute the password digest. The salt will change whenever the password is changed,
322
so it can be used to create single-use password reset tokens with `generates_token_for`:

activemodel/lib/active_model/locale/en.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ en:
1818
too_long:
1919
one: "is too long (maximum is 1 character)"
2020
other: "is too long (maximum is %{count} characters)"
21+
too_long_in_bytes: "is too long (maximum is %{count} bytes)"
2122
too_short:
2223
one: "is too short (minimum is 1 character)"
2324
other: "is too short (minimum is %{count} characters)"

activemodel/lib/active_model/secure_password.rb

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,21 @@ def has_secure_password(attribute = :password, validations: true)
132132
end
133133
end
134134

135-
validates_length_of attribute, maximum: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED
135+
# Validates that the password does not exceed the maximum allowed characters (72 characters) and
136+
# the maximum allowed bytes (72 bytes) for BCrypt. The character length validation is checked first
137+
# to provide a more user-friendly error message. However, the byte size validation is still necessary
138+
# due to BCrypt's inherent limitation of 72 bytes.
139+
validate do |record|
140+
password_value = record.public_send(attribute)
141+
if password_value.present?
142+
if password_value.length > ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED
143+
record.errors.add(attribute, :too_long, count: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED)
144+
elsif password_value.bytesize > ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED
145+
record.errors.add(attribute, :too_long_in_bytes, count: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED)
146+
end
147+
end
148+
end
149+
136150
validates_confirmation_of attribute, allow_blank: true
137151
end
138152
end

activemodel/test/cases/secure_password_test.rb

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,14 +62,24 @@ class SecurePasswordTest < ActiveModel::TestCase
6262
assert_equal ["can’t be blank"], @user.errors[:password]
6363
end
6464

65-
test "create a new user with validation and password length greater than 72" do
65+
test "create a new user with validation and password length greater than 72 characters" do
6666
@user.password = "a" * 73
6767
@user.password_confirmation = "a" * 73
6868
assert_not @user.valid?(:create), "user should be invalid"
6969
assert_equal 1, @user.errors.count
7070
assert_equal ["is too long (maximum is 72 characters)"], @user.errors[:password]
7171
end
7272

73+
test "create a new user with validation and password byte size greater than 72 bytes" do
74+
# Create a password with 73 bytes by using a 3-byte Unicode character (e.g., "あ") 24 times, followed by a 1-byte character "a".
75+
# This will result in a password length of 25 characters, but with a byte size of 73.
76+
@user.password = "あ" * 24 + "a"
77+
@user.password_confirmation = "あ" * 24 + "a"
78+
assert_not @user.valid?(:create), "user should be invalid"
79+
assert_equal 1, @user.errors.count
80+
assert_equal ["is too long (maximum is 72 bytes)"], @user.errors[:password]
81+
end
82+
7383
test "create a new user with validation and a blank password confirmation" do
7484
@user.password = "password"
7585
@user.password_confirmation = ""

0 commit comments

Comments
 (0)