authentication-logic 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/bin/console +11 -0
  3. data/bin/setup +8 -0
  4. data/lib/auth/logic/acts_as_authentic/base.rb +118 -0
  5. data/lib/auth/logic/acts_as_authentic/email.rb +32 -0
  6. data/lib/auth/logic/acts_as_authentic/logged_in_status.rb +87 -0
  7. data/lib/auth/logic/acts_as_authentic/login.rb +65 -0
  8. data/lib/auth/logic/acts_as_authentic/magic_columns.rb +40 -0
  9. data/lib/auth/logic/acts_as_authentic/password.rb +362 -0
  10. data/lib/auth/logic/acts_as_authentic/perishable_token.rb +125 -0
  11. data/lib/auth/logic/acts_as_authentic/persistence_token.rb +72 -0
  12. data/lib/auth/logic/acts_as_authentic/queries/case_sensitivity.rb +55 -0
  13. data/lib/auth/logic/acts_as_authentic/queries/find_with_case.rb +85 -0
  14. data/lib/auth/logic/acts_as_authentic/session_maintenance.rb +189 -0
  15. data/lib/auth/logic/acts_as_authentic/single_access_token.rb +85 -0
  16. data/lib/auth/logic/config.rb +41 -0
  17. data/lib/auth/logic/controller_adapters/abstract_adapter.rb +121 -0
  18. data/lib/auth/logic/controller_adapters/rack_adapter.rb +74 -0
  19. data/lib/auth/logic/controller_adapters/rails_adapter.rb +49 -0
  20. data/lib/auth/logic/controller_adapters/sinatra_adapter.rb +69 -0
  21. data/lib/auth/logic/cookie_credentials.rb +65 -0
  22. data/lib/auth/logic/crypto_providers/bcrypt.rb +116 -0
  23. data/lib/auth/logic/crypto_providers/md5/v2.rb +37 -0
  24. data/lib/auth/logic/crypto_providers/md5.rb +38 -0
  25. data/lib/auth/logic/crypto_providers/scrypt.rb +96 -0
  26. data/lib/auth/logic/crypto_providers/sha1/v2.rb +42 -0
  27. data/lib/auth/logic/crypto_providers/sha1.rb +43 -0
  28. data/lib/auth/logic/crypto_providers/sha256/v2.rb +60 -0
  29. data/lib/auth/logic/crypto_providers/sha256.rb +61 -0
  30. data/lib/auth/logic/crypto_providers/sha512/v2.rb +41 -0
  31. data/lib/auth/logic/crypto_providers/sha512.rb +40 -0
  32. data/lib/auth/logic/crypto_providers.rb +89 -0
  33. data/lib/auth/logic/errors.rb +52 -0
  34. data/lib/auth/logic/i18n/translator.rb +20 -0
  35. data/lib/auth/logic/i18n.rb +100 -0
  36. data/lib/auth/logic/random.rb +18 -0
  37. data/lib/auth/logic/session/base.rb +2205 -0
  38. data/lib/auth/logic/session/magic_column/assigns_last_request_at.rb +49 -0
  39. data/lib/auth/logic/test_case/mock_api_controller.rb +53 -0
  40. data/lib/auth/logic/test_case/mock_controller.rb +59 -0
  41. data/lib/auth/logic/test_case/mock_cookie_jar.rb +112 -0
  42. data/lib/auth/logic/test_case/mock_logger.rb +14 -0
  43. data/lib/auth/logic/test_case/mock_request.rb +36 -0
  44. data/lib/auth/logic/test_case/rails_request_adapter.rb +40 -0
  45. data/lib/auth/logic/test_case.rb +216 -0
  46. data/lib/auth/logic/version.rb +7 -0
  47. data/lib/auth/logic.rb +46 -0
  48. metadata +426 -0
