public
Description: A simple model based ruby authentication solution.
Homepage: http://rdoc.info/projects/binarylogic/authlogic
Clone URL: git://github.com/binarylogic/authlogic.git
Click here to lend your support to: authlogic and make a donation at www.pledgie.com !
authlogic / lib / authlogic / session / base.rb
100644 468 lines (400 sloc) 18.881 kb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
module Authlogic
  module Session # :nodoc:
    # = Base
    #
    # This is the muscle behind Authlogic. For detailed information on how to use this please refer to the README. For detailed method explanations see below.
    class Base
      include Config
      
      class << self
        attr_accessor :methods_configured
        
        # Returns true if a controller has been set and can be used properly. This MUST be set before anything can be done. Similar to how ActiveRecord won't allow you to do anything
        # without establishing a DB connection. In your framework environment this is done for you, but if you are using Authlogic outside of your framework, you need to assign a controller
        # object to Authlogic via Authlogic::Session::Base.controller = obj. See the controller= method for more information.
        def activated?
          !controller.nil?
        end
        
        # This accepts a controller object wrapped with the Authlogic controller adapter. The controller adapters close the gap between the different controllers in each framework.
        # That being said, Authlogic is expecting your object's class to extend Authlogic::ControllerAdapters::AbstractAdapter. See Authlogic::ControllerAdapters for more info.
        def controller=(value)
          Thread.current[:authlogic_controller] = value
        end
        
        def controller # :nodoc:
          Thread.current[:authlogic_controller]
        end
        
        # A convenince method. The same as:
        #
        # session = UserSession.new
        # session.create
        def create(*args, &block)
          session = new(*args)
          session.save(&block)
        end
        
        # Same as create but calls create!, which raises an exception when authentication fails.
        def create!(*args)
          session = new(*args)
          session.save!
        end
        
        # A convenience method for session.find_record. Finds your session by parameters, then session, then cookie, and finally by basic http auth.
        # This is perfect for persisting your session:
        #
        # helper_method :current_user_session, :current_user
        #
        # def current_user_session
        # return @current_user_session if defined?(@current_user_session)
        # @current_user_session = UserSession.find
        # end
        #
        # def current_user
        # return @current_user if defined?(@current_user)
        # @current_user = current_user_session && current_user_session.user
        # end
        #
        # Accepts a single parameter as the id, to find session that you marked with an id:
        #
        # UserSession.find(:secure)
        #
        # See the id method for more information on ids.
        def find(id = nil)
          session = new(id)
          session.before_find
          if session.find_record
            session.after_find
            session
          else
            nil
          end
        end
        
        # The name of the class that this session is authenticating with. For example, the UserSession class will authenticate with the User class
        # unless you specify otherwise in your configuration. See authenticate_with for information on how to change this value.
        def klass
          @klass ||=
            if klass_name
              klass_name.constantize
            else
              nil
            end
        end
        
        # Same as klass, just returns a string instead of the actual constant.
        def klass_name
          @klass_name ||=
            if guessed_name = name.scan(/(.*)Session/)[0]
              @klass_name = guessed_name[0]
            end
        end
      end
      
      attr_accessor :new_session
      attr_reader :record, :unauthorized_record
      attr_writer :authenticating_with, :id, :persisting
    
      # You can initialize a session by doing any of the following:
      #
      # UserSession.new
      # UserSession.new(:login => "login", :password => "password", :remember_me => true)
      # UserSession.new(User.first, true)
      #
      # If a user has more than one session you need to pass an id so that Authlogic knows how to differentiate the sessions. The id MUST be a Symbol.
      #
      # UserSession.new(:my_id)
      # UserSession.new({:login => "login", :password => "password", :remember_me => true}, :my_id)
      # UserSession.new(User.first, true, :my_id)
      #
      # For more information on ids see the id method.
      #
      # Lastly, the reason the id is separate from the first parameter hash is becuase this should be controlled by you, not by what the user passes.
      # A user could inject their own id and things would not work as expected.
      def initialize(*args)
        raise NotActivated.new(self) unless self.class.activated?
        
        create_configurable_methods!
        
        self.id = args.pop if args.last.is_a?(Symbol)
        
        if args.first.is_a?(Hash)
          self.credentials = args.first
        elsif !args.first.blank? && args.first.class < ::ActiveRecord::Base
          self.unauthorized_record = args.first
          self.remember_me = args[1] if args.size > 1
        end
      end
      
      # A flag for how the user is logging in. Possible values:
      #
      # * <tt>:password</tt> - username and password
      # * <tt>:unauthorized_record</tt> - an actual ActiveRecord object
      #
      # By default this is :password
      def authenticating_with
        @authenticating_with ||= :password
      end
      
      # Returns true if logging in with credentials. Credentials mean username and password.
      def authenticating_with_password?
        authenticating_with == :password
      end
      
      # Returns true if logging in with an unauthorized record
      def authenticating_with_unauthorized_record?
        authenticating_with == :unauthorized_record
      end
      alias_method :authenticating_with_record?, :authenticating_with_unauthorized_record?
      
      # Your login credentials in hash format. Usually {:login => "my login", :password => "<protected>"} depending on your configuration.
      # Password is protected as a security measure. The raw password should never be publicly accessible.
      def credentials
        {login_field => send(login_field), password_field => "<Protected>"}
      end
      
      # Lets you set your loging and password via a hash format. This is "params" safe. It only allows for 3 keys: your login field name, password field name, and remember me.
      def credentials=(values)
        return if values.blank? || !values.is_a?(Hash)
        values.symbolize_keys!
        values.each do |field, value|
          next if value.blank?
          send("#{field}=", value)
        end
      end
      
      # Resets everything, your errors, record, cookies, and session. Basically "logs out" a user.
      def destroy
        before_destroy
        
        errors.clear
        @record = nil
        
        after_destroy
        
        true
      end
      
      # The errors in Authlogic work JUST LIKE ActiveRecord. In fact, it uses the exact same ActiveRecord errors class. Use it the same way:
      #
      # === Example
      #
      # class UserSession
      # before_validation :check_if_awesome
      #
      # private
      # def check_if_awesome
      # errors.add(:login, "must contain awesome") if login && !login.include?("awesome")
      # errors.add_to_base("You must be awesome to log in") unless record.awesome?
      # end
      # end
      def errors
        @errors ||= Errors.new(self)
      end
      
      # Attempts to find the record by params, then session, then cookie, and finally basic http auth. See the class level find method if you are wanting to use this to persist your session.
      def find_record
        if record
          self.new_session = false
          return record
        end
        
        find_with.each do |find_method|
          if send("valid_#{find_method}?")
            self.new_session = false
            return record
          end
        end
        nil
      end
      
      # Allows you to set a unique identifier for your session, so that you can have more than 1 session at a time. A good example when this might be needed is when you want to have a normal user session
      # and a "secure" user session. The secure user session would be created only when they want to modify their billing information, or other sensitive information. Similar to me.com. This requires 2
      # user sessions. Just use an id for the "secure" session and you should be good.
      #
      # You can set the id during initialization (see initialize for more information), or as an attribute:
      #
      # session.id = :my_id
      #
      # Just be sure and set your id before you save your session.
      #
      # Lastly, to retrieve your session with the id check out the find class method.
      def id
        @id
      end
      
      def inspect # :nodoc:
        details = {}
        case authenticating_with
        when :unauthorized_record
          details[:unauthorized_record] = "<protected>"
        else
          details[login_field.to_sym] = send(login_field)
          details[password_field.to_sym] = "<protected>"
        end
        "#<#{self.class.name} #{details.inspect}>"
      end
      
      # Similar to ActiveRecord's new_record? Returns true if the session has not been saved yet.
      def new_session?
        new_session != false
      end
      
      def persisting # :nodoc:
        return @persisting if defined?(@persisting)
        @persisting = true
      end
      
      # Returns true if the session is being persisted. This is set to false if the session was found by the single_access_token, since logging in via a single access token should not remember the user in the
      # session or the cookie.
      def persisting?
        persisting == true
      end
      
      def remember_me # :nodoc:
        return @remember_me if defined?(@remember_me)
        @remember_me = self.class.remember_me
      end
      
      # Accepts a boolean as a flag to remember the session or not. Basically to expire the cookie at the end of the session or keep it for "remember_me_until".
      def remember_me=(value)
        @remember_me = value
      end
      
      # Allows users to be remembered via a cookie.
      def remember_me?
        remember_me == true || remember_me == "true" || remember_me == "1"
      end
      
      # When to expire the cookie. See remember_me_for configuration option to change this.
      def remember_me_until
        return unless remember_me?
        remember_me_for.from_now
      end
      
      # Creates / updates a new user session for you. It does all of the magic:
      #
      # 1. validates
      # 2. sets session
      # 3. sets cookie
      # 4. updates magic fields
      def save(&block)
        result = nil
        if valid?
          # hooks
          before_save
          new_session? ? before_create : before_update
          new_session? ? after_create : after_update
          after_save
          
          self.new_session = false
          result = self
        else
          result = false
        end
        
        yield result if block_given?
        result
      end
      
      # Same as save but raises an exception when authentication fails
      def save!
        result = save
        raise SessionInvalid.new(self) unless result
        result
      end
      
      # This lets you create a session by passing a single object of whatever you are authenticating. Let's say User. By passing a user object you are vouching for this user and saying you can guarantee
      # this user is who he says he is, create a session for him.
      #
      # This is how persistence works in Authlogic. Authlogic grabs your cookie credentials, finds a user by those credentials, and then vouches for that user and creates a session. You can do this for just about
      # anything, which comes in handy for those unique authentication methods. Do what you need to do to authenticate the user, guarantee he is who he says he is, then pass the object here. Authlogic will do its
      # magic: create a session and cookie. Now when the user refreshes their session will be persisted by their session and cookie.
      def unauthorized_record=(value)
        self.authenticating_with = :unauthorized_record
        @unauthorized_record = value
      end
      
      # Returns if the session is valid or not. Basically it means that a record could or could not be found. If the session is valid you will have a result when calling the "record" method. If it was unsuccessful
      # you will not have a record.
      def valid?
        errors.clear
        if valid_credentials?
          # hooks
          before_validation
          new_session? ? before_validation_on_create : before_validation_on_update
          validate
          
          valid_record?
          
          # hooks
          new_session? ? after_validation_on_create : after_validation_on_update
          after_validation
          return true if errors.empty?
        end
        
        self.record = nil
        false
      end
      
      # Tries to validate the session from information from a basic http auth, if it was provided.
      def valid_http_auth?
        controller.authenticate_with_http_basic do |login, password|
          if !login.blank? && !password.blank?
            send("#{login_field}=", login)
            send("#{password_field}=", password)
            return valid?
          end
        end
        
        false
      end
      
      private
        def controller
          self.class.controller
        end
        
        # The goal with Authlogic is to feel as natural as possible. As a result, this method creates methods on the fly
        # based on the configuration set. By default the configuration is based off of the columns names in the authenticating
        # model. Thus allowing you to call user_session.username instead of user_session.login if you have a username column
        # instead of a login column. Since class configuration can change during initialization it makes the most sense to enforce
        # this configuration during the first initialization. At this point, all configuration should be set.
        #
        # Lastly, each method is defined individually to allow the user to provide their own "custom" method and this makes sure
        # we don't replace their method.
        def create_configurable_methods!
          return if self.class.methods_configured == true
          
          self.class.send(:alias_method, klass_name.demodulize.underscore.to_sym, :record)
          self.class.send(:attr_writer, login_field) if !respond_to?("#{login_field}=")
          self.class.send(:attr_reader, login_field) if !respond_to?(login_field)
          self.class.send(:attr_writer, password_field) if !respond_to?("#{password_field}=")
          self.class.send(:define_method, password_field) {} if !respond_to?(password_field)
          
          self.class.class_eval <<-"end_eval", __FILE__, __LINE__
