Module: ActiveModel::SecurePassword::ClassMethods
- Defined in:
- lib/active_model/secure_password.rb
Instance Method Summary collapse
-
#has_secure_password(attribute = :password, validations: true, reset_token: true) ⇒ Object
Adds methods to set and authenticate against a BCrypt password.
Instance Method Details
#has_secure_password(attribute = :password, validations: true, reset_token: true) ⇒ Object
Adds methods to set and authenticate against a BCrypt password. This mechanism requires you to have a XXX_digest
attribute, where XXX
is the attribute name of your desired password.
The following validations are added automatically:
-
Password must be present on creation
-
Password length should be less than or equal to 72 bytes
-
Confirmation of password (using a
XXX_confirmation
attribute)
If confirmation validation is not needed, simply leave out the value for XXX_confirmation
(i.e. don’t provide a form field for it). When this attribute has a nil
value, the validation will not be triggered.
Additionally, a XXX_challenge
attribute is created. When set to a value other than nil
, it will validate against the currently persisted password. This validation relies on dirty tracking, as provided by ActiveModel::Dirty; if dirty tracking methods are not defined, this validation will fail.
All of the above validations can be omitted by passing validations: false
as an argument. This allows complete customizability of validation behavior.
Finally, a password reset token that’s valid for 15 minutes after issue is automatically configured when reset_token
is set to true (which it is by default) and the object responds to generates_token_for
(which Active Records do).
To use has_secure_password
, add bcrypt (~> 3.1.7) to your Gemfile:
gem "bcrypt", "~> 3.1.7"
Examples
Using Active Record (which automatically includes ActiveModel::SecurePassword)
# Schema: User(name:string, password_digest:string, recovery_password_digest:string)
class User < ActiveRecord::Base
has_secure_password
has_secure_password :recovery_password, validations: false
end
user = User.new(name: "david", password: "", password_confirmation: "nomatch")
user.save # => false, password required
user.password = "vr00m"
user.save # => false, confirmation doesn't match
user.password_confirmation = "vr00m"
user.save # => true
user.authenticate("notright") # => false
user.authenticate("vr00m") # => user
User.find_by(name: "david")&.authenticate("notright") # => false
User.find_by(name: "david")&.authenticate("vr00m") # => user
user.recovery_password = "42password"
user.recovery_password_digest # => "$2a$04$iOfhwahFymCs5weB3BNH/uXkTG65HR.qpW.bNhEjFP3ftli3o5DQC"
user.save # => true
user.authenticate_recovery_password("42password") # => user
user.update(password: "pwn3d", password_challenge: "") # => false, challenge doesn't authenticate
user.update(password: "nohack4u", password_challenge: "vr00m") # => true
user.authenticate("vr00m") # => false, old password
user.authenticate("nohack4u") # => user
Conditionally requiring a password
class Account
include ActiveModel::SecurePassword
attr_accessor :is_guest, :password_digest
has_secure_password
def errors
super.tap { |errors| errors.delete(:password, :blank) if is_guest }
end
end
account = Account.new
account.valid? # => false, password required
account.is_guest = true
account.valid? # => true
Using the password reset token
user = User.create!(name: "david", password: "123", password_confirmation: "123")
token = user.password_reset_token
User.find_by_password_reset_token(token) # returns user
# 16 minutes later...
User.find_by_password_reset_token(token) # returns nil
# raises ActiveSupport::MessageVerifier::InvalidSignature since the token is expired
User.find_by_password_reset_token!(token)
116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 |
# File 'lib/active_model/secure_password.rb', line 116 def has_secure_password(attribute = :password, validations: true, reset_token: true) # Load bcrypt gem only when has_secure_password is used. # This is to avoid ActiveModel (and by extension the entire framework) # being dependent on a binary library. begin require "bcrypt" rescue LoadError warn "You don't have bcrypt installed in your application. Please add it to your Gemfile and run bundle install." raise end include InstanceMethodsOnActivation.new(attribute, reset_token: reset_token) if validations include ActiveModel::Validations # This ensures the model has a password by checking whether the password_digest # is present, so that this works with both new and existing records. However, # when there is an error, the message is added to the password attribute instead # so that the error message will make sense to the end-user. validate do |record| record.errors.add(attribute, :blank) unless record.public_send("#{attribute}_digest").present? end validate do |record| if challenge = record.public_send(:"#{attribute}_challenge") digest_was = record.public_send(:"#{attribute}_digest_was") if record.respond_to?(:"#{attribute}_digest_was") unless digest_was.present? && BCrypt::Password.new(digest_was).is_password?(challenge) record.errors.add(:"#{attribute}_challenge") end end end # Validates that the password does not exceed the maximum allowed bytes for BCrypt (72 bytes). validate do |record| password_value = record.public_send(attribute) if password_value.present? && password_value.bytesize > ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED record.errors.add(attribute, :password_too_long) end end validates_confirmation_of attribute, allow_blank: true end # Only generate tokens for records that are capable of doing so (Active Records, not vanilla Active Models) if reset_token && respond_to?(:generates_token_for) generates_token_for :"#{attribute}_reset", expires_in: 15.minutes do public_send(:"#{attribute}_salt")&.last(10) end class_eval <<-RUBY, __FILE__, __LINE__ + 1 silence_redefinition_of_method :find_by_#{attribute}_reset_token def self.find_by_#{attribute}_reset_token(token) find_by_token_for(:#{attribute}_reset, token) end silence_redefinition_of_method :find_by_#{attribute}_reset_token! def self.find_by_#{attribute}_reset_token!(token) find_by_token_for!(:#{attribute}_reset, token) end RUBY end end |