module Authgasm module ActsAsAuthenticated # :nodoc: def self.included(base) base.extend(ClassMethods) end # = Acts As Authentic # Provides and "acts_as" method to include in your models to help with authentication. See method below. module ClassMethods # Call this method in your model to add in basic authentication madness: # # 1. Adds various validations for the login field # 2. Adds various validations for the password field # 3. Handles password encryption # 4. Adds usefule methods to dealing with authentication # # === Methods # For example purposes lets assume you have a User model. # # Class method name Description # User.unique_token returns unique token generated by your :crypto_provider # User.crypto_provider The class that you set in your :crypto_provider option # # Named Scopes # User.logged_in Find all users who are logged in, based on your :logged_in_timeout option # User.logged_out Same as above, but logged out # # Isntace method name # user.password= Method name based on the :password_field option. This is used to set the password. Pass the *raw* password to this # user.confirm_password= Confirms the password, needed to change the password # user.valid_password?(pass) Based on the valid of :password_field. Determines if the password passed is valid. The password could be encrypted or raw. # user.randomize_password! Basically resets the password to a random password using only letters and numbers # user.logged_in? Based on the :logged_in_timeout option. Tells you if the user is logged in or not # # === Options # * session_class: default: "#{name}Session", the related session class. Used so that you don't have to repeat yourself here. A lot of the configuration will be based off of the configuration values of this class. # * crypto_provider: default: Authgasm::Sha256CryptoProvider, class that provides Sha256 encryption. What ultimately encrypts your password. # * crypto_provider_type: default: options[:crypto_provider].respond_to?(:decrypt) ? :encryption : :hash. You can explicitly set this if you wish. Since encryptions and hashes are handled different this is the flag Authgasm uses. # * login_field: default: options[:session_class].login_field, the name of the field used for logging in # * login_field_type: default: options[:login_field] == :email ? :email : :login, tells authgasm how to validation the field, what regex to use, etc. # * password_field: default: options[:session_class].password_field, the name of the field to set the password, *NOT* the field the encrypted password is stored # * crypted_password_field: default: depends on which columns are present, checks: crypted_password, encrypted_password, password_hash, pw_hash, if none are present defaults to crypted_password. This is the name of column that your encrypted password is stored. # * password_salt_field: default: depends on which columns are present, checks: password_salt, pw_salt, salt, if none are present defaults to password_salt. This is the name of the field your salt is stored, only relevant for a hash crypto provider. # * remember_token_field: default: options[:session_class].remember_token_field, the name of the field your remember token is stored. What the cookie stores so the session can be "remembered" # * logged_in_timeout: default: 10.minutes, this allows you to specify a time the determines if a user is logged in or out. Useful if you want to count how many users are currently logged in. # * session_scopes: default: [nil], the sessions that we want to automatically reset when a user is created or updated so you don't have to worry about this. Set to [] to disable. Should be an array of scopes. See Authgasm::Session::Base#initialize for information on scopes. def acts_as_authentic(options = {}) # Setup default options options[:session_class] ||= "#{name}Session".constantize options[:crypto_provider] ||= Sha256CryptoProvider options[:crypto_provider_type] ||= options[:crypto_provider].respond_to?(:decrypt) ? :encryption : :hash options[:login_field] ||= options[:session_class].login_field options[:login_field_type] ||= options[:login_field] == :email ? :email : :login options[:password_field] ||= options[:session_class].password_field options[:crypted_password_field] ||= (columns.include?("crypted_password") && :crypted_password) || (columns.include?("encrypted_password") && :encrypted_password) || (columns.include?("password_hash") && :password_hash) || (columns.include?("pw_hash") && :pw_hash) || :crypted_password options[:password_salt_field] ||= (columns.include?("password_salt") && :password_salt) || (columns.include?("pw_salt") && :pw_salt) || (columns.include?("salt") && :salt) || :password_salt options[:remember_token_field] ||= options[:session_class].remember_token_field options[:logged_in_timeout] ||= 10.minutes options[:session_scopes] ||= [nil] # Validations case options[:login_field_type] when :email validates_length_of options[:login_field], :within => 6..100 email_name_regex = '[\w\.%\+\-]+' domain_head_regex = '(?:[A-Z0-9\-]+\.)+' domain_tld_regex = '(?:[A-Z]{2}|com|org|net|edu|gov|mil|biz|info|mobi|name|aero|jobs|museum)' email_regex = /\A#{email_name_regex}@#{domain_head_regex}#{domain_tld_regex}\z/i validates_format_of options[:login_field], :with => email_regex, :message => "should look like an email address." else validates_length_of options[:login_field], :within => 2..100 validates_format_of options[:login_field], :with => /\A\w[\w\.\-_@]+\z/, :message => "use only letters, numbers, and .-_@ please." end validates_uniqueness_of options[:login_field] validate :validate_password validates_numericality_of :login_count, :only_integer => :true, :greater_than_or_equal_to => 0, :allow_nil => true if column_names.include?("login_count") if column_names.include?("last_click_at") named_scope :logged_in, lambda { {:conditions => ["last_click_at > ?", options[:logged_in_timeout].ago]} } named_scope :logged_out, lambda { {:conditions => ["last_click_at <= ?", options[:logged_in_timeout].ago]} } end after_create :create_sessions! after_create :update_sessions! # Attributes attr_writer "confirm_#{options[:password_field]}" attr_accessor "tried_to_set_#{options[:password_field]}", :saving_from_session # Class methods class_eval <<-"end_eval", __FILE__, __LINE__ def self.unique_token crypto_provider.encrypt(Time.now.to_s + (1..10).collect{ rand.to_s }.join) end def self.crypto_provider #{options[:crypto_provider]} end end_eval # Instance methods if column_names.include?("last_click_at") class_eval <<-"end_eval", __FILE__, __LINE__ def logged_in? !last_click_at.nil? && last_click_at > #{options[:logged_in_timeout].to_i}.seconds.ago end end_eval end case options[:crypto_provider_type] when :hash class_eval <<-"end_eval", __FILE__, __LINE__ def #{options[:password_field]}=(pass) return if pass.blank? self.tried_to_set_#{options[:password_field]} = true @#{options[:password_field]} = pass salt = [Array.new(6) {rand(256).chr}.join].pack("m").chomp self.#{options[:remember_token_field]} = self.class.unique_token self.#{options[:password_salt_field]}, self.#{options[:crypted_password_field]} = salt, crypto_provider.encrypt(@#{options[:password_field]} + salt) end def valid_#{options[:password_field]}?(attempted_password) attempted_password == #{options[:crypted_password_field]} || #{options[:crypted_password_field]} == crypto_provider.encrypt(attempted_password + #{options[:password_salt_field]}) end end_eval when :encryption class_eval <<-"end_eval", __FILE__, __LINE__ def #{options[:password_field]}=(pass) return if pass.blank? self.tried_to_set_#{options[:password_field]} = true @#{options[:password_field]} = pass self.#{options[:remember_token_field]} = self.class.unique_token self.#{options[:crypted_password_field]} = crypto_provider.encrypt(@#{options[:password_field]}) end def valid_#{options[:password_field]}?(attemtped_password) attempted_password == #{options[:crypted_password_field]} || #{options[:crypted_password_field]} = crypto_provider.decrypt(attempted_password) end end_eval end class_eval <<-"end_eval", __FILE__, __LINE__ def #{options[:password_field]}; end def confirm_#{options[:password_field]}; end def crypto_provider self.class.crypto_provider end def randomize_#{options[:password_field]}! chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a newpass = "" 1.upto(10) { |i| newpass << chars[rand(chars.size-1)] } self.#{options[:password_field]} = newpass self.confirm_#{options[:password_field]} = newpass end protected def create_sessions! #{options[:session_scopes].inspect}.each { |scope| #{options[:session_class]}.create(self) } end def update_sessions! #{options[:session_scopes].inspect}.each { |scope| #{options[:session_class]}.update(self) } end def saving_from_session? saving_from_session == true end def tried_to_set_password? tried_to_set_password == true end def validate_password if new_record? || tried_to_set_#{options[:password_field]}? if @#{options[:password_field]}.blank? errors.add(:#{options[:password_field]}, "can not be blank") else errors.add(:confirm_#{options[:password_field]}, "did not match") if @confirm_#{options[:password_field]} != @#{options[:password_field]} end end end end_eval end end end end ActiveRecord::Base.send(:include, Authgasm::ActsAsAuthenticated)