def #{login_field}_with_authentication_flag=(value)
self.authenticating_with = :password
self.#{login_field}_without_authentication_flag = value
end
alias_method_chain :#{login_field}=, :authentication_flag
def #{password_field}_with_authentication_flag=(value)
self.authenticating_with = :password
self.#{password_field}_without_authentication_flag = value
end
alias_method_chain :#{password_field}=, :authentication_flag
private
# 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.
def protected_#{password_field}
@#{password_field}
end
end_eval
          
          self.class.methods_configured = true
        end
        
        def klass
          self.class.klass
        end
      
        def klass_name
          self.class.klass_name
        end
        
        def record=(value)
          @record = value
        end
        
        def search_for_record(method, value)
          klass.send(method, value)
        end
        
        def valid_credentials?
          unchecked_record = nil
          
          case authenticating_with
          when :password
            errors.add(login_field, I18n.t('error_messages.login_blank', :default => "can not be blank")) if send(login_field).blank?
            errors.add(password_field, I18n.t('error_messages.password_blank', :default => "can not be blank")) if send("protected_#{password_field}").blank?
            return false if errors.count > 0
            
            unchecked_record = search_for_record(find_by_login_method, send(login_field))
            
            if unchecked_record.blank?
              errors.add(login_field, I18n.t('error_messages.login_not_found', :default => "does not exist"))
              return false
            end
            
            unless unchecked_record.send(verify_password_method, send("protected_#{password_field}"))
              errors.add(password_field, I18n.t('error_messages.password_invalid', :default => "is not valid"))
              return false
            end
            
            self.record = unchecked_record
          when :unauthorized_record
            unchecked_record = unauthorized_record
            
            if unchecked_record.blank?
              errors.add_to_base(I18n.t('error_messages.blank_record', :default => "You can not login with a blank record"))
              return false
            end
            
            if unchecked_record.new_record?
              errors.add_to_base(I18n.t('error_messages.new_record', :default => "You can not login with a new record"))
              return false
            end
            
            self.record = unchecked_record
          end
          
          true
        end
        
        def valid_record?
          return true if disable_magic_states?
          [:active, :approved, :confirmed].each do |required_status|
            if record.respond_to?("#{required_status}?") && !record.send("#{required_status}?")
              errors.add_to_base(I18n.t("error_messages.not_#{required_status}", :default => "Your account is not #{required_status}"))
              return false
            end
          end
          true
        end
    end
  end
end