authentication-logic 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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