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