Created
November 15, 2018 04:39
-
-
Save br3nt/f802fb252ac3110b74841c6a183ac57c to your computer and use it in GitHub Desktop.
Better Implementation of Rails' ActiveModel::SecurePassword
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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