authgasm 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (87) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/Manifest +85 -0
  3. data/README.rdoc +164 -0
  4. data/Rakefile +15 -0
  5. data/authgasm.gemspec +183 -0
  6. data/init.rb +2 -0
  7. data/lib/authgasm.rb +18 -0
  8. data/lib/authgasm/acts_as_authentic.rb +200 -0
  9. data/lib/authgasm/controller.rb +16 -0
  10. data/lib/authgasm/session/active_record_trickery.rb +30 -0
  11. data/lib/authgasm/session/base.rb +365 -0
  12. data/lib/authgasm/session/callbacks.rb +47 -0
  13. data/lib/authgasm/session/config.rb +193 -0
  14. data/lib/authgasm/session/errors.rb +12 -0
  15. data/lib/authgasm/sha256_crypto_provider.rb +13 -0
  16. data/lib/authgasm/version.rb +56 -0
  17. data/test_app/README +256 -0
  18. data/test_app/Rakefile +10 -0
  19. data/test_app/app/controllers/application.rb +46 -0
  20. data/test_app/app/controllers/user_sessions_controller.rb +25 -0
  21. data/test_app/app/controllers/users_controller.rb +37 -0
  22. data/test_app/app/helpers/application_helper.rb +3 -0
  23. data/test_app/app/helpers/user_sessions_helper.rb +2 -0
  24. data/test_app/app/helpers/users_helper.rb +2 -0
  25. data/test_app/app/models/user.rb +3 -0
  26. data/test_app/app/models/user_session.rb +3 -0
  27. data/test_app/app/views/asses/edit.html.erb +12 -0
  28. data/test_app/app/views/asses/index.html.erb +18 -0
  29. data/test_app/app/views/asses/new.html.erb +11 -0
  30. data/test_app/app/views/asses/show.html.erb +3 -0
  31. data/test_app/app/views/layouts/application.html.erb +25 -0
  32. data/test_app/app/views/user_sessions/new.html.erb +13 -0
  33. data/test_app/app/views/users/_form.erb +15 -0
  34. data/test_app/app/views/users/edit.html.erb +8 -0
  35. data/test_app/app/views/users/new.html.erb +8 -0
  36. data/test_app/app/views/users/show.html.erb +19 -0
  37. data/test_app/config/boot.rb +109 -0
  38. data/test_app/config/database.yml +19 -0
  39. data/test_app/config/environment.rb +69 -0
  40. data/test_app/config/environments/development.rb +17 -0
  41. data/test_app/config/environments/production.rb +22 -0
  42. data/test_app/config/environments/test.rb +22 -0
  43. data/test_app/config/initializers/inflections.rb +10 -0
  44. data/test_app/config/initializers/mime_types.rb +5 -0
  45. data/test_app/config/initializers/new_rails_defaults.rb +17 -0
  46. data/test_app/config/routes.rb +7 -0
  47. data/test_app/db/development.sqlite3 +0 -0
  48. data/test_app/db/migrate/20081023040052_create_users.rb +17 -0
  49. data/test_app/db/schema.rb +25 -0
  50. data/test_app/db/test.sqlite3 +0 -0
  51. data/test_app/doc/README_FOR_APP +2 -0
  52. data/test_app/public/404.html +30 -0
  53. data/test_app/public/422.html +30 -0
  54. data/test_app/public/500.html +30 -0
  55. data/test_app/public/dispatch.cgi +10 -0
  56. data/test_app/public/dispatch.fcgi +24 -0
  57. data/test_app/public/dispatch.rb +10 -0
  58. data/test_app/public/favicon.ico +0 -0
  59. data/test_app/public/images/rails.png +0 -0
  60. data/test_app/public/javascripts/application.js +2 -0
  61. data/test_app/public/javascripts/controls.js +963 -0
  62. data/test_app/public/javascripts/dragdrop.js +972 -0
  63. data/test_app/public/javascripts/effects.js +1120 -0
  64. data/test_app/public/javascripts/prototype.js +4225 -0
  65. data/test_app/public/robots.txt +5 -0
  66. data/test_app/public/stylesheets/scaffold.css +62 -0
  67. data/test_app/script/about +4 -0
  68. data/test_app/script/console +3 -0
  69. data/test_app/script/dbconsole +3 -0
  70. data/test_app/script/destroy +3 -0
  71. data/test_app/script/generate +3 -0
  72. data/test_app/script/performance/benchmarker +3 -0
  73. data/test_app/script/performance/profiler +3 -0
  74. data/test_app/script/performance/request +3 -0
  75. data/test_app/script/plugin +3 -0
  76. data/test_app/script/process/inspector +3 -0
  77. data/test_app/script/process/reaper +3 -0
  78. data/test_app/script/process/spawner +3 -0
  79. data/test_app/script/runner +3 -0
  80. data/test_app/script/server +3 -0
  81. data/test_app/test/fixtures/users.yml +6 -0
  82. data/test_app/test/functional/user_sessions_controller_test.rb +15 -0
  83. data/test_app/test/functional/users_controller_test.rb +8 -0
  84. data/test_app/test/test_helper.rb +38 -0
  85. data/test_app/test/unit/ass_test.rb +8 -0
  86. data/test_app/test/unit/user_test.rb +8 -0
  87. metadata +182 -0
data/init.rb ADDED
@@ -0,0 +1,2 @@
1
+ require "digest/sha2"
2
+ require "authgasm"
data/lib/authgasm.rb ADDED
@@ -0,0 +1,18 @@
1
+ require File.dirname(__FILE__) + "/authgasm/version"
2
+ require File.dirname(__FILE__) + "/authgasm/controller"
3
+ require File.dirname(__FILE__) + "/authgasm/sha256_crypto_provider"
4
+ require File.dirname(__FILE__) + "/authgasm/acts_as_authentic"
5
+ require File.dirname(__FILE__) + "/authgasm/session/active_record_trickery"
6
+ require File.dirname(__FILE__) + "/authgasm/session/callbacks"
7
+ require File.dirname(__FILE__) + "/authgasm/session/config"
8
+ require File.dirname(__FILE__) + "/authgasm/session/errors"
9
+ require File.dirname(__FILE__) + "/authgasm/session/base"
10
+
11
+ module Authgasm
12
+ module Session
13
+ class Base
14
+ include ActiveRecordTrickery
15
+ include Callbacks
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,200 @@
1
+ module Authgasm
2
+ module ActsAsAuthenticated # :nodoc:
3
+ def self.included(base)
4
+ base.extend(ClassMethods)
5
+ end
6
+
7
+ # = Acts As Authentic
8
+ # Provides and "acts_as" method to include in your models to help with authentication. See method below.
9
+ module ClassMethods
10
+ # Call this method in your model to add in basic authentication madness:
11
+ #
12
+ # 1. Adds various validations for the login field
13
+ # 2. Adds various validations for the password field
14
+ # 3. Handles password encryption
15
+ # 4. Adds usefule methods to dealing with authentication
16
+ #
17
+ # === Methods
18
+ # For example purposes lets assume you have a User model.
19
+ #
20
+ # Class method name Description
21
+ # User.unique_token returns unique token generated by your :crypto_provider
22
+ # User.crypto_provider The class that you set in your :crypto_provider option
23
+ #
24
+ # Named Scopes
25
+ # User.logged_in Find all users who are logged in, based on your :logged_in_timeout option
26
+ # User.logged_out Same as above, but logged out
27
+ #
28
+ # Isntace method name
29
+ # user.password= Method name based on the :password_field option. This is used to set the password. Pass the *raw* password to this
30
+ # user.confirm_password= Confirms the password, needed to change the password
31
+ # 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.
32
+ # user.randomize_password! Basically resets the password to a random password using only letters and numbers
33
+ # user.logged_in? Based on the :logged_in_timeout option. Tells you if the user is logged in or not
34
+ #
35
+ # === Options
36
+ # * <tt>session_class:</tt> 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.
37
+ # * <tt>crypto_provider:</tt> default: Authgasm::Sha256CryptoProvider, class that provides Sha256 encryption. What ultimately encrypts your password.
38
+ # * <tt>crypto_provider_type:</tt> 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.
39
+ # * <tt>login_field:</tt> default: options[:session_class].login_field, the name of the field used for logging in
40
+ # * <tt>login_field_type:</tt> default: options[:login_field] == :email ? :email : :login, tells authgasm how to validation the field, what regex to use, etc.
41
+ # * <tt>password_field:</tt> default: options[:session_class].password_field, the name of the field to set the password, *NOT* the field the encrypted password is stored
42
+ # * <tt>crypted_password_field:</tt> 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.
43
+ # * <tt>password_salt_field:</tt> 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.
44
+ # * <tt>remember_token_field:</tt> 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"
45
+ # * <tt>logged_in_timeout:</tt> 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.
46
+ # * <tt>session_scopes:</tt> 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.
47
+ def acts_as_authentic(options = {})
48
+ # Setup default options
49
+ options[:session_class] ||= "#{name}Session".constantize
50
+ options[:crypto_provider] ||= Sha256CryptoProvider
51
+ options[:crypto_provider_type] ||= options[:crypto_provider].respond_to?(:decrypt) ? :encryption : :hash
52
+ options[:login_field] ||= options[:session_class].login_field
53
+ options[:login_field_type] ||= options[:login_field] == :email ? :email : :login
54
+ options[:password_field] ||= options[:session_class].password_field
55
+ options[:crypted_password_field] ||=
56
+ (columns.include?("crypted_password") && :crypted_password) ||
57
+ (columns.include?("encrypted_password") && :encrypted_password) ||
58
+ (columns.include?("password_hash") && :password_hash) ||
59
+ (columns.include?("pw_hash") && :pw_hash) ||
60
+ :crypted_password
61
+ options[:password_salt_field] ||=
62
+ (columns.include?("password_salt") && :password_salt) ||
63
+ (columns.include?("pw_salt") && :pw_salt) ||
64
+ (columns.include?("salt") && :salt) ||
65
+ :password_salt
66
+ options[:remember_token_field] ||= options[:session_class].remember_token_field
67
+ options[:logged_in_timeout] ||= 10.minutes
68
+ options[:session_scopes] ||= [nil]
69
+
70
+ # Validations
71
+ case options[:login_field_type]
72
+ when :email
73
+ validates_length_of options[:login_field], :within => 6..100
74
+ email_name_regex = '[\w\.%\+\-]+'
75
+ domain_head_regex = '(?:[A-Z0-9\-]+\.)+'
76
+ domain_tld_regex = '(?:[A-Z]{2}|com|org|net|edu|gov|mil|biz|info|mobi|name|aero|jobs|museum)'
77
+ email_regex = /\A#{email_name_regex}@#{domain_head_regex}#{domain_tld_regex}\z/i
78
+ validates_format_of options[:login_field], :with => email_regex, :message => "should look like an email address."
79
+ else
80
+ validates_length_of options[:login_field], :within => 2..100
81
+ validates_format_of options[:login_field], :with => /\A\w[\w\.\-_@]+\z/, :message => "use only letters, numbers, and .-_@ please."
82
+ end
83
+
84
+ validates_uniqueness_of options[:login_field]
85
+ validate :validate_password
86
+ validates_numericality_of :login_count, :only_integer => :true, :greater_than_or_equal_to => 0, :allow_nil => true if column_names.include?("login_count")
87
+
88
+ if column_names.include?("last_click_at")
89
+ named_scope :logged_in, lambda { {:conditions => ["last_click_at > ?", options[:logged_in_timeout].ago]} }
90
+ named_scope :logged_out, lambda { {:conditions => ["last_click_at <= ?", options[:logged_in_timeout].ago]} }
91
+ end
92
+
93
+ after_create :create_sessions!
94
+ after_create :update_sessions!
95
+
96
+ # Attributes
97
+ attr_writer "confirm_#{options[:password_field]}"
98
+ attr_accessor "tried_to_set_#{options[:password_field]}", :saving_from_session
99
+
100
+ # Class methods
101
+ class_eval <<-"end_eval", __FILE__, __LINE__
102
+ def self.unique_token
103
+ crypto_provider.encrypt(Time.now.to_s + (1..10).collect{ rand.to_s }.join)
104
+ end
105
+
106
+ def self.crypto_provider
107
+ #{options[:crypto_provider]}
108
+ end
109
+ end_eval
110
+
111
+ # Instance methods
112
+ if column_names.include?("last_click_at")
113
+ class_eval <<-"end_eval", __FILE__, __LINE__
114
+ def logged_in?
115
+ !last_click_at.nil? && last_click_at > #{options[:logged_in_timeout].to_i}.seconds.ago
116
+ end
117
+ end_eval
118
+ end
119
+
120
+ case options[:crypto_provider_type]
121
+ when :hash
122
+ class_eval <<-"end_eval", __FILE__, __LINE__
123
+ def #{options[:password_field]}=(pass)
124
+ return if pass.blank?
125
+ self.tried_to_set_#{options[:password_field]} = true
126
+ @#{options[:password_field]} = pass
127
+ salt = [Array.new(6) {rand(256).chr}.join].pack("m").chomp
128
+ self.#{options[:remember_token_field]} = self.class.unique_token
129
+ self.#{options[:password_salt_field]}, self.#{options[:crypted_password_field]} = salt, crypto_provider.encrypt(@#{options[:password_field]} + salt)
130
+ end
131
+
132
+ def valid_#{options[:password_field]}?(attempted_password)
133
+ attempted_password == #{options[:crypted_password_field]} || #{options[:crypted_password_field]} == crypto_provider.encrypt(attempted_password + #{options[:password_salt_field]})
134
+ end
135
+ end_eval
136
+ when :encryption
137
+ class_eval <<-"end_eval", __FILE__, __LINE__
138
+ def #{options[:password_field]}=(pass)
139
+ return if pass.blank?
140
+ self.tried_to_set_#{options[:password_field]} = true
141
+ @#{options[:password_field]} = pass
142
+ self.#{options[:remember_token_field]} = self.class.unique_token
143
+ self.#{options[:crypted_password_field]} = crypto_provider.encrypt(@#{options[:password_field]})
144
+ end
145
+
146
+ def valid_#{options[:password_field]}?(attemtped_password)
147
+ attempted_password == #{options[:crypted_password_field]} || #{options[:crypted_password_field]} = crypto_provider.decrypt(attempted_password)
148
+ end
149
+ end_eval
150
+ end
151
+
152
+ class_eval <<-"end_eval", __FILE__, __LINE__
153
+ def #{options[:password_field]}; end
154
+ def confirm_#{options[:password_field]}; end
155
+
156
+ def crypto_provider
157
+ self.class.crypto_provider
158
+ end
159
+
160
+ def randomize_#{options[:password_field]}!
161
+ chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
162
+ newpass = ""
163
+ 1.upto(10) { |i| newpass << chars[rand(chars.size-1)] }
164
+ self.#{options[:password_field]} = newpass
165
+ self.confirm_#{options[:password_field]} = newpass
166
+ end
167
+
168
+ protected
169
+ def create_sessions!
170
+ #{options[:session_scopes].inspect}.each { |scope| #{options[:session_class]}.create(self) }
171
+ end
172
+
173
+ def update_sessions!
174
+ #{options[:session_scopes].inspect}.each { |scope| #{options[:session_class]}.update(self) }
175
+ end
176
+
177
+ def saving_from_session?
178
+ saving_from_session == true
179
+ end
180
+
181
+ def tried_to_set_password?
182
+ tried_to_set_password == true
183
+ end
184
+
185
+ def validate_password
186
+ if new_record? || tried_to_set_#{options[:password_field]}?
187
+ if @#{options[:password_field]}.blank?
188
+ errors.add(:#{options[:password_field]}, "can not be blank")
189
+ else
190
+ errors.add(:confirm_#{options[:password_field]}, "did not match") if @confirm_#{options[:password_field]} != @#{options[:password_field]}
191
+ end
192
+ end
193
+ end
194
+ end_eval
195
+ end
196
+ end
197
+ end
198
+ end
199
+
200
+ ActiveRecord::Base.send(:include, Authgasm::ActsAsAuthenticated)
@@ -0,0 +1,16 @@
1
+ module Authgasm
2
+ # = Controller
3
+ # Adds a before_filter to set the controller object so that Authgasm can do its session and cookie magic
4
+ module Controller
5
+ def self.included(klass) # :nodoc:
6
+ klass.prepend_before_filter :set_controller
7
+ end
8
+
9
+ private
10
+ def set_controller
11
+ Authgasm::Session::Base.controller = self
12
+ end
13
+ end
14
+ end
15
+
16
+ ActionController::Base.send(:include, Authgasm::Controller)
@@ -0,0 +1,30 @@
1
+ module Authgasm
2
+ module Session
3
+ # = ActiveRecord Trickery
4
+ #
5
+ # Authgasm looks like ActiveRecord, sounds like ActiveRecord, but its not ActiveRecord. That's the goal here. This is useful for the various rails helper methods such as form_for, error_messages_for, etc.
6
+ # These helpers exptect various methods to be present. This adds in those methods into Authgasm.
7
+ module ActiveRecordTrickery
8
+ def self.included(klass) # :nodoc:
9
+ klass.extend ClassMethods
10
+ klass.send(:include, InstanceMethods)
11
+ end
12
+
13
+ module ClassMethods # :nodoc:
14
+ def human_attribute_name(attribute_key_name, options = {})
15
+ attribute_key_name.humanize
16
+ end
17
+ end
18
+
19
+ module InstanceMethods # :nodoc:
20
+ def id
21
+ nil
22
+ end
23
+
24
+ def new_record?
25
+ true
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,365 @@
1
+ module Authgasm
2
+ module Session # :nodoc:
3
+ # = Base
4
+ #
5
+ # This is the muscle behind Authgasm. For detailed information on how to use this please refer to the README. For detailed method explanations see below.
6
+ class Base
7
+ include Config
8
+
9
+ class << self
10
+ # Returns true if a controller have been set and can be used properly.
11
+ def activated?
12
+ !controller.blank?
13
+ end
14
+
15
+ def controller=(value) # :nodoc:
16
+ controllers[Thread.current] = value
17
+ end
18
+
19
+ def controller # :nodoc:
20
+ controllers[Thread.current]
21
+ end
22
+
23
+ # A convenince method. The same as:
24
+ #
25
+ # session = UserSession.new
26
+ # session.create
27
+ def create(*args)
28
+ session = new(*args)
29
+ session.create
30
+ end
31
+
32
+ # Same as create but calls create!, which raises an exception when authentication fails
33
+ def create!(*args)
34
+ session = new(*args)
35
+ session.create!
36
+ end
37
+
38
+ # Finds your session by session, then cookie, and finally basic http auth. Perfect for that global before_filter to find your logged in user:
39
+ #
40
+ # before_filter :load_user
41
+ #
42
+ # def load_user
43
+ # @user_session = UserSession.find
44
+ # @current_user = @user_session && @user_session.record
45
+ # end
46
+ #
47
+ # Accepts a single parameter as the scope. See initialize for more information on scopes.
48
+ def find(scope = nil)
49
+ args = [scope].compact
50
+ session = new(*args)
51
+ return session if session.valid_session? || session.valid_cookie?(true) || session.valid_http_auth?(true)
52
+ nil
53
+ end
54
+
55
+ def klass # :nodoc:
56
+ @klass ||=
57
+ if klass_name
58
+ klass_name.constantize
59
+ else
60
+ nil
61
+ end
62
+ end
63
+
64
+ def klass_name # :nodoc:
65
+ @klass_name ||=
66
+ if guessed_name = name.scan(/(.*)Session/)[0]
67
+ @klass_name = guessed_name[0]
68
+ end
69
+ end
70
+
71
+ # Convenience method. The same as:
72
+ #
73
+ # session = UserSession.new
74
+ # session.update
75
+ def update(*args)
76
+ session = new(*args)
77
+ session.update
78
+ end
79
+
80
+ # The same as update but calls update!, which raises an exception when authentication fails
81
+ def update!(*args)
82
+ session = new(*args)
83
+ session.update!
84
+ end
85
+
86
+ private
87
+ def controllers
88
+ @@controllers ||= {}
89
+ end
90
+ end
91
+
92
+ attr_accessor :login_with, :remember_me, :scope
93
+ attr_reader :record, :unauthorized_record
94
+
95
+ # You can initialize a session by doing any of the following:
96
+ #
97
+ # UserSession.new
98
+ # UserSession.new(login, password)
99
+ # UserSession.new(:login => login, :password => password)
100
+ #
101
+ # If a user has more than one session you need to pass a scope so that Authgasm knows how to differentiate the sessions. The scope MUST be a Symbol.
102
+ #
103
+ # UserSession.new(:my_scope)
104
+ # UserSession.new(login, password, :my_scope)
105
+ # UserSession.new({:login => loing, :password => password}, :my_scope)
106
+ #
107
+ # Scopes are rarely used, but they can be useful. For example, what if users allow other users to login into their account via proxy? Now that user can "technically" be logged into 2 accounts at once.
108
+ # To solve this just pass a scope called :proxy, or whatever you want. Authgasm will separate everything out.s
109
+ def initialize(*args)
110
+ create_configurable_methods!
111
+
112
+ self.scope = args.pop if args.last.is_a?(Symbol)
113
+
114
+ case args.size
115
+ when 1
116
+ credentials_or_record = args.first
117
+ case credentials_or_record
118
+ when Hash
119
+ self.credentials = credentials_or_record
120
+ else
121
+ self.unauthorized_record = credentials_or_record
122
+ end
123
+ else
124
+ send("#{login_field}=", args[0])
125
+ send("#{password_field}=", args[1])
126
+ self.remember_me = args[2]
127
+ end
128
+ end
129
+
130
+ # Creates a new user session for you. It does all of the magic:
131
+ #
132
+ # 1. validates
133
+ # 2. sets session
134
+ # 3. sets cookie
135
+ # 4. updates magic fields
136
+ def create(updating = false)
137
+ if valid?(true)
138
+ cookies[cookie_key] = {
139
+ :value => record.send(remember_token_field),
140
+ :expires => remember_me? ? remember_me_for.from_now : nil
141
+ }
142
+
143
+ if !updating
144
+ record.login_count = record.login_count + 1 if record.respond_to?(:login_count)
145
+
146
+ if record.respond_to?(:current_login_at)
147
+ record.last_login_at = record.current_login_at if record.respond_to?(:last_login_at)
148
+ record.current_login_at = Time.now
149
+ end
150
+
151
+ if record.respond_to?(:current_login_ip)
152
+ record.last_login_ip = record.current_login_ip if record.respond_to?(:last_login_ip)
153
+ record.current_login_ip = controller.request.remote_ip
154
+ end
155
+
156
+ record.saving_from_session = true
157
+ record.save(false)
158
+ end
159
+
160
+ self
161
+ end
162
+ end
163
+
164
+ # Same as create but raises an exception when authentication fails
165
+ def create!(updating = false)
166
+ raise SessionInvalid.new(self) unless create(updating)
167
+ end
168
+ alias_method :start!, :create!
169
+
170
+ # Your login credentials in hash format. Usually {:login => "my login", :password => "<protected>"} depending on your configuration.
171
+ # Password is protected as a security measure. The raw password should never be publicly accessible.
172
+ def credentials
173
+ {login_field => send(login_field), password_field => "<Protected>"}
174
+ end
175
+
176
+ # Lets you set your loging and password via a hash format.
177
+ def credentials=(values)
178
+ values.symbolize_keys!
179
+ raise(ArgumentError, "Only 2 credentials are allowed: #{login_field} and #{password_field}") if !values.is_a?(Hash) || values.keys.size > 2 || !values.key?(login_field) || !values.key?(password_field)
180
+ values.each { |field, value| send("#{field}=", value) }
181
+ end
182
+
183
+ # Resets everything, your errors, record, cookies, and session. Basically "logs out" a user.
184
+ def destroy
185
+ errors.clear
186
+ @record = nil
187
+ cookies.delete cookie_key
188
+ session[session_key] = nil
189
+ end
190
+
191
+ # Errors when authentication fails, just like ActiveRecord errors. In fact it uses the same exact class.
192
+ def errors
193
+ @errors ||= Errors.new(self)
194
+ end
195
+
196
+ def inspect # :nodoc:
197
+ details = {}
198
+ case login_with
199
+ when :unauthorized_record
200
+ details[:unauthorized_record] = unauthorized_record
201
+ else
202
+ details[login_field.to_sym] = send(login_field)
203
+ details[password_field.to_sym] = "<protected>"
204
+ end
205
+ "#<#{self.class.name} #{details.inspect}>"
206
+ end
207
+
208
+ # Allows users to be remembered via a cookie.
209
+ def remember_me?
210
+ remember_me == true || remember_me = "true" || remember_me == "1"
211
+ end
212
+
213
+ # When to expire the cookie. See remember_me_for configuration option to change this.
214
+ def remember_me_until
215
+ remember_me_for.from_now
216
+ end
217
+
218
+ # Sometimes you don't want to create a session via credentials (login and password). Maybe you already have the record. Just set this record to this and it will be authenticated when you try to validate
219
+ # the session. Basically this is another form of credentials, you are just skipping username and password validation.
220
+ def unauthorized_record=(value)
221
+ self.login_with = :unauthorized_record
222
+ @unauthorized_record = value
223
+ end
224
+
225
+ # Updates the session with any new information. Resets the session and cookie.
226
+ def update
227
+ create(true)
228
+ end
229
+
230
+ # Same as update but raises an exception if validation is failed
231
+ def update!
232
+ create!(true)
233
+ end
234
+
235
+ def valid?(set_session = false)
236
+ errors.clear
237
+ temp_record = unauthorized_record
238
+
239
+ if login_with == :credentials
240
+ errors.add(login_field, "can not be blank") if login.blank?
241
+ errors.add(password_field, "can not be blank") if protected_password.blank?
242
+ return false if errors.count > 0
243
+
244
+ temp_record = klass.send(find_by_login_method, send(login_field))
245
+
246
+ if temp_record.blank?
247
+ errors.add(login_field, "was not found")
248
+ return false
249
+ end
250
+
251
+ unless temp_record.send(verify_password_method, protected_password)
252
+ errors.add(password_field, "is invalid")
253
+ return false
254
+ end
255
+ end
256
+
257
+ [:approved, :confirmed, :inactive].each do |required_status|
258
+ if temp_record.respond_to?("#{required_status}?") && !temp_record.send("#{required_status}?")
259
+ errors.add_to_base("Your account has not been #{required_status}")
260
+ return false
261
+ end
262
+ end
263
+
264
+ # All is good, lets set the record
265
+ @record = temp_record
266
+
267
+ # Now lets set the session to make things easier on successive requests. This is nice when logging in from a cookie, the next requests will be right from the session, which is quicker.
268
+ if set_session
269
+ session[session_key] = record.id
270
+ if record.class.column_names.include?("last_click_at")
271
+ record.last_click_at = Time.now
272
+ record.saving_from_session = true
273
+ record.save(false)
274
+ end
275
+ end
276
+
277
+ true
278
+ end
279
+
280
+ def valid_http_auth?(set_session = false)
281
+ controller.authenticate_with_http_basic do |login, password|
282
+ if !login.blank? && !password.blank?
283
+ send("#{login_method}=", login)
284
+ send("#{password_method}=", password)
285
+ return valid?(set_session)
286
+ end
287
+ end
288
+
289
+ false
290
+ end
291
+
292
+ def valid_cookie?(set_session = false)
293
+ if cookie_credentials
294
+ self.unauthorized_record = klass.send("find_by_#{remember_token_field}", cookie_credentials)
295
+ valid?(set_session)
296
+ end
297
+
298
+ false
299
+ end
300
+
301
+ def valid_session?
302
+ if session_credentials
303
+ self.unauthorized_record = klass.find_by_id(session_credentials)
304
+ return valid?
305
+ end
306
+
307
+ false
308
+ end
309
+
310
+ private
311
+ def controller
312
+ self.class.controller
313
+ end
314
+
315
+ def cookies
316
+ controller.send(:cookies)
317
+ end
318
+
319
+ def cookie_credentials
320
+ cookies[cookie_key]
321
+ end
322
+
323
+ def create_configurable_methods!
324
+ return if respond_to?(login_field) # already created these methods
325
+
326
+ self.class.class_eval <<-"end_eval", __FILE__, __LINE__
327
+ attr_reader :#{login_field}
328
+
329
+ def #{login_field}=(value)
330
+ self.login_with = :credentials
331
+ @#{login_field} = value
332
+ end
333
+
334
+ def #{password_field}=(value)
335
+ self.login_with = :credentials
336
+ @#{password_field} = value
337
+ end
338
+
339
+ def #{password_field}; end
340
+ end_eval
341
+ end
342
+
343
+ def klass
344
+ self.class.klass
345
+ end
346
+
347
+ def klass_name
348
+ self.class.klass_name
349
+ end
350
+
351
+ # The password should not be accessible publicly. This way forms using form_for don't fill the password with the attempted password. The prevent this we just create this method that is private.
352
+ def protected_password
353
+ @password
354
+ end
355
+
356
+ def session
357
+ controller.session
358
+ end
359
+
360
+ def session_credentials
361
+ session[session_key]
362
+ end
363
+ end
364
+ end
365
+ end