module ActiveModel::SecurePassword::ClassMethods
def has_secure_password(attribute = :password, validations: true, reset_token: true)
# raises ActiveSupport::MessageVerifier::InvalidSignature since the token is expired
User.find_by_password_reset_token(token) # returns nil
# 16 minutes later...
User.find_by_password_reset_token(token) # returns user
token = user.password_reset_token
user = User.create!(name: "david", password: "123", password_confirmation: "123")
===== Using the password reset token
account.valid? # => true
account.is_guest = true
account.valid? # => false, password required
account = Account.new
end
end
super.tap { |errors| errors.delete(:password, :blank) if is_guest }
def errors
has_secure_password
attr_accessor :is_guest, :password_digest
include ActiveModel::SecurePassword
class Account
===== Conditionally requiring a password
user.authenticate("nohack4u") # => user
user.authenticate("vr00m") # => false, old password
user.update(password: "nohack4u", password_challenge: "vr00m") # => true
user.update(password: "pwn3d", password_challenge: "") # => false, challenge doesn't authenticate
user.authenticate_recovery_password("42password") # => user
user.save # => true
user.recovery_password_digest # => "$2a$04$iOfhwahFymCs5weB3BNH/uXkTG65HR.qpW.bNhEjFP3ftli3o5DQC"
user.recovery_password = "42password"
User.find_by(name: "david")&.authenticate("vr00m") # => user
User.find_by(name: "david")&.authenticate("notright") # => false
user.authenticate("vr00m") # => user
user.authenticate("notright") # => false
user.save # => true
user.password_confirmation = "vr00m"
user.save # => false, confirmation doesn't match
user.password = "vr00m"
user.save # => false, password required
user = User.new(name: "david", password: "", password_confirmation: "nomatch")
end
has_secure_password :recovery_password, validations: false
has_secure_password
class User < ActiveRecord::Base
# Schema: User(name:string, password_digest:string, recovery_password_digest:string)
===== Using Active Record (which automatically includes ActiveModel::SecurePassword)
==== Examples
gem "bcrypt", "~> 3.1.7"
To use +has_secure_password+, add bcrypt (~> 3.1.7) to your Gemfile:
and the object responds to +generates_token_for+ (which Active Records do).
is automatically configured when +reset_token+ is set to true (which it is by default)
Finally, a password reset token that's valid for 15 minutes after issue
customizability of validation behavior.
validations: false as an argument. This allows complete
All of the above validations can be omitted by passing
validation will fail.
ActiveModel::Dirty; if dirty tracking methods are not defined, this
password. This validation relies on dirty tracking, as provided by
value other than +nil+, it will validate against the currently persisted
Additionally, a +XXX_challenge+ attribute is created. When set to a
triggered.
it). When this attribute has a +nil+ value, the validation will not be
value for +XXX_confirmation+ (i.e. don't provide a form field for
If confirmation validation is not needed, simply leave out the
* Confirmation of password (using a +XXX_confirmation+ attribute)
* Password length should be less than or equal to 72 bytes
* Password must be present on creation
The following validations are added automatically:
where +XXX+ is the attribute name of your desired password.
This mechanism requires you to have a +XXX_digest+ attribute,
Adds methods to set and authenticate against a BCrypt password.
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