authgasm 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +20 -0
- data/Manifest +85 -0
- data/README.rdoc +164 -0
- data/Rakefile +15 -0
- data/authgasm.gemspec +183 -0
- data/init.rb +2 -0
- data/lib/authgasm.rb +18 -0
- data/lib/authgasm/acts_as_authentic.rb +200 -0
- data/lib/authgasm/controller.rb +16 -0
- data/lib/authgasm/session/active_record_trickery.rb +30 -0
- data/lib/authgasm/session/base.rb +365 -0
- data/lib/authgasm/session/callbacks.rb +47 -0
- data/lib/authgasm/session/config.rb +193 -0
- data/lib/authgasm/session/errors.rb +12 -0
- data/lib/authgasm/sha256_crypto_provider.rb +13 -0
- data/lib/authgasm/version.rb +56 -0
- data/test_app/README +256 -0
- data/test_app/Rakefile +10 -0
- data/test_app/app/controllers/application.rb +46 -0
- data/test_app/app/controllers/user_sessions_controller.rb +25 -0
- data/test_app/app/controllers/users_controller.rb +37 -0
- data/test_app/app/helpers/application_helper.rb +3 -0
- data/test_app/app/helpers/user_sessions_helper.rb +2 -0
- data/test_app/app/helpers/users_helper.rb +2 -0
- data/test_app/app/models/user.rb +3 -0
- data/test_app/app/models/user_session.rb +3 -0
- data/test_app/app/views/asses/edit.html.erb +12 -0
- data/test_app/app/views/asses/index.html.erb +18 -0
- data/test_app/app/views/asses/new.html.erb +11 -0
- data/test_app/app/views/asses/show.html.erb +3 -0
- data/test_app/app/views/layouts/application.html.erb +25 -0
- data/test_app/app/views/user_sessions/new.html.erb +13 -0
- data/test_app/app/views/users/_form.erb +15 -0
- data/test_app/app/views/users/edit.html.erb +8 -0
- data/test_app/app/views/users/new.html.erb +8 -0
- data/test_app/app/views/users/show.html.erb +19 -0
- data/test_app/config/boot.rb +109 -0
- data/test_app/config/database.yml +19 -0
- data/test_app/config/environment.rb +69 -0
- data/test_app/config/environments/development.rb +17 -0
- data/test_app/config/environments/production.rb +22 -0
- data/test_app/config/environments/test.rb +22 -0
- data/test_app/config/initializers/inflections.rb +10 -0
- data/test_app/config/initializers/mime_types.rb +5 -0
- data/test_app/config/initializers/new_rails_defaults.rb +17 -0
- data/test_app/config/routes.rb +7 -0
- data/test_app/db/development.sqlite3 +0 -0
- data/test_app/db/migrate/20081023040052_create_users.rb +17 -0
- data/test_app/db/schema.rb +25 -0
- data/test_app/db/test.sqlite3 +0 -0
- data/test_app/doc/README_FOR_APP +2 -0
- data/test_app/public/404.html +30 -0
- data/test_app/public/422.html +30 -0
- data/test_app/public/500.html +30 -0
- data/test_app/public/dispatch.cgi +10 -0
- data/test_app/public/dispatch.fcgi +24 -0
- data/test_app/public/dispatch.rb +10 -0
- data/test_app/public/favicon.ico +0 -0
- data/test_app/public/images/rails.png +0 -0
- data/test_app/public/javascripts/application.js +2 -0
- data/test_app/public/javascripts/controls.js +963 -0
- data/test_app/public/javascripts/dragdrop.js +972 -0
- data/test_app/public/javascripts/effects.js +1120 -0
- data/test_app/public/javascripts/prototype.js +4225 -0
- data/test_app/public/robots.txt +5 -0
- data/test_app/public/stylesheets/scaffold.css +62 -0
- data/test_app/script/about +4 -0
- data/test_app/script/console +3 -0
- data/test_app/script/dbconsole +3 -0
- data/test_app/script/destroy +3 -0
- data/test_app/script/generate +3 -0
- data/test_app/script/performance/benchmarker +3 -0
- data/test_app/script/performance/profiler +3 -0
- data/test_app/script/performance/request +3 -0
- data/test_app/script/plugin +3 -0
- data/test_app/script/process/inspector +3 -0
- data/test_app/script/process/reaper +3 -0
- data/test_app/script/process/spawner +3 -0
- data/test_app/script/runner +3 -0
- data/test_app/script/server +3 -0
- data/test_app/test/fixtures/users.yml +6 -0
- data/test_app/test/functional/user_sessions_controller_test.rb +15 -0
- data/test_app/test/functional/users_controller_test.rb +8 -0
- data/test_app/test/test_helper.rb +38 -0
- data/test_app/test/unit/ass_test.rb +8 -0
- data/test_app/test/unit/user_test.rb +8 -0
- metadata +182 -0
data/init.rb
ADDED
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
|