Skip to content

Instantly share code, notes, and snippets.

@br3nt
Created November 15, 2018 04:39
Show Gist options
  • Save br3nt/f802fb252ac3110b74841c6a183ac57c to your computer and use it in GitHub Desktop.
Save br3nt/f802fb252ac3110b74841c6a183ac57c to your computer and use it in GitHub Desktop.
Better Implementation of Rails' ActiveModel::SecurePassword
# frozen_string_literal: true
module SecureAttribute
extend ActiveSupport::Concern
# BCrypt hash function can handle maximum 72 bytes, and if we pass
# password of length more than 72 bytes it ignores extra characters.
# Hence need to put a restriction on password length.
MAX_PASSWORD_LENGTH_ALLOWED = 72
class << self
attr_accessor :min_cost # :nodoc:
end
self.min_cost = false
class_methods do
def has_secure_password(**options)
has_secure_attribute(:password, **options)
alias_method :authenticate, :authenticate_password
end
def has_secure_attribute(attribute_name, validations: true)
# Load bcrypt gem only when has_secure_attribute is used.
# This is to avoid ActiveModel (and by extension the entire framework)
# being dependent on a binary library.
begin
require_dependency "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
attribute_name = attribute_name.to_sym
digest_name = '%s_digest' % attribute_name
authentication_name = 'authenticate_%s' % attribute_name
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
attr_accessor attribute_name
define_singleton_method(authentication_name) do |unencrypted_value|
candidates = all.select(primary_key, digest_name).to_a
match = candidates.find {|m| m.send(authentication_name, unencrypted_value) }
match ? all.find(match.id) : false
end
define_method(authentication_name) do |unencrypted_value|
digest = read_attribute(digest_name)
BCrypt::Password.new(digest).is_password?(unencrypted_value) && self
end
define_method('%s=' % attribute_name) do |unencrypted_value|
if unencrypted_value.present?
write_attribute(digest_name, BCrypt::Password.create(unencrypted_value, cost: cost))
else
write_attribute(digest_name, nil)
end
instance_variable_set('@%s' % attribute_name, unencrypted_value)
end
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_name, :blank) unless read_attribute(digest_name).present?
end
validates attribute_name, length: { maximum: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED }
validates attribute_name, confirmation: { allow_blank: true }
end
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment