Created
February 6, 2015 21:22
-
-
Save todd/eba4d2fdb5c2ff4fa65a to your computer and use it in GitHub Desktop.
SecureAttributes PoC
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
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 |
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
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