Module: ActiveModel::SecurePassword::ClassMethods
- Defined in:
- activemodel/lib/active_model/secure_password.rb
Instance Method Summary collapse
-
#has_secure_password(attribute = :password, validations: true, reset_token: true, algorithm: nil) ⇒ Object
Adds methods to set and authenticate against a BCrypt password.
Instance Method Details
#has_secure_password(attribute = :password, validations: true, reset_token: true, algorithm: nil) ⇒ 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_confirmationattribute)
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.
A password reset token (valid for 15 minutes by default) 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).
Finally, the reset token expiry can be customized by passing a hash to has_secure_password:
has_secure_password reset_token: { expires_in: 1.hour }
To use has_secure_password, add bcrypt (~> 3.1.7) to your Gemfile:
gem "bcrypt", "~> 3.1.7"
If you want to use a different password hashing algorithm, you can implement your own class that responds to algorithm_name, hash_password, verify_password, password_salt and validate. For an example implementation, see BCryptPassword in bcrypt_password.rb.
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.password_algorithm # => :bcrypt
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)
Customizing the hashing algorithm
has_secure_password supports :bcrypt (default) and :argon2 out of the box. To use :argon2, add gem “argon2”, “~> 2.3” to your Gemfile and set the algorithm option:
class User < ActiveRecord::Base
has_secure_password algorithm: :argon2
end
To add a custom algorithm, create a class that implements hash_password, verify_password, password_salt, validate and algorithm_name methods, then register it:
class ScryptPassword
def initialize
require "scrypt"
rescue LoadError
warn "You don't have scrypt installed in your application. Please add it to your Gemfile and run bundle install."
raise
end
def hash_password(unencrypted_password)
SCrypt::Password.create(unencrypted_password)
end
def verify_password(password, digest)
SCrypt::Password.new(digest) == password
end
def password_salt(digest)
SCrypt::Password.new(digest).salt
end
def validate(_record, _attribute)
# Scrypt has no maximum input size, no validation needed
end
def algorithm_name
:scrypt
end
end
ActiveModel::SecurePassword.register_algorithm :scrypt, ScryptPassword
class User < ActiveRecord::Base
has_secure_password algorithm: :scrypt
end
194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 |
# File 'activemodel/lib/active_model/secure_password.rb', line 194 def has_secure_password(attribute = :password, validations: true, reset_token: true, algorithm: nil) # Resolve algorithm: can be a Symbol (for registry lookup), an instance, or default to BCrypt algorithm = case algorithm when Symbol algorithm_class = ActiveModel::SecurePassword.lookup_algorithm(algorithm) raise ArgumentError, "Unknown password algorithm: #{algorithm.inspect}" unless algorithm_class algorithm_class.new when nil BCryptPassword.new else algorithm end include InstanceMethodsOnActivation.new(attribute, reset_token: reset_token, algorithm: algorithm) 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? && algorithm.verify_password(challenge, digest_was) record.errors.add(:"#{attribute}_challenge") end end end # Performs password hashing algorithm-specific validations (such as a max input size) validate do |record| algorithm.validate(record, attribute) end validates_confirmation_of attribute, allow_nil: 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) reset_token_expires_in = reset_token.is_a?(Hash) ? reset_token[:expires_in] : DEFAULT_RESET_TOKEN_EXPIRES_IN silence_redefinition_of_method(:"#{attribute}_reset_token_expires_in") define_method(:"#{attribute}_reset_token_expires_in") { reset_token_expires_in } generates_token_for :"#{attribute}_reset", expires_in: reset_token_expires_in 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 |