@@ -0,0 +1,2205 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "request_store"
4
+
5
+ module Authentication
6
+ module Logic
7
+ module Session
8
+ module Activation
9
+ # :nodoc:
10
+ class NotActivatedError < ::StandardError
11
+ def initialize
12
+ super(
13
+ "You must activate the Authentication::Logic::Session::Base.controller with " \
14
+ "a controller object before creating objects"
15
+ )
16
+ end
17
+ end
18
+ end
19
+
20
+ module Existence
21
+ # :nodoc:
22
+ class SessionInvalidError < ::StandardError
23
+ def initialize(session)
24
+ message = I18n.t(
25
+ "error_messages.session_invalid",
26
+ default: "Your session is invalid and has the following errors:"
27
+ )
28
+ message += " #{session.errors.full_messages.to_sentence}"
29
+ super message
30
+ end
31
+ end
32
+ end
33
+
34
+ # This is the most important class in Authentication::Logic. You will inherit this class
35
+ # for your own eg. `UserSession`.
36
+ #
37
+ # Ongoing consolidation of modules
38
+ # ================================
39
+ #
40
+ # We are consolidating modules into this class (inlining mixins). When we
41
+ # are done, there will only be this one file. It will be quite large, but it
42
+ # will be easier to trace execution.
43
+ #
44
+ # Once consolidation is complete, we hope to identify and extract
45
+ # collaborating objects. For example, there may be a "session adapter" that
46
+ # connects this class with the existing `ControllerAdapters`. Perhaps a
47
+ # data object or a state machine will reveal itself.
48
+ #
49
+ # Activation
50
+ # ==========
51
+ #
52
+ # Activating Authentication::Logic requires that you pass it an
53
+ # Authentication::Logic::ControllerAdapters::AbstractAdapter object, or a class that
54
+ # extends it. This is sort of like a database connection for an ORM library,
55
+ # Authentication::Logic can't do anything until it is "connected" to a controller. If
56
+ # you are using a supported framework, Authentication::Logic takes care of this for you.
57
+ #
58
+ # ActiveRecord Trickery
59
+ # =====================
60
+ #
61
+ # Authentication::Logic looks like ActiveRecord, sounds like ActiveRecord, but its not
62
+ # ActiveRecord. That's the goal here. This is useful for the various rails
63
+ # helper methods such as form_for, error_messages_for, or any method that
64
+ # expects an ActiveRecord object. The point is to disguise the object as an
65
+ # ActiveRecord object so we can take advantage of the many ActiveRecord
66
+ # tools.
67
+ #
68
+ # Brute Force Protection
69
+ # ======================
70
+ #
71
+ # A brute force attacks is executed by hammering a login with as many password
72
+ # combinations as possible, until one works. A brute force attacked is generally
73
+ # combated with a slow hashing algorithm such as BCrypt. You can increase the cost,
74
+ # which makes the hash generation slower, and ultimately increases the time it takes
75
+ # to execute a brute force attack. Just to put this into perspective, if a hacker was
76
+ # to gain access to your server and execute a brute force attack locally, meaning
77
+ # there is no network lag, it would probably take decades to complete. Now throw in
78
+ # network lag and it would take MUCH longer.
79
+ #
80
+ # But for those that are extra paranoid and can't get enough protection, why not stop
81
+ # them as soon as you realize something isn't right? That's what this module is all
82
+ # about. By default the consecutive_failed_logins_limit configuration option is set to
83
+ # 50, if someone consecutively fails to login after 50 attempts their account will be
84
+ # suspended. This is a very liberal number and at this point it should be obvious that
85
+ # something is not right. If you wish to lower this number just set the configuration
86
+ # to a lower number:
87
+ #
88
+ # class UserSession < Authentication::Logic::Session::Base
89
+ # consecutive_failed_logins_limit 10
90
+ # end
91
+ #
92
+ # Callbacks
93
+ # =========
94
+ #
95
+ # Between these callbacks and the configuration, this is the contract between me and
96
+ # you to safely modify Authentication::Logic's behavior. I will do everything I can to make sure
97
+ # these do not change.
98
+ #
99
+ # Check out the sub modules of Authentication::Logic::Session. They are very concise, clear, and
100
+ # to the point. More importantly they use the same API that you would use to extend
101
+ # Authentication::Logic. That being said, they are great examples of how to extend Authentication::Logic and
102
+ # add / modify behavior to Authentication::Logic. These modules could easily be pulled out into
103
+ # their own plugin and become an "add on" without any change.
104
+ #
105
+ # Now to the point of this module. Just like in ActiveRecord you have before_save,
106
+ # before_validation, etc. You have similar callbacks with Authentication::Logic, see the METHODS
107
+ # constant below. The order of execution is as follows:
108
+ #
109
+ # before_persisting
110
+ # persist
111
+ # after_persisting
112
+ # [save record if record.has_changes_to_save?]
113
+ #
114
+ # before_validation
115
+ # before_validation_on_create
116
+ # before_validation_on_update
117
+ # validate
118
+ # after_validation_on_update
119
+ # after_validation_on_create
120
+ # after_validation
121
+ # [save record if record.has_changes_to_save?]
122
+ #
123
+ # before_save
124
+ # before_create
125
+ # before_update
126
+ # after_update
127
+ # after_create
128
+ # after_save
129
+ # [save record if record.has_changes_to_save?]
130
+ #
131
+ # before_destroy
132
+ # [save record if record.has_changes_to_save?]
133
+ # after_destroy
134
+ #
135
+ # Notice the "save record if has_changes_to_save" lines above. This helps with performance. If
136
+ # you need to make changes to the associated record, there is no need to save the
137
+ # record, Authentication::Logic will do it for you. This allows multiple modules to modify the
138
+ # record and execute as few queries as possible.
139
+ #
140
+ # **WARNING**: unlike ActiveRecord, these callbacks must be set up on the class level:
141
+ #
142
+ # class UserSession < Authentication::Logic::Session::Base
143
+ # before_validation :my_method
144
+ # validate :another_method
145
+ # # ..etc
146
+ # end
147
+ #
148
+ # You can NOT define a "before_validation" method, this is bad practice and does not
149
+ # allow Authentication::Logic to extend properly with multiple extensions. Please ONLY use the
150
+ # method above.
151
+ #
152
+ # HTTP Basic Authentication
153
+ # =========================
154
+ #
155
+ # Handles all authentication that deals with basic HTTP auth. Which is
156
+ # authentication built into the HTTP protocol:
157
+ #
158
+ # http://username:password@whatever.com
159
+ #
160
+ # Also, if you are not comfortable letting users pass their raw username and
161
+ # password you can use a single access token, as described below.
162
+ #
163
+ # Magic Columns
164
+ # =============
165
+ #
166
+ # Just like ActiveRecord has "magic" columns, such as: created_at and updated_at.
167
+ # Authentication::Logic has its own "magic" columns too:
168
+ #
169
+ # * login_count - Increased every time an explicit login is made. This will *NOT*
170
+ # increase if logging in by a session, cookie, or basic http auth
171
+ # * failed_login_count - This increases for each consecutive failed login. See
172
+ # the consecutive_failed_logins_limit option for details.
173
+ # * last_request_at - Updates every time the user logs in, either by explicitly
174
+ # logging in, or logging in by cookie, session, or http auth
175
+ # * current_login_at - Updates with the current time when an explicit login is made.
176
+ # * last_login_at - Updates with the value of current_login_at before it is reset.
177
+ # * current_login_ip - Updates with the request ip when an explicit login is made.
178
+ # * last_login_ip - Updates with the value of current_login_ip before it is reset.
179
+ #
180
+ # Multiple Simultaneous Sessions
181
+ # ==============================
182
+ #
183
+ # See `id`. Allows you to separate sessions with an id, ultimately letting
184
+ # you create multiple sessions for the same user.
185
+ #
186
+ # Timeout
187
+ # =======
188
+ #
189
+ # Think about financial websites, if you are inactive for a certain period
190
+ # of time you will be asked to log back in on your next request. You can do
191
+ # this with Authentication::Logic easily, there are 2 parts to this:
192
+ #
193
+ # 1. Define the timeout threshold:
194
+ #
195
+ # acts_as_authentic do |c|
196
+ # c.logged_in_timeout = 10.minutes # default is 10.minutes
197
+ # end
198
+ #
199
+ # 2. Enable logging out on timeouts
200
+ #
201
+ # class UserSession < Authentication::Logic::Session::Base
202
+ # logout_on_timeout true # default is false
203
+ # end
204
+ #
205
+ # This will require a user to log back in if they are inactive for more than
206
+ # 10 minutes. In order for this feature to be used you must have a
207
+ # last_request_at datetime column in your table for whatever model you are
208
+ # authenticating with.
209
+ #
210
+ # Params
211
+ # ======
212
+ #
213
+ # This module is responsible for authenticating the user via params, which ultimately
214
+ # allows the user to log in using a URL like the following:
215
+ #
216
+ # https://www.domain.com?user_credentials=4LiXF7FiGUppIPubBPey
217
+ #
218
+ # Notice the token in the URL, this is a single access token. A single access token is
219
+ # used for single access only, it is not persisted. Meaning the user provides it,
220
+ # Authentication::Logic grants them access, and that's it. If they want access again they need to
221
+ # provide the token again. Authentication::Logic will *NEVER* try to persist the session after
222
+ # authenticating through this method.
223
+ #
224
+ # For added security, this token is *ONLY* allowed for RSS and ATOM requests. You can
225
+ # change this with the configuration. You can also define if it is allowed dynamically
226
+ # by defining a single_access_allowed? method in your controller. For example:
227
+ #
228
+ # class UsersController < ApplicationController
229
+ # private
230
+ # def single_access_allowed?
231
+ # action_name == "index"
232
+ # end
233
+ #
234
+ # Also, by default, this token is permanent. Meaning if the user changes their
235
+ # password, this token will remain the same. It will only change when it is explicitly
236
+ # reset.
237
+ #
238
+ # You can modify all of this behavior with the Config sub module.
239
+ #
240
+ # Perishable Token
241
+ # ================
242
+ #
243
+ # Maintains the perishable token, which is helpful for confirming records or
244
+ # authorizing records to reset their password. All that this module does is
245
+ # reset it after a session have been saved, just keep it changing. The more
246
+ # it changes, the tighter the security.
247
+ #
248
+ # See Authentication::Logic::ActsAsAuthentic::PerishableToken for more information.
249
+ #
250
+ # Scopes
251
+ # ======
252
+ #
253
+ # Authentication can be scoped, and it's easy, you just need to define how you want to
254
+ # scope everything. See `.with_scope`.
255
+ #
256
+ # Unauthorized Record
257
+ # ===================
258
+ #
259
+ # Allows you to create session with an object. Ex:
260
+ #
261
+ # UserSession.create(my_user_object)
262
+ #
263
+ # Be careful with this, because Authentication::Logic is assuming that you have already
264
+ # confirmed that the user is who he says he is.
265
+ #
266
+ # For example, this is the method used to persist the session internally.
267
+ # Authentication::Logic finds the user with the persistence token. At this point we know
268
+ # the user is who he says he is, so Authentication::Logic just creates a session with
269
+ # the record. This is particularly useful for 3rd party authentication
270
+ # methods, such as OpenID. Let that method verify the identity, once it's
271
+ # verified, pass the object and create a session.
272
+ #
273
+ # Magic States
274
+ # ============
275
+ #
276
+ # Authentication::Logic tries to check the state of the record before creating the session. If
277
+ # your record responds to the following methods and any of them return false,
278
+ # validation will fail:
279
+ #
280
+ # Method name Description
281
+ # active? Is the record marked as active?
282
+ # approved? Has the record been approved?
283
+ # confirmed? Has the record been confirmed?
284
+ #
285
+ # Authentication::Logic does nothing to define these methods for you, its up to you to define what
286
+ # they mean. If your object responds to these methods Authentication::Logic will use them,
287
+ # otherwise they are ignored.
288
+ #
289
+ # What's neat about this is that these are checked upon any type of login. When
290
+ # logging in explicitly, by cookie, session, or basic http auth. So if you mark a user
291
+ # inactive in the middle of their session they wont be logged back in next time they
292
+ # refresh the page. Giving you complete control.
293
+ #
294
+ # Need Authentication::Logic to check your own "state"? No problem, check out the hooks section
295
+ # below. Add in a before_validation to do your own checking. The sky is the limit.
296
+ #
297
+ # Validation
298
+ # ==========
299
+ #
300
+ # The errors in Authentication::Logic work just like ActiveRecord. In fact, it uses
301
+ # the `ActiveModel::Errors` class. Use it the same way:
302
+ #
303
+ # ```
304
+ # class UserSession
305
+ # validate :check_if_awesome
306
+ #
307
+ # private
308
+ #
309
+ # def check_if_awesome
310
+ # if login && !login.include?("awesome")
311
+ # errors.add(:login, "must contain awesome")
312
+ # end
313
+ # unless attempted_record.awesome?
314
+ # errors.add(:base, "You must be awesome to log in")
315
+ # end
316
+ # end
317
+ # end
318
+ # ```
319
+ class Base
320
+ extend ActiveModel::Naming
321
+ extend ActiveModel::Translation
322
+ extend Authentication::Logic::Config
323
+ include ActiveSupport::Callbacks
324
+
325
+ E_AC_PARAMETERS = <<~EOS
326
+ Passing an ActionController::Parameters to Authentication::Logic is not allowed.
327
+
328
+ In Authentication::Logic 3, especially during the transition of rails to Strong
329
+ Parameters, it was common for Authentication::Logic users to forget to `permit`
330
+ their params. They would pass their params into Authentication::Logic, we'd call
331
+ `to_h`, and they'd be surprised when authentication failed.
332
+
333
+ In 2018, people are still making this mistake. We'd like to help them
334
+ and make auth-logic a little simpler at the same time, so in Authentication::Logic
335
+ 3.7.0, we deprecated the use of ActionController::Parameters. Instead,
336
+ pass a plain Hash. Please replace:
337
+
338
+ UserSession.new(user_session_params)
339
+ UserSession.create(user_session_params)
340
+
341
+ with
342
+
343
+ UserSession.new(user_session_params.to_h)
344
+ UserSession.create(user_session_params.to_h)
345
+
346
+ And don't forget to `permit`!
347
+
348
+ We discussed this issue thoroughly between late 2016 and early
349
+ 2018. Notable discussions include:
350
+
351
+ - https://github.com/binarylogic/authlogic/issues/512
352
+ - https://github.com/binarylogic/authlogic/pull/558
353
+ - https://github.com/binarylogic/authlogic/pull/577
354
+ EOS
355
+ E_DPR_FIND_BY_LOGIN_METHOD = <<~EOS.squish.freeze
356
+ find_by_login_method is deprecated in favor of record_selection_method,
357
+ to avoid confusion with ActiveRecord's "Dynamic Finders".
358
+ (https://guides.rubyonrails.org/v6.0/active_record_querying.html#dynamic-finders)
359
+ For example, rubocop-rails is confused by the deprecated method.
360
+ (https://github.com/rubocop-hq/rubocop-rails/blob/master/lib/rubocop/cop/rails/dynamic_find_by.rb)
361
+ EOS
362
+ VALID_SAME_SITE_VALUES = [nil, "Lax", "Strict", "None"].freeze
363
+
364
+ # Callbacks
365
+ # =========
366
+
367
+ METHODS = %w[
368
+ before_persisting
369
+ persist
370
+ after_persisting
371
+ before_validation
372
+ before_validation_on_create
373
+ before_validation_on_update
374
+ validate
375
+ after_validation_on_update
376
+ after_validation_on_create
377
+ after_validation
378
+ before_save
379
+ before_create
380
+ before_update
381
+ after_update
382
+ after_create
383
+ after_save
384
+ before_destroy
385
+ after_destroy
386
+ ].freeze
387
+
388
+ # Defines the "callback installation methods" used below.
389
+ METHODS.each do |method|
390
+ class_eval <<-EOS, __FILE__, __LINE__ + 1
391
+ def self.#{method}(*filter_list, &block)
392
+ set_callback(:#{method}, *filter_list, &block)
393
+ end
394
+ EOS
395
+ end
396
+
397
+ # Defines session life cycle events that support callbacks.
398
+ define_callbacks(
399
+ *METHODS,
400
+ terminator: ->(_target, result_lambda) { result_lambda.call == false }
401
+ )
402
+ define_callbacks(
403
+ "persist",
404
+ terminator: ->(_target, result_lambda) { result_lambda.call == true }
405
+ )
406
+
407
+ # Use the "callback installation methods" defined above
408
+ # -----------------------------------------------------
409
+
410
+ before_persisting :reset_stale_state
411
+
412
+ # `persist` callbacks, in order of priority
413
+ persist :persist_by_params
414
+ persist :persist_by_cookie
415
+ persist :persist_by_session
416
+ persist :persist_by_http_auth, if: :persist_by_http_auth?
417
+
418
+ after_persisting :enforce_timeout
419
+ after_persisting :update_session, unless: :single_access?
420
+ after_persisting :set_last_request_at
421
+
422
+ before_save :update_info
423
+ before_save :set_last_request_at
424
+
425
+ after_save :reset_perishable_token!
426
+ after_save :save_cookie, if: :cookie_enabled?
427
+ after_save :update_session
428
+ after_create :renew_session_id
429
+
430
+ after_destroy :destroy_cookie, if: :cookie_enabled?
431
+ after_destroy :update_session
432
+
433
+ # `validate` callbacks, in deliberate order. For example,
434
+ # validate_magic_states must run *after* a record is found.
435
+ validate :validate_by_password, if: :authenticating_with_password?
436
+ validate(
437
+ :validate_by_unauthorized_record,
438
+ if: :authenticating_with_unauthorized_record?
439
+ )
440
+ validate :validate_magic_states, unless: :disable_magic_states?
441
+ validate :reset_failed_login_count, if: :reset_failed_login_count?
442
+ validate :validate_failed_logins, if: :being_brute_force_protected?
443
+ validate :increase_failed_login_count
444
+
445
+ # Accessors
446
+ # =========
447
+
448
+ class << self
449
+ attr_accessor(
450
+ :configured_password_methods
451
+ )
452
+ end
453
+ attr_accessor(
454
+ :invalid_password,
455
+ :new_session,
456
+ :priority_record,
457
+ :record,
458
+ :single_access,
459
+ :stale_record,
460
+ :unauthorized_record
461
+ )
462
+ attr_writer :scope
463
+
464
+ # Public class methods
465
+ # ====================
466
+
467
+ class << self
468
+ # Returns true if a controller has been set and can be used properly.
469
+ # This MUST be set before anything can be done. Similar to how
470
+ # ActiveRecord won't allow you to do anything without establishing a DB
471
+ # connection. In your framework environment this is done for you, but if
472
+ # you are using Authentication::Logic outside of your framework, you need to assign
473
+ # a controller object to Authentication::Logic via
474
+ # Authentication::Logic::Session::Base.controller = obj. See the controller= method
475
+ # for more information.
476
+ def activated?
477
+ !controller.nil?
478
+ end
479
+
480
+ # Allow users to log in via HTTP basic authentication.
481
+ #
482
+ # * <tt>Default:</tt> false
483
+ # * <tt>Accepts:</tt> Boolean
484
+ def allow_http_basic_auth(value = nil)
485
+ rw_config(:allow_http_basic_auth, value, false)
486
+ end
487
+ alias allow_http_basic_auth= allow_http_basic_auth
488
+
489
+ # Lets you change which model to use for authentication.
490
+ #
491
+ # * <tt>Default:</tt> inferred from the class name. UserSession would
492
+ # automatically try User
493
+ # * <tt>Accepts:</tt> an ActiveRecord class
494
+ def authenticate_with(klass)
495
+ @klass_name = klass.name
496
+ @klass = klass
497
+ end
498
+ alias authenticate_with= authenticate_with
499
+
500
+ # The current controller object
501
+ def controller
502
+ RequestStore.store[:auth_logic_controller]
503
+ end
504
+
505
+ # This accepts a controller object wrapped with the Authentication::Logic controller
506
+ # adapter. The controller adapters close the gap between the different
507
+ # controllers in each framework. That being said, Authentication::Logic is expecting
508
+ # your object's class to extend
509
+ # Authentication::Logic::ControllerAdapters::AbstractAdapter. See
510
+ # Authentication::Logic::ControllerAdapters for more info.
511
+ #
512
+ # Lastly, this is thread safe.
513
+ def controller=(value)
514
+ RequestStore.store[:auth_logic_controller] = value
515
+ end
516
+
517
+ # To help protect from brute force attacks you can set a limit on the
518
+ # allowed number of consecutive failed logins. By default this is 50,
519
+ # this is a very liberal number, and if someone fails to login after 50
520
+ # tries it should be pretty obvious that it's a machine trying to login
521
+ # in and very likely a brute force attack.
522
+ #
523
+ # In order to enable this field your model MUST have a
524
+ # failed_login_count (integer) field.
525
+ #
526
+ # If you don't know what a brute force attack is, it's when a machine
527
+ # tries to login into a system using every combination of character
528
+ # possible. Thus resulting in possibly millions of attempts to log into
529
+ # an account.
530
+ #
531
+ # * <tt>Default:</tt> 50
532
+ # * <tt>Accepts:</tt> Integer, set to 0 to disable
533
+ def consecutive_failed_logins_limit(value = nil)
534
+ rw_config(:consecutive_failed_logins_limit, value, 50)
535
+ end
536
+ alias consecutive_failed_logins_limit= consecutive_failed_logins_limit
537
+
538
+ # The name of the cookie or the key in the cookies hash. Be sure and use
539
+ # a unique name. If you have multiple sessions and they use the same
540
+ # cookie it will cause problems. Also, if a id is set it will be
541
+ # inserted into the beginning of the string. Example:
542
+ #
543
+ # session = UserSession.new
544
+ # session.cookie_key => "user_credentials"
545
+ #
546
+ # session = UserSession.new(:super_high_secret)
547
+ # session.cookie_key => "super_high_secret_user_credentials"
548
+ #
549
+ # * <tt>Default:</tt> "#{klass_name.underscore}_credentials"
550
+ # * <tt>Accepts:</tt> String
551
+ def cookie_key(value = nil)
552
+ rw_config(:cookie_key, value, "#{klass_name.underscore}_credentials")
553
+ end
554
+ alias cookie_key= cookie_key
555
+
556
+ # A convenience method. The same as:
557
+ #
558
+ # session = UserSession.new(*args)
559
+ # session.save
560
+ #
561
+ # Instead you can do:
562
+ #
563
+ # UserSession.create(*args)
564
+ def create(*args, &block)
565
+ session = new(*args)
566
+ session.save(&block)
567
+ session
568
+ end
569
+
570
+ # Same as create but calls create!, which raises an exception when
571
+ # validation fails.
572
+ def create!(*args)
573
+ session = new(*args)
574
+ session.save!
575
+ session
576
+ end
577
+
578
+ # Set this to true if you want to disable the checking of active?, approved?, and
579
+ # confirmed? on your record. This is more or less of a convenience feature, since
580
+ # 99% of the time if those methods exist and return false you will not want the
581
+ # user logging in. You could easily accomplish this same thing with a
582
+ # before_validation method or other callbacks.
583
+ #
584
+ # * <tt>Default:</tt> false
585
+ # * <tt>Accepts:</tt> Boolean
586
+ def disable_magic_states(value = nil)
587
+ rw_config(:disable_magic_states, value, false)
588
+ end
589
+ alias disable_magic_states= disable_magic_states
590
+
591
+ # Once the failed logins limit has been exceed, how long do you want to
592
+ # ban the user? This can be a temporary or permanent ban.
593
+ #
594
+ # * <tt>Default:</tt> 2.hours
595
+ # * <tt>Accepts:</tt> Fixnum, set to 0 for permanent ban
596
+ def failed_login_ban_for(value = nil)
597
+ rw_config(:failed_login_ban_for, (!value.nil? && value) || value, 2.hours.to_i)
598
+ end
599
+ alias failed_login_ban_for= failed_login_ban_for
600
+
601
+ # This is how you persist a session. This finds the record for the
602
+ # current session using a variety of methods. It basically tries to "log
603
+ # in" the user without the user having to explicitly log in. Check out
604
+ # the other Authentication::Logic::Session modules for more information.
605
+ #
606
+ # The best way to use this method is something like:
607
+ #
608
+ # helper_method :current_user_session, :current_user
609
+ #
610
+ # def current_user_session
611
+ # return @current_user_session if defined?(@current_user_session)
612
+ # @current_user_session = UserSession.find
613
+ # end
614
+ #
615
+ # def current_user
616
+ # return @current_user if defined?(@current_user)
617
+ # @current_user = current_user_session && current_user_session.user
618
+ # end
619
+ #
620
+ # Also, this method accepts a single parameter as the id, to find
621
+ # session that you marked with an id:
622
+ #
623
+ # UserSession.find(:secure)
624
+ #
625
+ # See the id method for more information on ids.
626
+ #
627
+ # Priority Record
628
+ # ===============
629
+ #
630
+ # This internal feature supports ActiveRecord's optimistic locking feature,
631
+ # which is automatically enabled when a table has a `lock_version` column.
632
+ #
633
+ # ```
634
+ # # https://api.rubyonrails.org/classes/ActiveRecord/Locking/Optimistic.html
635
+ # p1 = Person.find(1)
636
+ # p2 = Person.find(1)
637
+ # p1.first_name = "Michael"
638
+ # p1.save
639
+ # p2.first_name = "should fail"
640
+ # p2.save # Raises an ActiveRecord::StaleObjectError
641
+ # ```
642
+ #
643
+ # Now, consider the following Authentication::Logic scenario:
644
+ #
645
+ # ```
646
+ # User.log_in_after_password_change = true
647
+ # ben = User.find(1)
648
+ # UserSession.create(ben)
649
+ # ben.password = "newpasswd"
650
+ # ben.password_confirmation = "newpasswd"
651
+ # ben.save
652
+ # ```
653
+ #
654
+ # We've used one of Authentication::Logic's session maintenance features,
655
+ # `log_in_after_password_change`. So, when we call `ben.save`, there is a
656
+ # `before_save` callback that logs Ben in (`UserSession.find`). Well, when
657
+ # we log Ben in, we update his user record, eg. `login_count`. When we're
658
+ # done logging Ben in, then the normal `ben.save` happens. So, there were
659
+ # two `update` queries. If those two updates came from different User
660
+ # instances, we would get a `StaleObjectError`.
661
+ #
662
+ # Our solution is to carefully pass around a single `User` instance, using
663
+ # it for all `update` queries, thus avoiding the `StaleObjectError`.
664
+ def find(id = nil, priority_record = nil)
665
+ session = new({ priority_record: priority_record }, id)
666
+ session.priority_record = priority_record
667
+ return unless session.persisting?
668
+
669
+ session
670
+ end
671
+
672
+ # @deprecated in favor of record_selection_method
673
+ def find_by_login_method(value = nil)
674
+ ::ActiveSupport::Deprecation.warn(E_DPR_FIND_BY_LOGIN_METHOD)
675
+ record_selection_method(value)
676
+ end
677
+ alias find_by_login_method= find_by_login_method
678
+
679
+ # The text used to identify credentials (username/password) combination
680
+ # when a bad login attempt occurs. When you show error messages for a
681
+ # bad login, it's considered good security practice to hide which field
682
+ # the user has entered incorrectly (the login field or the password
683
+ # field). For a full explanation, see
684
+ # http://www.gnucitizen.org/blog/username-enumeration-vulnerabilities/
685
+ #
686
+ # Example of use:
687
+ #
688
+ # class UserSession < Authentication::Logic::Session::Base
689
+ # generalize_credentials_error_messages true
690
+ # end
691
+ #
692
+ # This would make the error message for bad logins and bad passwords
693
+ # look identical:
694
+ #
695
+ # Login/Password combination is not valid
696
+ #
697
+ # Alternatively you may use a custom message:
698
+ #
699
+ # class UserSession < AuthLogic::Session::Base
700
+ # generalize_credentials_error_messages "Your login information is invalid"
701
+ # end
702
+ #
703
+ # This will instead show your custom error message when the UserSession is invalid.
704
+ #
705
+ # The downside to enabling this is that is can be too vague for a user
706
+ # that has a hard time remembering their username and password
707
+ # combinations. It also disables the ability to to highlight the field
708
+ # with the error when you use form_for.
709
+ #
710
+ # If you are developing an app where security is an extreme priority
711
+ # (such as a financial application), then you should enable this.
712
+ # Otherwise, leaving this off is fine.
713
+ #
714
+ # * <tt>Default</tt> false
715
+ # * <tt>Accepts:</tt> Boolean
716
+ def generalize_credentials_error_messages(value = nil)
717
+ rw_config(:generalize_credentials_error_messages, value, false)
718
+ end
719
+ alias generalize_credentials_error_messages= generalize_credentials_error_messages
720
+
721
+ # HTTP authentication realm
722
+ #
723
+ # Sets the HTTP authentication realm.
724
+ #
725
+ # Note: This option has no effect unless request_http_basic_auth is true
726
+ #
727
+ # * <tt>Default:</tt> 'Application'
728
+ # * <tt>Accepts:</tt> String
729
+ def http_basic_auth_realm(value = nil)
730
+ rw_config(:http_basic_auth_realm, value, "Application")
731
+ end
732
+ alias http_basic_auth_realm= http_basic_auth_realm
733
+
734
+ # Should the cookie be set as httponly? If true, the cookie will not be
735
+ # accessible from javascript
736
+ #
737
+ # * <tt>Default:</tt> true
738
+ # * <tt>Accepts:</tt> Boolean
739
+ def httponly(value = nil)
740
+ rw_config(:httponly, value, true)
741
+ end
742
+ alias httponly= httponly
743
+
744
+ # How to name the class, works JUST LIKE ActiveRecord, except it uses
745
+ # the following namespace:
746
+ #
747
+ # auth.logic.models.user_session
748
+ def human_name(*)
749
+ I18n.t("models.#{name.underscore}", count: 1, default: name.humanize)
750
+ end
751
+
752
+ def i18n_scope
753
+ I18n.scope
754
+ end
755
+
756
+ # The name of the class that this session is authenticating with. For
757
+ # example, the UserSession class will authenticate with the User class
758
+ # unless you specify otherwise in your configuration. See
759
+ # authenticate_with for information on how to change this value.
760
+ #
761
+ # @api public
762
+ def klass
763
+ @klass ||= klass_name&.constantize
764
+ end
765
+
766
+ # The model name, guessed from the session class name, e.g. "User",
767
+ # from "UserSession".
768
+ #
769
+ # TODO: This method can return nil. We should explore this. It seems
770
+ # likely to cause a NoMethodError later, so perhaps we should raise an
771
+ # error instead.
772
+ #
773
+ # @api private
774
+ def klass_name
775
+ return @klass_name if instance_variable_defined?(:@klass_name)
776
+
777
+ @klass_name = name.scan(/(.*)Session/)[0]&.first
778
+ end
779
+
780
+ # The name of the method you want Authentication::Logic to create for storing the
781
+ # login / username. Keep in mind this is just for your
782
+ # Authentication::Logic::Session, if you want it can be something completely
783
+ # different than the field in your model. So if you wanted people to
784
+ # login with a field called "login" and then find users by email this is
785
+ # completely doable. See the `record_selection_method` configuration
786
+ # option for details.
787
+ #
788
+ # * <tt>Default:</tt> klass.login_field || klass.email_field
789
+ # * <tt>Accepts:</tt> Symbol or String
790
+ def login_field(value = nil)
791
+ rw_config(:login_field, value, klass.login_field || klass.email_field)
792
+ end
793
+ alias login_field= login_field
794
+
795
+ # With acts_as_authentic you get a :logged_in_timeout configuration
796
+ # option. If this is set, after this amount of time has passed the user
797
+ # will be marked as logged out. Obviously, since web based apps are on a
798
+ # per request basis, we have to define a time limit threshold that
799
+ # determines when we consider a user to be "logged out". Meaning, if
800
+ # they login and then leave the website, when do mark them as logged
801
+ # out? I recommend just using this as a fun feature on your website or
802
+ # reports, giving you a ballpark number of users logged in and active.
803
+ # This is not meant to be a dead accurate representation of a user's
804
+ # logged in state, since there is really no real way to do this with web
805
+ # based apps. Think about a user that logs in and doesn't log out. There
806
+ # is no action that tells you that the user isn't technically still
807
+ # logged in and active.
808
+ #
809
+ # That being said, you can use that feature to require a new login if
810
+ # their session times out. Similar to how financial sites work. Just set
811
+ # this option to true and if your record returns true for stale? then
812
+ # they will be required to log back in.
813
+ #
814
+ # Lastly, UserSession.find will still return an object if the session is
815
+ # stale, but you will not get a record. This allows you to determine if
816
+ # the user needs to log back in because their session went stale, or
817
+ # because they just aren't logged in. Just call
818
+ # current_user_session.stale? as your flag.
819
+ #
820
+ # * <tt>Default:</tt> false
821
+ # * <tt>Accepts:</tt> Boolean
822
+ def logout_on_timeout(value = nil)
823
+ rw_config(:logout_on_timeout, value, false)
824
+ end
825
+ alias logout_on_timeout= logout_on_timeout
826
+
827
+ # Every time a session is found the last_request_at field for that record is
828
+ # updated with the current time, if that field exists. If you want to limit how
829
+ # frequent that field is updated specify the threshold here. For example, if your
830
+ # user is making a request every 5 seconds, and you feel this is too frequent, and
831
+ # feel a minute is a good threshold. Set this to 1.minute. Once a minute has
832
+ # passed in between requests the field will be updated.
833
+ #
834
+ # * <tt>Default:</tt> 0
835
+ # * <tt>Accepts:</tt> integer representing time in seconds
836
+ def last_request_at_threshold(value = nil)
837
+ rw_config(:last_request_at_threshold, value, 0)
838
+ end
839
+ alias last_request_at_threshold= last_request_at_threshold
840
+
841
+ # Works exactly like cookie_key, but for params. So a user can login via
842
+ # params just like a cookie or a session. Your URL would look like:
843
+ #
844
+ # http://www.domain.com?user_credentials=my_single_access_key
845
+ #
846
+ # You can change the "user_credentials" key above with this
847
+ # configuration option. Keep in mind, just like cookie_key, if you
848
+ # supply an id the id will be appended to the front. Check out
849
+ # cookie_key for more details. Also checkout the "Single Access /
850
+ # Private Feeds Access" section in the README.
851
+ #
852
+ # * <tt>Default:</tt> cookie_key
853
+ # * <tt>Accepts:</tt> String
854
+ def params_key(value = nil)
855
+ rw_config(:params_key, value, cookie_key)
856
+ end
857
+ alias params_key= params_key
858
+
859
+ # Works exactly like login_field, but for the password instead. Returns
860
+ # :password if a login_field exists.
861
+ #
862
+ # * <tt>Default:</tt> :password
863
+ # * <tt>Accepts:</tt> Symbol or String
864
+ def password_field(value = nil)
865
+ rw_config(:password_field, value, login_field && :password)
866
+ end
867
+ alias password_field= password_field
868
+
869
+ # Authentication::Logic tries to validate the credentials passed to it. One part of
870
+ # validation is actually finding the user and making sure it exists.
871
+ # What method it uses the do this is up to you.
872
+ #
873
+ # ```
874
+ # # user_session.rb
875
+ # record_selection_method :find_by_email
876
+ # ```
877
+ #
878
+ # This is the recommended way to find the user by email address.
879
+ # The resulting query will be `User.find_by_email(send(login_field))`.
880
+ # (`login_field` will fall back to `email_field` if there's no `login`
881
+ # or `username` column).
882
+ #
883
+ # In your User model you can make that method do anything you want,
884
+ # giving you complete control of how users are found by the UserSession.
885
+ #
886
+ # Let's take an example: You want to allow users to login by username or
887
+ # email. Set this to the name of the class method that does this in the
888
+ # User model. Let's call it "find_by_username_or_email"
889
+ #
890
+ # ```
891
+ # class User < ActiveRecord::Base
892
+ # def self.find_by_username_or_email(login)
893
+ # find_by_username(login) || find_by_email(login)
894
+ # end
895
+ # end
896
+ # ```
897
+ #
898
+ # Now just specify the name of this method for this configuration option
899
+ # and you are all set. You can do anything you want here. Maybe you
900
+ # allow users to have multiple logins and you want to search a has_many
901
+ # relationship, etc. The sky is the limit.
902
+ #
903
+ # * <tt>Default:</tt> "find_by_smart_case_login_field"
904
+ # * <tt>Accepts:</tt> Symbol or String
905
+ def record_selection_method(value = nil)
906
+ rw_config(:record_selection_method, value, "find_by_smart_case_login_field")
907
+ end
908
+ alias record_selection_method= record_selection_method
909
+
910
+ # Whether or not to request HTTP authentication
911
+ #
912
+ # If set to true and no HTTP authentication credentials are sent with
913
+ # the request, the Rails controller method
914
+ # authenticate_or_request_with_http_basic will be used and a '401
915
+ # Authorization Required' header will be sent with the response. In
916
+ # most cases, this will cause the classic HTTP authentication popup to
917
+ # appear in the users browser.
918
+ #
919
+ # If set to false, the Rails controller method
920
+ # authenticate_with_http_basic is used and no 401 header is sent.
921
+ #
922
+ # Note: This parameter has no effect unless allow_http_basic_auth is
923
+ # true
924
+ #
925
+ # * <tt>Default:</tt> false
926
+ # * <tt>Accepts:</tt> Boolean
927
+ def request_http_basic_auth(value = nil)
928
+ rw_config(:request_http_basic_auth, value, false)
929
+ end
930
+ alias request_http_basic_auth= request_http_basic_auth
931
+
932
+ # If sessions should be remembered by default or not.
933
+ #
934
+ # * <tt>Default:</tt> false
935
+ # * <tt>Accepts:</tt> Boolean
936
+ def remember_me(value = nil)
937
+ rw_config(:remember_me, value, false)
938
+ end
939
+ alias remember_me= remember_me
940
+
941
+ # The length of time until the cookie expires.
942
+ #
943
+ # * <tt>Default:</tt> 3.months
944
+ # * <tt>Accepts:</tt> Integer, length of time in seconds, such as 60 or 3.months
945
+ def remember_me_for(value = nil)
946
+ rw_config(:remember_me_for, value, 3.months)
947
+ end
948
+ alias remember_me_for= remember_me_for
949
+
950
+ # Should the cookie be prevented from being send along with cross-site
951
+ # requests?
952
+ #
953
+ # * <tt>Default:</tt> nil
954
+ # * <tt>Accepts:</tt> String, one of nil, 'Lax' or 'Strict'
955
+ def same_site(value = nil)
956
+ unless VALID_SAME_SITE_VALUES.include?(value)
957
+ msg = "Invalid same_site value: #{value}. Valid: #{VALID_SAME_SITE_VALUES.inspect}"
958
+ raise ArgumentError, msg
959
+ end
960
+ rw_config(:same_site, value)
961
+ end
962
+ alias same_site= same_site
963
+
964
+ # The current scope set, should be used in the block passed to with_scope.
965
+ def scope
966
+ RequestStore.store[:auth_logic_scope]
967
+ end
968
+
969
+ # Should the cookie be set as secure? If true, the cookie will only be sent over
970
+ # SSL connections
971
+ #
972
+ # * <tt>Default:</tt> true
973
+ # * <tt>Accepts:</tt> Boolean
974
+ def secure(value = nil)
975
+ rw_config(:secure, value, true)
976
+ end
977
+ alias secure= secure
978
+
979
+ # Should the Rack session ID be reset after authentication, to protect
980
+ # against Session Fixation attacks?
981
+ #
982
+ # * <tt>Default:</tt> true
983
+ # * <tt>Accepts:</tt> Boolean
984
+ def session_fixation_defense(value = nil)
985
+ rw_config(:session_fixation_defense, value, true)
986
+ end
987
+ alias session_fixation_defense= session_fixation_defense
988
+
989
+ # Should the cookie be signed? If the controller adapter supports it, this is a
990
+ # measure against cookie tampering.
991
+ def sign_cookie(value = nil)
992
+ if value && controller && !controller.cookies.respond_to?(:signed)
993
+ raise "Signed cookies not supported with #{controller.class}!"
994
+ end
995
+
996
+ rw_config(:sign_cookie, value, false)
997
+ end
998
+ alias sign_cookie= sign_cookie
999
+
1000
+ # Should the cookie be encrypted? If the controller adapter supports it, this is a
1001
+ # measure to hide the contents of the cookie (e.g. persistence_token)
1002
+ def encrypt_cookie(value = nil)
1003
+ if value && controller && !controller.cookies.respond_to?(:encrypted)
1004
+ raise "Encrypted cookies not supported with #{controller.class}!"
1005
+ end
1006
+
1007
+ if value && sign_cookie
1008
+ raise "It is recommended to use encrypt_cookie instead of sign_cookie. " \
1009
+ "You may not enable both options."
1010
+ end
1011
+ rw_config(:encrypt_cookie, value, false)
1012
+ end
1013
+ alias encrypt_cookie= encrypt_cookie
1014
+
1015
+ # Works exactly like cookie_key, but for sessions. See cookie_key for more info.
1016
+ #
1017
+ # * <tt>Default:</tt> cookie_key
1018
+ # * <tt>Accepts:</tt> Symbol or String
1019
+ def session_key(value = nil)
1020
+ rw_config(:session_key, value, cookie_key)
1021
+ end
1022
+ alias session_key= session_key
1023
+
1024
+ # Authentication is allowed via a single access token, but maybe this is
1025
+ # something you don't want for your application as a whole. Maybe this
1026
+ # is something you only want for specific request types. Specify a list
1027
+ # of allowed request types and single access authentication will only be
1028
+ # allowed for the ones you specify.
1029
+ #
1030
+ # * <tt>Default:</tt> ["application/rss+xml", "application/atom+xml"]
1031
+ # * <tt>Accepts:</tt> String of a request type, or :all or :any to
1032
+ # allow single access authentication for any and all request types
1033
+ def single_access_allowed_request_types(value = nil)
1034
+ rw_config(
1035
+ :single_access_allowed_request_types,
1036
+ value,
1037
+ ["application/rss+xml", "application/atom+xml"]
1038
+ )
1039
+ end
1040
+ alias single_access_allowed_request_types= single_access_allowed_request_types
1041
+
1042
+ # The name of the method in your model used to verify the password. This
1043
+ # should be an instance method. It should also be prepared to accept a
1044
+ # raw password and a crytped password.
1045
+ #
1046
+ # * <tt>Default:</tt> "valid_password?" defined in acts_as_authentic/password.rb
1047
+ # * <tt>Accepts:</tt> Symbol or String
1048
+ def verify_password_method(value = nil)
1049
+ rw_config(:verify_password_method, value, "valid_password?")
1050
+ end
1051
+ alias verify_password_method= verify_password_method
1052
+
1053
+ # What with_scopes focuses on is scoping the query when finding the
1054
+ # object and the name of the cookie / session. It works very similar to
1055
+ # ActiveRecord::Base#with_scopes. It accepts a hash with any of the
1056
+ # following options:
1057
+ #
1058
+ # * <tt>find_options:</tt> any options you can pass into ActiveRecord::Base.find.
1059
+ # This is used when trying to find the record.
1060
+ # * <tt>id:</tt> The id of the session, this gets merged with the real id. For
1061
+ # information ids see the id method.
1062
+ #
1063
+ # Here is how you use it:
1064
+ #
1065
+ # ```
1066
+ # UserSession.with_scope(find_options: User.where(account_id: 2), id: "account_2") do
1067
+ # UserSession.find
1068
+ # end
1069
+ # ```
1070
+ #
1071
+ # Essentially what the above does is scope the searching of the object
1072
+ # with the sql you provided. So instead of:
1073
+ #
1074
+ # ```
1075
+ # User.where("login = 'ben'").first
1076
+ # ```
1077
+ #
1078
+ # it would effectively be:
1079
+ #
1080
+ # ```
1081
+ # User.where("login = 'ben' and account_id = 2").first
1082
+ # ```
1083
+ #
1084
+ # You will also notice the :id option. This works just like the id
1085
+ # method. It scopes your cookies. So the name of your cookie will be:
1086
+ #
1087
+ # account_2_user_credentials
1088
+ #
1089
+ # instead of:
1090
+ #
1091
+ # user_credentials
1092
+ #
1093
+ # What is also nifty about scoping with an :id is that it merges your
1094
+ # id's. So if you do:
1095
+ #
1096
+ # UserSession.with_scope(
1097
+ # find_options: { conditions: "account_id = 2"},
1098
+ # id: "account_2"
1099
+ # ) do
1100
+ # session = UserSession.new
1101
+ # session.id = :secure
1102
+ # end
1103
+ #
1104
+ # The name of your cookies will be:
1105
+ #
1106
+ # secure_account_2_user_credentials
1107
+ def with_scope(options = {})
1108
+ raise ArgumentError, "You must provide a block" unless block_given?
1109
+
1110
+ self.scope = options
1111
+ result = yield
1112
+ self.scope = nil
1113
+ result
1114
+ end
1115
+ end
1116
+
1117
+ # Constructor
1118
+ # ===========
1119
+
1120
+ def initialize(*args)
1121
+ @id = nil
1122
+ self.scope = self.class.scope
1123
+ define_record_alias_method
1124
+ raise Activation::NotActivatedError unless self.class.activated?
1125
+
1126
+ unless self.class.configured_password_methods
1127
+ configure_password_methods
1128
+ self.class.configured_password_methods = true
1129
+ end
1130
+ instance_variable_set("@#{password_field}", nil)
1131
+ self.credentials = args
1132
+ end
1133
+
1134
+ # Public instance methods
1135
+ # =======================
1136
+
1137
+ # You should use this as a place holder for any records that you find
1138
+ # during validation. The main reason for this is to allow other modules to
1139
+ # use it if needed. Take the failed_login_count feature, it needs this in
1140
+ # order to increase the failed login count.
1141
+ attr_reader :attempted_record
1142
+
1143
+ # See attempted_record
1144
+ def attempted_record=(value)
1145
+ value = priority_record if value == priority_record # See notes in `.find`
1146
+ @attempted_record = value
1147
+ end
1148
+
1149
+ # Returns true when the consecutive_failed_logins_limit has been
1150
+ # exceeded and is being temporarily banned. Notice the word temporary,
1151
+ # the user will not be permanently banned unless you choose to do so
1152
+ # with configuration. By default they will be banned for 2 hours. During
1153
+ # that 2 hour period this method will return true.
1154
+ def being_brute_force_protected?
1155
+ exceeded_failed_logins_limit? &&
1156
+ (
1157
+ failed_login_ban_for <= 0 ||
1158
+ attempted_record.respond_to?(:updated_at) &&
1159
+ attempted_record.updated_at >= failed_login_ban_for.seconds.ago
1160
+ )
1161
+ end
1162
+
1163
+ # The credentials you passed to create your session, in a redacted format
1164
+ # intended for output (debugging, logging). See credentials= for more
1165
+ # info.
1166
+ #
1167
+ # @api private
1168
+ def credentials
1169
+ if authenticating_with_unauthorized_record?
1170
+ { unauthorized_record: "<protected>" }
1171
+ elsif authenticating_with_password?
1172
+ {
1173
+ login_field.to_sym => send(login_field),
1174
+ password_field.to_sym => "<protected>"
1175
+ }
1176
+ else
1177
+ {}
1178
+ end
1179
+ end
1180
+
1181
+ # Set your credentials before you save your session. There are many
1182
+ # method signatures.
1183
+ #
1184
+ # ```
1185
+ # # A hash of credentials is most common
1186
+ # session.credentials = { login: "foo", password: "bar", remember_me: true }
1187
+ #
1188
+ # # You must pass an actual Hash, `ActionController::Parameters` is
1189
+ # # specifically not allowed.
1190
+ #
1191
+ # # You can pass an array of objects:
1192
+ # session.credentials = [my_user_object, true]
1193
+ #
1194
+ # # If you need to set an id (see `#id`) pass it last.
1195
+ # session.credentials = [
1196
+ # {:login => "foo", :password => "bar", :remember_me => true},
1197
+ # :my_id
1198
+ # ]
1199
+ # session.credentials = [my_user_object, true, :my_id]
1200
+ #
1201
+ # The `id` is something that you control yourself, it should never be
1202
+ # set from a hash or a form.
1203
+ #
1204
+ # # Finally, there's priority_record
1205
+ # [{ priority_record: my_object }, :my_id]
1206
+ # ```
1207
+ #
1208
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
1209
+ def credentials=(value)
1210
+ normalized = Array.wrap(value)
1211
+ raise TypeError, E_AC_PARAMETERS if normalized.first.instance_of?(::ActionController::Parameters)
1212
+
1213
+ # Allows you to set the remember_me option when passing credentials.
1214
+ values = value.is_a?(Array) ? value : [value]
1215
+ case values.first
1216
+ when Hash
1217
+ if values.first.with_indifferent_access.key?(:remember_me)
1218
+ self.remember_me = values.first.with_indifferent_access[:remember_me]
1219
+ end
1220
+ else
1221
+ r = values.find { |val| val.is_a?(TrueClass) || val.is_a?(FalseClass) }
1222
+ self.remember_me = r unless r.nil?
1223
+ end
1224
+
1225
+ # Accepts the login_field / password_field credentials combination in
1226
+ # hash form.
1227
+ #
1228
+ # You must pass an actual Hash, `ActionController::Parameters` is
1229
+ # specifically not allowed.
1230
+ values = Array.wrap(value)
1231
+ if values.first.is_a?(Hash)
1232
+ sliced = values
1233
+ .first
1234
+ .with_indifferent_access
1235
+ .slice(login_field, password_field)
1236
+ sliced.each do |field, val|
1237
+ next if val.blank?
1238
+
1239
+ send("#{field}=", val)
1240
+ end
1241
+ end
1242
+
1243
+ # Setting the unauthorized record if it exists in the credentials passed.
1244
+ values = value.is_a?(Array) ? value : [value]
1245
+ self.unauthorized_record = values.first if values.first.class < ::ActiveRecord::Base
1246
+
1247
+ # Setting the id if it is passed in the credentials.
1248
+ values = value.is_a?(Array) ? value : [value]
1249
+ self.id = values.last if values.last.is_a?(Symbol)
1250
+
1251
+ # Setting priority record if it is passed. The only way it can be passed
1252
+ # is through an array:
1253
+ #
1254
+ # session.credentials = [real_user_object, priority_user_object]
1255
+ #
1256
+ # See notes in `.find`
1257
+ values = value.is_a?(Array) ? value : [value]
1258
+ self.priority_record = values[1] if values[1].class < ::ActiveRecord::Base
1259
+ end
1260
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
1261
+
1262
+ # Clears all errors and the associated record, you should call this
1263
+ # terminate a session, thus requiring the user to authenticate again if
1264
+ # it is needed.
1265
+ def destroy
1266
+ run_callbacks :before_destroy
1267
+ save_record
1268
+ errors.clear
1269
+ @record = nil
1270
+ run_callbacks :after_destroy
1271
+ true
1272
+ end
1273
+
1274
+ def destroyed?
1275
+ record.nil?
1276
+ end
1277
+
1278
+ # @api public
1279
+ def errors
1280
+ @errors ||= ::ActiveModel::Errors.new(self)
1281
+ end
1282
+
1283
+ # If the cookie should be marked as httponly (not accessible via javascript)
1284
+ def httponly
1285
+ return @httponly if defined?(@httponly)
1286
+
1287
+ @httponly = self.class.httponly
1288
+ end
1289
+
1290
+ # Accepts a boolean as to whether the cookie should be marked as
1291
+ # httponly. If true, the cookie will not be accessible from javascript
1292
+ attr_writer :httponly
1293
+
1294
+ # See httponly
1295
+ def httponly?
1296
+ httponly == true || httponly == "true" || httponly == "1"
1297
+ end
1298
+
1299
+ # Allows you to set a unique identifier for your session, so that you can
1300
+ # have more than 1 session at a time.
1301
+ #
1302
+ # For example, you may want to have simultaneous private and public
1303
+ # sessions. Or, a normal user session and a "secure" user session. The
1304
+ # secure user session would be created only when they want to modify their
1305
+ # billing information, or other sensitive information.
1306
+ #
1307
+ # You can set the id during initialization (see initialize for more
1308
+ # information), or as an attribute:
1309
+ #
1310
+ # session.id = :my_id
1311
+ #
1312
+ # Set your id before you save your session.
1313
+ #
1314
+ # Lastly, to retrieve your session with the id, use the `.find` method.
1315
+ attr_accessor :id
1316
+
1317
+ def inspect
1318
+ format(
1319
+ "#<%s: %s>",
1320
+ self.class.name,
1321
+ credentials.blank? ? "no credentials provided" : credentials.inspect
1322
+ )
1323
+ end
1324
+
1325
+ def invalid_password?
1326
+ invalid_password == true
1327
+ end
1328
+
1329
+ # Don't use this yourself, this is to just trick some of the helpers
1330
+ # since this is the method it calls.
1331
+ def new_record?
1332
+ new_session?
1333
+ end
1334
+
1335
+ # Returns true if the session is new, meaning no action has been taken
1336
+ # on it and a successful save has not taken place.
1337
+ def new_session?
1338
+ new_session != false
1339
+ end
1340
+
1341
+ def persisted?
1342
+ !(new_record? || destroyed?)
1343
+ end
1344
+
1345
+ # Returns boolean indicating if the session is being persisted or not,
1346
+ # meaning the user does not have to explicitly log in in order to be
1347
+ # logged in.
1348
+ #
1349
+ # If the session has no associated record, it will try to find a record
1350
+ # and persist the session.
1351
+ #
1352
+ # This is the method that the class level method find uses to ultimately
1353
+ # persist the session.
1354
+ def persisting?
1355
+ return true unless record.nil?
1356
+
1357
+ self.attempted_record = nil
1358
+ self.remember_me = cookie_credentials&.remember_me?
1359
+ run_callbacks :before_persisting
1360
+ run_callbacks :persist
1361
+ ensure_authentication_attempted
1362
+ if errors.empty? && !attempted_record.nil?
1363
+ self.record = attempted_record
1364
+ run_callbacks :after_persisting
1365
+ save_record
1366
+ self.new_session = false
1367
+ true
1368
+ else
1369
+ false
1370
+ end
1371
+ end
1372
+
1373
+ def save_record(alternate_record = nil)
1374
+ r = alternate_record || record
1375
+ return unless r != priority_record
1376
+ return unless r&.has_changes_to_save? && !r.readonly?
1377
+
1378
+ r.save_without_session_maintenance(validate: false)
1379
+ end
1380
+
1381
+ # Tells you if the record is stale or not. Meaning the record has timed
1382
+ # out. This will only return true if you set logout_on_timeout to true
1383
+ # in your configuration. Basically how a bank website works. If you
1384
+ # aren't active over a certain period of time your session becomes stale
1385
+ # and requires you to log back in.
1386
+ def stale?
1387
+ if remember_me?
1388
+ remember_me_expired?
1389
+ else
1390
+ !stale_record.nil? || (logout_on_timeout? && record && record.logged_out?)
1391
+ end
1392
+ end
1393
+
1394
+ # Is the cookie going to expire after the session is over, or will it stick around?
1395
+ def remember_me
1396
+ return @remember_me if defined?(@remember_me)
1397
+
1398
+ @remember_me = self.class.remember_me
1399
+ end
1400
+
1401
+ # Accepts a boolean as a flag to remember the session or not. Basically
1402
+ # to expire the cookie at the end of the session or keep it for
1403
+ # "remember_me_until".
1404
+ attr_writer :remember_me
1405
+
1406
+ # See remember_me
1407
+ def remember_me?
1408
+ remember_me == true || remember_me == "true" || remember_me == "1"
1409
+ end
1410
+
1411
+ # Has the cookie expired due to current time being greater than remember_me_until.
1412
+ def remember_me_expired?
1413
+ return unless remember_me?
1414
+
1415
+ cookie_credentials.remember_me_until < ::Time.now
1416
+ end
1417
+
1418
+ # How long to remember the user if remember_me is true. This is based on the class
1419
+ # level configuration: remember_me_for
1420
+ def remember_me_for
1421
+ return unless remember_me?
1422
+
1423
+ self.class.remember_me_for
1424
+ end
1425
+
1426
+ # When to expire the cookie. See remember_me_for configuration option to change
1427
+ # this.
1428
+ def remember_me_until
1429
+ return unless remember_me?
1430
+
1431
+ remember_me_for.from_now
1432
+ end
1433
+
1434
+ # After you have specified all of the details for your session you can
1435
+ # try to save it. This will run validation checks and find the
1436
+ # associated record, if all validation passes. If validation does not
1437
+ # pass, the save will fail and the errors will be stored in the errors
1438
+ # object.
1439
+ def save
1440
+ result = nil
1441
+ if valid?
1442
+ self.record = attempted_record
1443
+
1444
+ run_callbacks :before_save
1445
+ run_callbacks(new_session? ? :before_create : :before_update)
1446
+ run_callbacks(new_session? ? :after_create : :after_update)
1447
+ run_callbacks :after_save
1448
+
1449
+ save_record
1450
+ self.new_session = false
1451
+ result = true
1452
+ else
1453
+ result = false
1454
+ end
1455
+
1456
+ yield result if block_given?
1457
+ result
1458
+ end
1459
+
1460
+ # Same as save but raises an exception of validation errors when
1461
+ # validation fails
1462
+ def save!
1463
+ result = save
1464
+ raise Existence::SessionInvalidError, self unless result
1465
+
1466
+ result
1467
+ end
1468
+
1469
+ # If the cookie should be marked as secure (SSL only)
1470
+ def secure
1471
+ return @secure if defined?(@secure)
1472
+
1473
+ @secure = self.class.secure
1474
+ end
1475
+
1476
+ # Accepts a boolean as to whether the cookie should be marked as secure. If true
1477
+ # the cookie will only ever be sent over an SSL connection.
1478
+ attr_writer :secure
1479
+
1480
+ # See secure
1481
+ def secure?
1482
+ secure == true || secure == "true" || secure == "1"
1483
+ end
1484
+
1485
+ # If the cookie should be marked as SameSite with 'Lax' or 'Strict' flag.
1486
+ def same_site
1487
+ return @same_site if defined?(@same_site)
1488
+
1489
+ @same_site = self.class.same_site(nil)
1490
+ end
1491
+
1492
+ # Accepts nil, 'Lax' or 'Strict' as possible flags.
1493
+ def same_site=(value)
1494
+ unless VALID_SAME_SITE_VALUES.include?(value)
1495
+ msg = "Invalid same_site value: #{value}. Valid: #{VALID_SAME_SITE_VALUES.inspect}"
1496
+ raise ArgumentError, msg
1497
+ end
1498
+ @same_site = value
1499
+ end
1500
+
1501
+ # If the cookie should be signed
1502
+ def sign_cookie
1503
+ return @sign_cookie if defined?(@sign_cookie)
1504
+
1505
+ @sign_cookie = self.class.sign_cookie
1506
+ end
1507
+
1508
+ # Accepts a boolean as to whether the cookie should be signed. If true
1509
+ # the cookie will be saved and verified using a signature.
1510
+ attr_writer :sign_cookie
1511
+
1512
+ # See sign_cookie
1513
+ def sign_cookie?
1514
+ sign_cookie == true || sign_cookie == "true" || sign_cookie == "1"
1515
+ end
1516
+
1517
+ # If the cookie should be encrypted
1518
+ def encrypt_cookie
1519
+ return @encrypt_cookie if defined?(@encrypt_cookie)
1520
+
1521
+ @encrypt_cookie = self.class.encrypt_cookie
1522
+ end
1523
+
1524
+ # Accepts a boolean as to whether the cookie should be encrypted. If true
1525
+ # the cookie will be saved in an encrypted state.
1526
+ attr_writer :encrypt_cookie
1527
+
1528
+ # See encrypt_cookie
1529
+ def encrypt_cookie?
1530
+ encrypt_cookie == true || encrypt_cookie == "true" || encrypt_cookie == "1"
1531
+ end
1532
+
1533
+ # The scope of the current object
1534
+ def scope
1535
+ @scope ||= {}
1536
+ end
1537
+
1538
+ def to_key
1539
+ new_record? ? nil : record.to_key
1540
+ end
1541
+
1542
+ # For rails >= 3.0
1543
+ def to_model
1544
+ self
1545
+ end
1546
+
1547
+ # Determines if the information you provided for authentication is valid
1548
+ # or not. If there is a problem with the information provided errors will
1549
+ # be added to the errors object and this method will return false.
1550
+ #
1551
+ # @api public
1552
+ def valid?
1553
+ errors.clear
1554
+ self.attempted_record = nil
1555
+ run_the_before_validation_callbacks
1556
+
1557
+ # Run the `validate` callbacks, eg. `validate_by_password`.
1558
+ # This is when `attempted_record` is set.
1559
+ run_callbacks(:validate)
1560
+
1561
+ ensure_authentication_attempted
1562
+ run_the_after_validation_callbacks if errors.empty?
1563
+ save_record(attempted_record)
1564
+ errors.empty?
1565
+ end
1566
+
1567
+ # Private class methods
1568
+ # =====================
1569
+
1570
+ class << self
1571
+ private
1572
+
1573
+ def scope=(value)
1574
+ RequestStore.store[:auth_logic_scope] = value
1575
+ end
1576
+ end
1577
+
1578
+ # Private instance methods
1579
+ # ========================
1580
+
1581
+ private
1582
+
1583
+ def add_general_credentials_error
1584
+ error_message =
1585
+ if self.class.generalize_credentials_error_messages.is_a? String
1586
+ self.class.generalize_credentials_error_messages
1587
+ else
1588
+ "#{login_field.to_s.humanize}/Password combination is not valid"
1589
+ end
1590
+ errors.add(
1591
+ :base,
1592
+ I18n.t("error_messages.general_credentials_error", default: error_message)
1593
+ )
1594
+ end
1595
+
1596
+ def add_invalid_password_error
1597
+ if generalize_credentials_error_messages?
1598
+ add_general_credentials_error
1599
+ else
1600
+ errors.add(
1601
+ password_field,
1602
+ I18n.t("error_messages.password_invalid", default: "is not valid")
1603
+ )
1604
+ end
1605
+ end
1606
+
1607
+ def add_login_not_found_error
1608
+ if generalize_credentials_error_messages?
1609
+ add_general_credentials_error
1610
+ else
1611
+ errors.add(
1612
+ login_field,
1613
+ I18n.t("error_messages.login_not_found", default: "is not valid")
1614
+ )
1615
+ end
1616
+ end
1617
+
1618
+ def allow_http_basic_auth?
1619
+ self.class.allow_http_basic_auth == true
1620
+ end
1621
+
1622
+ def authenticating_with_password?
1623
+ login_field && (!send(login_field).nil? || !send("protected_#{password_field}").nil?)
1624
+ end
1625
+
1626
+ def authenticating_with_unauthorized_record?
1627
+ !unauthorized_record.nil?
1628
+ end
1629
+
1630
+ # Used for things like cookie_key, session_key, etc.
1631
+ # Examples:
1632
+ # - user_credentials
1633
+ # - ziggity_zack_user_credentials
1634
+ # - ziggity_zack is an "id"
1635
+ # - see persistence_token_test.rb
1636
+ def build_key(last_part)
1637
+ [id, scope[:id], last_part].compact.join("_")
1638
+ end
1639
+
1640
+ def clear_failed_login_count
1641
+ return unless record.respond_to?(:failed_login_count)
1642
+
1643
+ record.failed_login_count = 0
1644
+ end
1645
+
1646
+ def consecutive_failed_logins_limit
1647
+ self.class.consecutive_failed_logins_limit
1648
+ end
1649
+
1650
+ def controller
1651
+ self.class.controller
1652
+ end
1653
+
1654
+ def cookie_key
1655
+ build_key(self.class.cookie_key)
1656
+ end
1657
+
1658
+ # Look in the `cookie_jar`, find the cookie that contains auth-logic
1659
+ # credentials (`cookie_key`).
1660
+ #
1661
+ # @api private
1662
+ # @return ::Authentication::Logic::CookieCredentials or if no cookie is found, nil
1663
+ def cookie_credentials
1664
+ return unless cookie_enabled?
1665
+
1666
+ cookie_value = cookie_jar[cookie_key]
1667
+ return if cookie_value.nil?
1668
+
1669
+ ::Authentication::Logic::CookieCredentials.parse(cookie_value)
1670
+ end
1671
+
1672
+ def cookie_enabled?
1673
+ !controller.cookies.nil?
1674
+ end
1675
+
1676
+ def cookie_jar
1677
+ if self.class.encrypt_cookie
1678
+ controller.cookies.encrypted
1679
+ elsif self.class.sign_cookie
1680
+ controller.cookies.signed
1681
+ else
1682
+ controller.cookies
1683
+ end
1684
+ end
1685
+
1686
+ def configure_password_methods
1687
+ define_login_field_methods
1688
+ define_password_field_methods
1689
+ end
1690
+
1691
+ # Assign a new controller-session ID, to defend against Session Fixation.
1692
+ # https://guides.rubyonrails.org/v6.0/security.html#session-fixation
1693
+ def renew_session_id
1694
+ return unless self.class.session_fixation_defense
1695
+
1696
+ controller.renew_session_id
1697
+ end
1698
+
1699
+ def define_login_field_methods
1700
+ return unless login_field
1701
+
1702
+ self.class.send(:attr_writer, login_field) unless respond_to?("#{login_field}=")
1703
+ self.class.send(:attr_reader, login_field) unless respond_to?(login_field)
1704
+ end
1705
+
1706
+ # @api private
1707
+ def define_password_field_methods
1708
+ return unless password_field
1709
+
1710
+ define_password_field_writer_method
1711
+ define_password_field_reader_methods
1712
+ end
1713
+
1714
+ # The password should not be accessible publicly. This way forms using
1715
+ # form_for don't fill the password with the attempted password. To prevent
1716
+ # this we just create this method that is private.
1717
+ #
1718
+ # @api private
1719
+ def define_password_field_reader_methods
1720
+ unless respond_to?(password_field)
1721
+ # Deliberate no-op method, see rationale above.
1722
+ self.class.send(:define_method, password_field) {}
1723
+ end
1724
+ self.class.class_eval(
1725
+ <<-EOS, __FILE__, __LINE__ + 1
1726
+ private
1727
+ def protected_#{password_field}
1728
+ @#{password_field}
1729
+ end
1730
+ EOS
1731
+ )
1732
+ end
1733
+
1734
+ def define_password_field_writer_method
1735
+ return if respond_to?("#{password_field}=")
1736
+
1737
+ self.class.send(:attr_writer, password_field)
1738
+ end
1739
+
1740
+ # Creating an alias method for the "record" method based on the klass
1741
+ # name, so that we can do:
1742
+ #
1743
+ # session.user
1744
+ #
1745
+ # instead of:
1746
+ #
1747
+ # session.record
1748
+ #
1749
+ # @api private
1750
+ def define_record_alias_method
1751
+ noun = klass_name.demodulize.underscore.to_sym
1752
+ return if respond_to?(noun)
1753
+
1754
+ self.class.send(:alias_method, noun, :record)
1755
+ end
1756
+
1757
+ def destroy_cookie
1758
+ controller.cookies.delete cookie_key, domain: controller.cookie_domain
1759
+ end
1760
+
1761
+ def disable_magic_states?
1762
+ self.class.disable_magic_states == true
1763
+ end
1764
+
1765
+ def enforce_timeout
1766
+ return unless stale?
1767
+
1768
+ self.stale_record = record
1769
+ self.record = nil
1770
+ end
1771
+
1772
+ def ensure_authentication_attempted
1773
+ return unless errors.empty? && attempted_record.nil?
1774
+
1775
+ errors.add(
1776
+ :base,
1777
+ I18n.t(
1778
+ "error_messages.no_authentication_details",
1779
+ default: "You did not provide any details for authentication."
1780
+ )
1781
+ )
1782
+ end
1783
+
1784
+ def exceeded_failed_logins_limit?
1785
+ !attempted_record.nil? &&
1786
+ attempted_record.respond_to?(:failed_login_count) &&
1787
+ consecutive_failed_logins_limit.positive? &&
1788
+ attempted_record.failed_login_count &&
1789
+ attempted_record.failed_login_count >= consecutive_failed_logins_limit
1790
+ end
1791
+
1792
+ # @deprecated in favor of `self.class.record_selection_method`
1793
+ def find_by_login_method
1794
+ ::ActiveSupport::Deprecation.warn(E_DPR_FIND_BY_LOGIN_METHOD)
1795
+ self.class.record_selection_method
1796
+ end
1797
+
1798
+ def generalize_credentials_error_messages?
1799
+ self.class.generalize_credentials_error_messages
1800
+ end
1801
+
1802
+ # @api private
1803
+ def generate_cookie_for_saving
1804
+ {
1805
+ value: generate_cookie_value.to_s,
1806
+ expires: remember_me_until,
1807
+ secure: secure,
1808
+ httponly: httponly,
1809
+ same_site: same_site,
1810
+ domain: controller.cookie_domain
1811
+ }
1812
+ end
1813
+
1814
+ def generate_cookie_value
1815
+ ::Authentication::Logic::CookieCredentials.new(
1816
+ record.persistence_token,
1817
+ record.send(record.class.primary_key),
1818
+ remember_me? ? remember_me_until : nil
1819
+ )
1820
+ end
1821
+
1822
+ # Returns a Proc to be executed by
1823
+ # `ActionController::HttpAuthentication::Basic` when credentials are
1824
+ # present in the HTTP request.
1825
+ #
1826
+ # @api private
1827
+ # @return Proc
1828
+ def http_auth_login_proc
1829
+ proc do |login, password|
1830
+ if !login.blank? && !password.blank?
1831
+ send("#{login_field}=", login)
1832
+ send("#{password_field}=", password)
1833
+ valid?
1834
+ end
1835
+ end
1836
+ end
1837
+
1838
+ def failed_login_ban_for
1839
+ self.class.failed_login_ban_for
1840
+ end
1841
+
1842
+ def increase_failed_login_count
1843
+ return unless invalid_password? && attempted_record.respond_to?(:failed_login_count)
1844
+
1845
+ attempted_record.failed_login_count ||= 0
1846
+ attempted_record.failed_login_count += 1
1847
+ end
1848
+
1849
+ def increment_login_count
1850
+ return unless record.respond_to?(:login_count)
1851
+
1852
+ record.login_count = (record.login_count.blank? ? 1 : record.login_count + 1)
1853
+ end
1854
+
1855
+ def klass
1856
+ self.class.klass
1857
+ end
1858
+
1859
+ def klass_name
1860
+ self.class.klass_name
1861
+ end
1862
+
1863
+ def last_request_at_threshold
1864
+ self.class.last_request_at_threshold
1865
+ end
1866
+
1867
+ def login_field
1868
+ self.class.login_field
1869
+ end
1870
+
1871
+ def logout_on_timeout?
1872
+ self.class.logout_on_timeout == true
1873
+ end
1874
+
1875
+ def params_credentials
1876
+ controller.params[params_key]
1877
+ end
1878
+
1879
+ def params_enabled?
1880
+ return false if !params_credentials || !klass.column_names.include?("single_access_token")
1881
+ return controller.single_access_allowed? if controller.responds_to_single_access_allowed?
1882
+
1883
+ params_enabled_by_allowed_request_types?
1884
+ end
1885
+
1886
+ def params_enabled_by_allowed_request_types?
1887
+ case single_access_allowed_request_types
1888
+ when Array
1889
+ single_access_allowed_request_types.include?(controller.request_content_type) ||
1890
+ single_access_allowed_request_types.include?(:all)
1891
+ else
1892
+ %i[all any].include?(single_access_allowed_request_types)
1893
+ end
1894
+ end
1895
+
1896
+ def params_key
1897
+ build_key(self.class.params_key)
1898
+ end
1899
+
1900
+ def password_field
1901
+ self.class.password_field
1902
+ end
1903
+
1904
+ # Tries to validate the session from information in the cookie
1905
+ def persist_by_cookie
1906
+ creds = cookie_credentials
1907
+ if creds&.persistence_token.present?
1908
+ record = search_for_record("find_by_#{klass.primary_key}", creds.record_id)
1909
+ self.unauthorized_record = record if record && record.persistence_token == creds.persistence_token
1910
+ valid?
1911
+ else
1912
+ false
1913
+ end
1914
+ end
1915
+
1916
+ def persist_by_params
1917
+ return false unless params_enabled?
1918
+
1919
+ self.unauthorized_record = search_for_record(
1920
+ "find_by_single_access_token",
1921
+ params_credentials
1922
+ )
1923
+ self.single_access = valid?
1924
+ end
1925
+
1926
+ def persist_by_http_auth
1927
+ login_proc = http_auth_login_proc
1928
+
1929
+ if self.class.request_http_basic_auth
1930
+ controller.authenticate_or_request_with_http_basic(
1931
+ self.class.http_basic_auth_realm,
1932
+ &login_proc
1933
+ )
1934
+ else
1935
+ controller.authenticate_with_http_basic(&login_proc)
1936
+ end
1937
+
1938
+ false
1939
+ end
1940
+
1941
+ def persist_by_http_auth?
1942
+ allow_http_basic_auth? && login_field && password_field
1943
+ end
1944
+
1945
+ # Tries to validate the session from information in the session
1946
+ def persist_by_session
1947
+ persistence_token, record_id = session_credentials
1948
+ if !persistence_token.nil?
1949
+ record = persist_by_session_search(persistence_token, record_id)
1950
+ self.unauthorized_record = record if record && record.persistence_token == persistence_token
1951
+ valid?
1952
+ else
1953
+ false
1954
+ end
1955
+ end
1956
+
1957
+ # Allow finding by persistence token, because when records are created
1958
+ # the session is maintained in a before_save, when there is no id.
1959
+ # This is done for performance reasons and to save on queries.
1960
+ def persist_by_session_search(persistence_token, record_id)
1961
+ if record_id.nil?
1962
+ search_for_record("find_by_persistence_token", persistence_token.to_s)
1963
+ else
1964
+ search_for_record("find_by_#{klass.primary_key}", record_id.to_s)
1965
+ end
1966
+ end
1967
+
1968
+ def reset_stale_state
1969
+ self.stale_record = nil
1970
+ end
1971
+
1972
+ def reset_perishable_token!
1973
+ if record.respond_to?(:reset_perishable_token) &&
1974
+ !record.disable_perishable_token_maintenance?
1975
+ record.reset_perishable_token
1976
+ end
1977
+ end
1978
+
1979
+ # @api private
1980
+ def required_magic_states_for(record)
1981
+ %i[active approved confirmed].select do |state|
1982
+ record.respond_to?("#{state}?")
1983
+ end
1984
+ end
1985
+
1986
+ def reset_failed_login_count?
1987
+ exceeded_failed_logins_limit? && !being_brute_force_protected?
1988
+ end
1989
+
1990
+ def reset_failed_login_count
1991
+ attempted_record.failed_login_count = 0
1992
+ end
1993
+
1994
+ # @api private
1995
+ def run_the_after_validation_callbacks
1996
+ run_callbacks(new_session? ? :after_validation_on_create : :after_validation_on_update)
1997
+ run_callbacks(:after_validation)
1998
+ end
1999
+
2000
+ # @api private
2001
+ def run_the_before_validation_callbacks
2002
+ run_callbacks(:before_validation)
2003
+ run_callbacks(new_session? ? :before_validation_on_create : :before_validation_on_update)
2004
+ end
2005
+
2006
+ # `args[0]` is the name of a model method, like
2007
+ # `find_by_single_access_token` or `find_by_smart_case_login_field`.
2008
+ def search_for_record(*args)
2009
+ search_scope.scoping do
2010
+ klass.send(*args)
2011
+ end
2012
+ end
2013
+
2014
+ # Returns an AR relation representing the scope of the search. The
2015
+ # relation is either provided directly by, or defined by
2016
+ # `find_options`.
2017
+ def search_scope
2018
+ if scope[:find_options].is_a?(ActiveRecord::Relation)
2019
+ scope[:find_options]
2020
+ else
2021
+ conditions = scope[:find_options] && scope[:find_options][:conditions] || {}
2022
+ klass.send(:where, conditions)
2023
+ end
2024
+ end
2025
+
2026
+ # @api private
2027
+ def set_last_request_at
2028
+ current_time = Time.current
2029
+ MagicColumn::AssignsLastRequestAt
2030
+ .new(current_time, record, controller, last_request_at_threshold)
2031
+ .assign
2032
+ end
2033
+
2034
+ def single_access?
2035
+ single_access == true
2036
+ end
2037
+
2038
+ def single_access_allowed_request_types
2039
+ self.class.single_access_allowed_request_types
2040
+ end
2041
+
2042
+ def save_cookie
2043
+ cookie_jar[cookie_key] = generate_cookie_for_saving
2044
+ end
2045
+
2046
+ # @api private
2047
+ # @return [String] - Examples:
2048
+ # - user_credentials_id
2049
+ # - ziggity_zack_user_credentials_id
2050
+ # - ziggity_zack is an "id", see `#id`
2051
+ # - see persistence_token_test.rb
2052
+ def session_compound_key
2053
+ "#{session_key}_#{klass.primary_key}"
2054
+ end
2055
+
2056
+ def session_credentials
2057
+ [
2058
+ controller.session[session_key],
2059
+ controller.session[session_compound_key]
2060
+ ].collect { |i| i.nil? ? i : i.to_s }.compact
2061
+ end
2062
+
2063
+ # @return [String] - Examples:
2064
+ # - user_credentials
2065
+ # - ziggity_zack_user_credentials
2066
+ # - ziggity_zack is an "id", see `#id`
2067
+ # - see persistence_token_test.rb
2068
+ def session_key
2069
+ build_key(self.class.session_key)
2070
+ end
2071
+
2072
+ def update_info
2073
+ increment_login_count
2074
+ clear_failed_login_count
2075
+ update_login_timestamps
2076
+ update_login_ip_addresses
2077
+ end
2078
+
2079
+ def update_login_ip_addresses
2080
+ return unless record.respond_to?(:current_login_ip)
2081
+
2082
+ record.last_login_ip = record.current_login_ip if record.respond_to?(:last_login_ip)
2083
+ record.current_login_ip = controller.request.ip
2084
+ end
2085
+
2086
+ def update_login_timestamps
2087
+ return unless record.respond_to?(:current_login_at)
2088
+
2089
+ record.last_login_at = record.current_login_at if record.respond_to?(:last_login_at)
2090
+ record.current_login_at = Time.current
2091
+ end
2092
+
2093
+ def update_session
2094
+ update_session_set_persistence_token
2095
+ update_session_set_primary_key
2096
+ end
2097
+
2098
+ # Updates the session, setting the primary key (usually `id`) of the
2099
+ # record.
2100
+ #
2101
+ # @api private
2102
+ def update_session_set_primary_key
2103
+ compound_key = session_compound_key
2104
+ controller.session[compound_key] = record && record.send(record.class.primary_key)
2105
+ end
2106
+
2107
+ # Updates the session, setting the `persistence_token` of the record.
2108
+ #
2109
+ # @api private
2110
+ def update_session_set_persistence_token
2111
+ controller.session[session_key] = record && record.persistence_token
2112
+ end
2113
+
2114
+ # In keeping with the metaphor of ActiveRecord, verification of the
2115
+ # password is referred to as a "validation".
2116
+ def validate_by_password
2117
+ self.invalid_password = false
2118
+ validate_by_password__blank_fields
2119
+ return if errors.count.positive?
2120
+
2121
+ self.attempted_record = search_for_record(
2122
+ self.class.record_selection_method,
2123
+ send(login_field)
2124
+ )
2125
+ if attempted_record.blank?
2126
+ add_login_not_found_error
2127
+ return
2128
+ end
2129
+ validate_by_password__invalid_password
2130
+ end
2131
+
2132
+ def validate_by_password__blank_fields
2133
+ if send(login_field).blank?
2134
+ errors.add(
2135
+ login_field,
2136
+ I18n.t("error_messages.login_blank", default: "cannot be blank")
2137
+ )
2138
+ end
2139
+ return unless send("protected_#{password_field}").blank?
2140
+
2141
+ errors.add(
2142
+ password_field,
2143
+ I18n.t("error_messages.password_blank", default: "cannot be blank")
2144
+ )
2145
+ end
2146
+
2147
+ # Verify the password, usually using `valid_password?` in
2148
+ # `acts_as_authentic/password.rb`. If it cannot be verified, we
2149
+ # refer to it as "invalid".
2150
+ def validate_by_password__invalid_password
2151
+ unless attempted_record.send(
2152
+ verify_password_method,
2153
+ send("protected_#{password_field}")
2154
+ )
2155
+ self.invalid_password = true
2156
+ add_invalid_password_error
2157
+ end
2158
+ end
2159
+
2160
+ def validate_by_unauthorized_record
2161
+ self.attempted_record = unauthorized_record
2162
+ end
2163
+
2164
+ def validate_magic_states
2165
+ return true if attempted_record.nil?
2166
+
2167
+ required_magic_states_for(attempted_record).each do |required_status|
2168
+ next if attempted_record.send("#{required_status}?")
2169
+
2170
+ errors.add(
2171
+ :base,
2172
+ I18n.t(
2173
+ "error_messages.not_#{required_status}",
2174
+ default: "Your account is not #{required_status}"
2175
+ )
2176
+ )
2177
+ return false
2178
+ end
2179
+ true
2180
+ end
2181
+
2182
+ def validate_failed_logins
2183
+ # Clear all other error messages, as they are irrelevant at this point and can
2184
+ # only provide additional information that is not needed
2185
+ errors.clear
2186
+ duration = failed_login_ban_for.zero? ? "" : " temporarily"
2187
+ errors.add(
2188
+ :base,
2189
+ I18n.t(
2190
+ "error_messages.consecutive_failed_logins_limit_exceeded",
2191
+ default: format(
2192
+ "Consecutive failed logins limit exceeded, account has been%s disabled.",
2193
+ duration
2194
+ )
2195
+ )
2196
+ )
2197
+ end
2198
+
2199
+ def verify_password_method
2200
+ self.class.verify_password_method
2201
+ end
2202
+ end
2203
+ end
2204
+ end
2205
+ end