Module: ActiveModel::SecurePassword::ClassMethods

Defined in:
activemodel/lib/active_model/secure_password.rb

Instance Method Summary collapse

Instance Method Details

#has_secure_password(attribute = :password, validations: 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.

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

80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'activemodel/lib/active_model/secure_password.rb', line 80

def has_secure_password(attribute = :password, validations: 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
    $stderr.puts "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)

  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_length_of attribute, maximum: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED
    validates_confirmation_of attribute, allow_blank: true
  end
end