Skip to content

Instantly share code, notes, and snippets.

@todd
Created February 6, 2015 21:22
Show Gist options
  • Save todd/eba4d2fdb5c2ff4fa65a to your computer and use it in GitHub Desktop.
Save todd/eba4d2fdb5c2ff4fa65a to your computer and use it in GitHub Desktop.
SecureAttributes PoC
module SecureAttributes
extend ActiveSupport::Concern
included do
# Load bcrypt gem only when SecureAttributes 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
include ActiveModel::Validations
end
# BCrypt hash function can handle maximum 72 characters, and if we pass
# password of length more than 72 characters it ignores extra characters.
# Hence need to put a restriction on password length.
MAX_PASSWORD_LENGTH_ALLOWED = 72
class << self
attr_accessor :min_cost
end
self.min_cost = false
module ClassMethods
def has_secure_attribute(attribute, options = {})
attribute = attribute.to_sym
unless secure_attributes.include? attribute
secure_attributes << attribute
if options.fetch(:validations, true)
# 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.send("#{attribute}_digest").present?
end
validates_length_of attribute, maximum: MAX_PASSWORD_LENGTH_ALLOWED
validates_confirmation_of attribute, allow_blank: true
end
end
end
def secure_attributes
@secure_attributes ||= []
end
end
module InstanceMethodsOnActivation
def method_missing(method, *args, &block)
method = method.to_s
if method =~ /^(#{match_group})=$/
set_secure_attribute($1, args.first)
elsif method =~ /^compare_(#{match_group})$/
compare_secure_attribute($1, args.first)
else
super
end
end
def respond_to?(method, include_private = false)
method = method.to_s
if method =~ /^(#{match_group})=?$/
true
elsif method =~ /^compare_(#{match_group})$/
true
else
super
end
end
private
def match_group
self.class.secure_attributes.join('|')
end
def set_secure_attribute(attribute_name, value)
singleton_class.class_eval { attr_reader attribute_name }
if value.nil?
instance_variable_set("@#{attribute_name}", nil)
self.send("#{attribute_name}_digest=", nil)
else
instance_variable_set("@#{attribute_name}", value)
self.send("#{attribute_name}_digest=", encrypt(value))
end
end
def compare_secure_attribute(attribute_name, value)
BCrypt::Password.new(self.send("#{attribute_name}_digest")).is_password?(value) && self
end
def encrypt(unencrypted_data)
cost = SecureAttributes.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
BCrypt::Password.create(unencrypted_data, cost: cost)
end
end
end
class TestModel < ActiveRecord::Base
include SecureAttributes
has_secure_attribute :token, validations: true
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment