module ActiveModel::SecurePassword::ClassMethods

def has_secure_password(attribute = :password, validations: true, reset_token: true)

User.find_by_password_reset_token!(token)
# 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