authlogic 4.5.0 → 6.4.2

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 (153) hide show
  1. checksums.yaml +4 -4
  2. data/lib/authlogic/acts_as_authentic/base.rb +19 -19
  3. data/lib/authlogic/acts_as_authentic/email.rb +3 -170
  4. data/lib/authlogic/acts_as_authentic/logged_in_status.rb +3 -1
  5. data/lib/authlogic/acts_as_authentic/login.rb +7 -174
  6. data/lib/authlogic/acts_as_authentic/magic_columns.rb +7 -4
  7. data/lib/authlogic/acts_as_authentic/password.rb +67 -256
  8. data/lib/authlogic/acts_as_authentic/perishable_token.rb +8 -5
  9. data/lib/authlogic/acts_as_authentic/persistence_token.rb +10 -4
  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 +36 -20
  12. data/lib/authlogic/acts_as_authentic/session_maintenance.rb +12 -8
  13. data/lib/authlogic/acts_as_authentic/single_access_token.rb +10 -8
  14. data/lib/authlogic/config.rb +9 -1
  15. data/lib/authlogic/controller_adapters/abstract_adapter.rb +28 -4
  16. data/lib/authlogic/controller_adapters/rack_adapter.rb +2 -0
  17. data/lib/authlogic/controller_adapters/rails_adapter.rb +7 -30
  18. data/lib/authlogic/controller_adapters/sinatra_adapter.rb +6 -0
  19. data/lib/authlogic/cookie_credentials.rb +63 -0
  20. data/lib/authlogic/crypto_providers/bcrypt.rb +3 -3
  21. data/lib/authlogic/crypto_providers/md5/v2.rb +35 -0
  22. data/lib/authlogic/crypto_providers/md5.rb +6 -6
  23. data/lib/authlogic/crypto_providers/scrypt.rb +2 -0
  24. data/lib/authlogic/crypto_providers/sha1/v2.rb +41 -0
  25. data/lib/authlogic/crypto_providers/sha1.rb +7 -6
  26. data/lib/authlogic/crypto_providers/sha256/v2.rb +58 -0
  27. data/lib/authlogic/crypto_providers/sha256.rb +5 -0
  28. data/lib/authlogic/crypto_providers/sha512/v2.rb +39 -0
  29. data/lib/authlogic/crypto_providers/sha512.rb +9 -5
  30. data/lib/authlogic/crypto_providers.rb +5 -20
  31. data/lib/authlogic/errors.rb +50 -0
  32. data/lib/authlogic/i18n/translator.rb +4 -1
  33. data/lib/authlogic/i18n.rb +3 -1
  34. data/lib/authlogic/random.rb +2 -0
  35. data/lib/authlogic/session/base.rb +2197 -39
  36. data/lib/authlogic/session/magic_column/assigns_last_request_at.rb +46 -0
  37. data/lib/authlogic/test_case/mock_api_controller.rb +52 -0
  38. data/lib/authlogic/test_case/mock_controller.rb +3 -1
  39. data/lib/authlogic/test_case/mock_cookie_jar.rb +32 -6
  40. data/lib/authlogic/test_case/mock_logger.rb +2 -0
  41. data/lib/authlogic/test_case/mock_request.rb +12 -0
  42. data/lib/authlogic/test_case/rails_request_adapter.rb +9 -1
  43. data/lib/authlogic/test_case.rb +5 -0
  44. data/lib/authlogic/version.rb +2 -1
  45. data/lib/authlogic.rb +5 -28
  46. metadata +175 -200
  47. data/.github/ISSUE_TEMPLATE/bug_report.md +0 -28
  48. data/.github/ISSUE_TEMPLATE/feature_proposal.md +0 -32
  49. data/.github/triage.md +0 -86
  50. data/.gitignore +0 -15
  51. data/.rubocop.yml +0 -133
  52. data/.rubocop_todo.yml +0 -74
  53. data/.travis.yml +0 -24
  54. data/CHANGELOG.md +0 -348
  55. data/CONTRIBUTING.md +0 -91
  56. data/Gemfile +0 -6
  57. data/LICENSE +0 -20
  58. data/README.md +0 -448
  59. data/Rakefile +0 -21
  60. data/UPGRADING.md +0 -22
  61. data/authlogic.gemspec +0 -40
  62. data/doc/use_normal_rails_validation.md +0 -82
  63. data/gemfiles/Gemfile.rails-4.2.x +0 -6
  64. data/gemfiles/Gemfile.rails-5.1.x +0 -6
  65. data/gemfiles/Gemfile.rails-5.2.x +0 -6
  66. data/lib/authlogic/acts_as_authentic/restful_authentication.rb +0 -106
  67. data/lib/authlogic/acts_as_authentic/validations_scope.rb +0 -35
  68. data/lib/authlogic/authenticates_many/association.rb +0 -50
  69. data/lib/authlogic/authenticates_many/base.rb +0 -81
  70. data/lib/authlogic/crypto_providers/aes256.rb +0 -71
  71. data/lib/authlogic/crypto_providers/wordpress.rb +0 -72
  72. data/lib/authlogic/regex.rb +0 -79
  73. data/lib/authlogic/session/activation.rb +0 -73
  74. data/lib/authlogic/session/active_record_trickery.rb +0 -65
  75. data/lib/authlogic/session/brute_force_protection.rb +0 -127
  76. data/lib/authlogic/session/callbacks.rb +0 -153
  77. data/lib/authlogic/session/cookies.rb +0 -329
  78. data/lib/authlogic/session/existence.rb +0 -103
  79. data/lib/authlogic/session/foundation.rb +0 -105
  80. data/lib/authlogic/session/http_auth.rb +0 -107
  81. data/lib/authlogic/session/id.rb +0 -53
  82. data/lib/authlogic/session/klass.rb +0 -73
  83. data/lib/authlogic/session/magic_columns.rb +0 -119
  84. data/lib/authlogic/session/magic_states.rb +0 -82
  85. data/lib/authlogic/session/params.rb +0 -130
  86. data/lib/authlogic/session/password.rb +0 -318
  87. data/lib/authlogic/session/perishable_token.rb +0 -24
  88. data/lib/authlogic/session/persistence.rb +0 -77
  89. data/lib/authlogic/session/priority_record.rb +0 -38
  90. data/lib/authlogic/session/scopes.rb +0 -138
  91. data/lib/authlogic/session/session.rb +0 -77
  92. data/lib/authlogic/session/timeout.rb +0 -103
  93. data/lib/authlogic/session/unauthorized_record.rb +0 -56
  94. data/lib/authlogic/session/validation.rb +0 -93
  95. data/test/acts_as_authentic_test/base_test.rb +0 -27
  96. data/test/acts_as_authentic_test/email_test.rb +0 -241
  97. data/test/acts_as_authentic_test/logged_in_status_test.rb +0 -64
  98. data/test/acts_as_authentic_test/login_test.rb +0 -153
  99. data/test/acts_as_authentic_test/magic_columns_test.rb +0 -29
  100. data/test/acts_as_authentic_test/password_test.rb +0 -263
  101. data/test/acts_as_authentic_test/perishable_token_test.rb +0 -98
  102. data/test/acts_as_authentic_test/persistence_token_test.rb +0 -62
  103. data/test/acts_as_authentic_test/restful_authentication_test.rb +0 -48
  104. data/test/acts_as_authentic_test/session_maintenance_test.rb +0 -150
  105. data/test/acts_as_authentic_test/single_access_test.rb +0 -46
  106. data/test/adapter_test.rb +0 -23
  107. data/test/authenticates_many_test.rb +0 -33
  108. data/test/config_test.rb +0 -38
  109. data/test/crypto_provider_test/aes256_test.rb +0 -16
  110. data/test/crypto_provider_test/bcrypt_test.rb +0 -16
  111. data/test/crypto_provider_test/scrypt_test.rb +0 -16
  112. data/test/crypto_provider_test/sha1_test.rb +0 -25
  113. data/test/crypto_provider_test/sha256_test.rb +0 -16
  114. data/test/crypto_provider_test/sha512_test.rb +0 -16
  115. data/test/crypto_provider_test/wordpress_test.rb +0 -26
  116. data/test/fixtures/companies.yml +0 -5
  117. data/test/fixtures/employees.yml +0 -17
  118. data/test/fixtures/projects.yml +0 -3
  119. data/test/fixtures/users.yml +0 -41
  120. data/test/i18n/lol.yml +0 -4
  121. data/test/i18n_test.rb +0 -35
  122. data/test/libs/affiliate.rb +0 -9
  123. data/test/libs/company.rb +0 -8
  124. data/test/libs/employee.rb +0 -9
  125. data/test/libs/employee_session.rb +0 -4
  126. data/test/libs/ldaper.rb +0 -5
  127. data/test/libs/project.rb +0 -5
  128. data/test/libs/user.rb +0 -9
  129. data/test/libs/user_session.rb +0 -27
  130. data/test/random_test.rb +0 -15
  131. data/test/session_test/activation_test.rb +0 -45
  132. data/test/session_test/active_record_trickery_test.rb +0 -78
  133. data/test/session_test/brute_force_protection_test.rb +0 -110
  134. data/test/session_test/callbacks_test.rb +0 -42
  135. data/test/session_test/cookies_test.rb +0 -244
  136. data/test/session_test/credentials_test.rb +0 -0
  137. data/test/session_test/existence_test.rb +0 -88
  138. data/test/session_test/foundation_test.rb +0 -24
  139. data/test/session_test/http_auth_test.rb +0 -60
  140. data/test/session_test/id_test.rb +0 -19
  141. data/test/session_test/klass_test.rb +0 -42
  142. data/test/session_test/magic_columns_test.rb +0 -62
  143. data/test/session_test/magic_states_test.rb +0 -60
  144. data/test/session_test/params_test.rb +0 -61
  145. data/test/session_test/password_test.rb +0 -107
  146. data/test/session_test/perishability_test.rb +0 -17
  147. data/test/session_test/persistence_test.rb +0 -35
  148. data/test/session_test/scopes_test.rb +0 -68
  149. data/test/session_test/session_test.rb +0 -80
  150. data/test/session_test/timeout_test.rb +0 -84
  151. data/test/session_test/unauthorized_record_test.rb +0 -15
  152. data/test/session_test/validation_test.rb +0 -25
  153. data/test/test_helper.rb +0 -272
@@ -1,49 +1,2207 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "request_store"
4
+
1
5
  module Authlogic
2
- module Session # :nodoc:
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
+
3
33
  # This is the most important class in Authlogic. You will inherit this class
4
34
  # for your own eg. `UserSession`.
5
35
  #
6
- # Code is organized topically. Each topic is represented by a module. So, to
7
- # learn about password-based authentication, read the `Password` module.
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:
8
301
  #
9
- # It is common for methods (.initialize and #credentials=, for example) to
10
- # be implemented in multiple mixins. Those methods will call `super`, so the
11
- # order of `include`s here is important.
302
+ # ```
303
+ # class UserSession
304
+ # validate :check_if_awesome
12
305
  #
13
- # Also, to fully understand such a method (like #credentials=) you will need
14
- # to mentally combine all of its definitions. This is perhaps the primary
15
- # disadvantage of topical organization using modules.
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
+ # ```
16
318
  class Base
17
- include Foundation
18
- include Callbacks
19
-
20
- # Included first so that the session resets itself to nil
21
- include Timeout
22
-
23
- # Included in a specific order so they are tried in this order when persisting
24
- include Params
25
- include Cookies
26
- include Session
27
- include HttpAuth
28
-
29
- # Included in a specific order so magic states gets run after a record is found
30
- # TODO: What does "magic states gets run" mean? Be specific.
31
- include Password
32
- include UnauthorizedRecord
33
- include MagicStates
34
-
35
- include Activation
36
- include ActiveRecordTrickery
37
- include BruteForceProtection
38
- include Existence
39
- include Klass
40
- include MagicColumns
41
- include PerishableToken
42
- include Persistence
43
- include Scopes
44
- include Id
45
- include Validation
46
- 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
+ E_DPR_FIND_BY_LOGIN_METHOD = <<~EOS.squish.freeze
355
+ find_by_login_method is deprecated in favor of record_selection_method,
356
+ to avoid confusion with ActiveRecord's "Dynamic Finders".
357
+ (https://guides.rubyonrails.org/v6.0/active_record_querying.html#dynamic-finders)
358
+ For example, rubocop-rails is confused by the deprecated method.
359
+ (https://github.com/rubocop-hq/rubocop-rails/blob/master/lib/rubocop/cop/rails/dynamic_find_by.rb)
360
+ EOS
361
+ VALID_SAME_SITE_VALUES = [nil, "Lax", "Strict", "None"].freeze
362
+
363
+ # Callbacks
364
+ # =========
365
+
366
+ METHODS = %w[
367
+ before_persisting
368
+ persist
369
+ after_persisting
370
+ before_validation
371
+ before_validation_on_create
372
+ before_validation_on_update
373
+ validate
374
+ after_validation_on_update
375
+ after_validation_on_create
376
+ after_validation
377
+ before_save
378
+ before_create
379
+ before_update
380
+ after_update
381
+ after_create
382
+ after_save
383
+ before_destroy
384
+ after_destroy
385
+ ].freeze
386
+
387
+ # Defines the "callback installation methods" used below.
388
+ METHODS.each do |method|
389
+ class_eval <<-EOS, __FILE__, __LINE__ + 1
390
+ def self.#{method}(*filter_list, &block)
391
+ set_callback(:#{method}, *filter_list, &block)
392
+ end
393
+ EOS
394
+ end
395
+
396
+ # Defines session life cycle events that support callbacks.
397
+ define_callbacks(
398
+ *METHODS,
399
+ terminator: ->(_target, result_lambda) { result_lambda.call == false }
400
+ )
401
+ define_callbacks(
402
+ "persist",
403
+ terminator: ->(_target, result_lambda) { result_lambda.call == true }
404
+ )
405
+
406
+ # Use the "callback installation methods" defined above
407
+ # -----------------------------------------------------
408
+
409
+ before_persisting :reset_stale_state
410
+
411
+ # `persist` callbacks, in order of priority
412
+ persist :persist_by_params
413
+ persist :persist_by_cookie
414
+ persist :persist_by_session
415
+ persist :persist_by_http_auth, if: :persist_by_http_auth?
416
+
417
+ after_persisting :enforce_timeout
418
+ after_persisting :update_session, unless: :single_access?
419
+ after_persisting :set_last_request_at
420
+
421
+ before_save :update_info
422
+ before_save :set_last_request_at
423
+
424
+ after_save :reset_perishable_token!
425
+ after_save :save_cookie, if: :cookie_enabled?
426
+ after_save :update_session
427
+ after_create :renew_session_id
428
+
429
+ after_destroy :destroy_cookie, if: :cookie_enabled?
430
+ after_destroy :update_session
431
+
432
+ # `validate` callbacks, in deliberate order. For example,
433
+ # validate_magic_states must run *after* a record is found.
434
+ validate :validate_by_password, if: :authenticating_with_password?
435
+ validate(
436
+ :validate_by_unauthorized_record,
437
+ if: :authenticating_with_unauthorized_record?
438
+ )
439
+ validate :validate_magic_states, unless: :disable_magic_states?
440
+ validate :reset_failed_login_count, if: :reset_failed_login_count?
441
+ validate :validate_failed_logins, if: :being_brute_force_protected?
442
+ validate :increase_failed_login_count
443
+
444
+ # Accessors
445
+ # =========
446
+
447
+ class << self
448
+ attr_accessor(
449
+ :configured_password_methods
450
+ )
451
+ end
452
+ attr_accessor(
453
+ :invalid_password,
454
+ :new_session,
455
+ :priority_record,
456
+ :record,
457
+ :single_access,
458
+ :stale_record,
459
+ :unauthorized_record
460
+ )
461
+ attr_writer(
462
+ :scope,
463
+ :id
464
+ )
465
+
466
+ # Public class methods
467
+ # ====================
468
+
469
+ class << self
470
+ # Returns true if a controller has been set and can be used properly.
471
+ # This MUST be set before anything can be done. Similar to how
472
+ # ActiveRecord won't allow you to do anything without establishing a DB
473
+ # connection. In your framework environment this is done for you, but if
474
+ # you are using Authlogic outside of your framework, you need to assign
475
+ # a controller object to Authlogic via
476
+ # Authlogic::Session::Base.controller = obj. See the controller= method
477
+ # for more information.
478
+ def activated?
479
+ !controller.nil?
480
+ end
481
+
482
+ # Allow users to log in via HTTP basic authentication.
483
+ #
484
+ # * <tt>Default:</tt> false
485
+ # * <tt>Accepts:</tt> Boolean
486
+ def allow_http_basic_auth(value = nil)
487
+ rw_config(:allow_http_basic_auth, value, false)
488
+ end
489
+ alias allow_http_basic_auth= allow_http_basic_auth
490
+
491
+ # Lets you change which model to use for authentication.
492
+ #
493
+ # * <tt>Default:</tt> inferred from the class name. UserSession would
494
+ # automatically try User
495
+ # * <tt>Accepts:</tt> an ActiveRecord class
496
+ def authenticate_with(klass)
497
+ @klass_name = klass.name
498
+ @klass = klass
499
+ end
500
+ alias authenticate_with= authenticate_with
501
+
502
+ # The current controller object
503
+ def controller
504
+ RequestStore.store[:authlogic_controller]
505
+ end
506
+
507
+ # This accepts a controller object wrapped with the Authlogic controller
508
+ # adapter. The controller adapters close the gap between the different
509
+ # controllers in each framework. That being said, Authlogic is expecting
510
+ # your object's class to extend
511
+ # Authlogic::ControllerAdapters::AbstractAdapter. See
512
+ # Authlogic::ControllerAdapters for more info.
513
+ #
514
+ # Lastly, this is thread safe.
515
+ def controller=(value)
516
+ RequestStore.store[:authlogic_controller] = value
517
+ end
518
+
519
+ # To help protect from brute force attacks you can set a limit on the
520
+ # allowed number of consecutive failed logins. By default this is 50,
521
+ # this is a very liberal number, and if someone fails to login after 50
522
+ # tries it should be pretty obvious that it's a machine trying to login
523
+ # in and very likely a brute force attack.
524
+ #
525
+ # In order to enable this field your model MUST have a
526
+ # failed_login_count (integer) field.
527
+ #
528
+ # If you don't know what a brute force attack is, it's when a machine
529
+ # tries to login into a system using every combination of character
530
+ # possible. Thus resulting in possibly millions of attempts to log into
531
+ # an account.
532
+ #
533
+ # * <tt>Default:</tt> 50
534
+ # * <tt>Accepts:</tt> Integer, set to 0 to disable
535
+ def consecutive_failed_logins_limit(value = nil)
536
+ rw_config(:consecutive_failed_logins_limit, value, 50)
537
+ end
538
+ alias consecutive_failed_logins_limit= consecutive_failed_logins_limit
539
+
540
+ # The name of the cookie or the key in the cookies hash. Be sure and use
541
+ # a unique name. If you have multiple sessions and they use the same
542
+ # cookie it will cause problems. Also, if a id is set it will be
543
+ # inserted into the beginning of the string. Example:
544
+ #
545
+ # session = UserSession.new
546
+ # session.cookie_key => "user_credentials"
547
+ #
548
+ # session = UserSession.new(:super_high_secret)
549
+ # session.cookie_key => "super_high_secret_user_credentials"
550
+ #
551
+ # * <tt>Default:</tt> "#{klass_name.underscore}_credentials"
552
+ # * <tt>Accepts:</tt> String
553
+ def cookie_key(value = nil)
554
+ rw_config(:cookie_key, value, "#{klass_name.underscore}_credentials")
555
+ end
556
+ alias cookie_key= cookie_key
557
+
558
+ # A convenience method. The same as:
559
+ #
560
+ # session = UserSession.new(*args)
561
+ # session.save
562
+ #
563
+ # Instead you can do:
564
+ #
565
+ # UserSession.create(*args)
566
+ def create(*args, &block)
567
+ session = new(*args)
568
+ session.save(&block)
569
+ session
570
+ end
571
+
572
+ # Same as create but calls create!, which raises an exception when
573
+ # validation fails.
574
+ def create!(*args)
575
+ session = new(*args)
576
+ session.save!
577
+ session
578
+ end
579
+
580
+ # Set this to true if you want to disable the checking of active?, approved?, and
581
+ # confirmed? on your record. This is more or less of a convenience feature, since
582
+ # 99% of the time if those methods exist and return false you will not want the
583
+ # user logging in. You could easily accomplish this same thing with a
584
+ # before_validation method or other callbacks.
585
+ #
586
+ # * <tt>Default:</tt> false
587
+ # * <tt>Accepts:</tt> Boolean
588
+ def disable_magic_states(value = nil)
589
+ rw_config(:disable_magic_states, value, false)
590
+ end
591
+ alias disable_magic_states= disable_magic_states
592
+
593
+ # Once the failed logins limit has been exceed, how long do you want to
594
+ # ban the user? This can be a temporary or permanent ban.
595
+ #
596
+ # * <tt>Default:</tt> 2.hours
597
+ # * <tt>Accepts:</tt> Fixnum, set to 0 for permanent ban
598
+ def failed_login_ban_for(value = nil)
599
+ rw_config(:failed_login_ban_for, (!value.nil? && value) || value, 2.hours.to_i)
600
+ end
601
+ alias failed_login_ban_for= failed_login_ban_for
602
+
603
+ # This is how you persist a session. This finds the record for the
604
+ # current session using a variety of methods. It basically tries to "log
605
+ # in" the user without the user having to explicitly log in. Check out
606
+ # the other Authlogic::Session modules for more information.
607
+ #
608
+ # The best way to use this method is something like:
609
+ #
610
+ # helper_method :current_user_session, :current_user
611
+ #
612
+ # def current_user_session
613
+ # return @current_user_session if defined?(@current_user_session)
614
+ # @current_user_session = UserSession.find
615
+ # end
616
+ #
617
+ # def current_user
618
+ # return @current_user if defined?(@current_user)
619
+ # @current_user = current_user_session && current_user_session.user
620
+ # end
621
+ #
622
+ # Also, this method accepts a single parameter as the id, to find
623
+ # session that you marked with an id:
624
+ #
625
+ # UserSession.find(:secure)
626
+ #
627
+ # See the id method for more information on ids.
628
+ #
629
+ # Priority Record
630
+ # ===============
631
+ #
632
+ # This internal feature supports ActiveRecord's optimistic locking feature,
633
+ # which is automatically enabled when a table has a `lock_version` column.
634
+ #
635
+ # ```
636
+ # # https://api.rubyonrails.org/classes/ActiveRecord/Locking/Optimistic.html
637
+ # p1 = Person.find(1)
638
+ # p2 = Person.find(1)
639
+ # p1.first_name = "Michael"
640
+ # p1.save
641
+ # p2.first_name = "should fail"
642
+ # p2.save # Raises an ActiveRecord::StaleObjectError
643
+ # ```
644
+ #
645
+ # Now, consider the following Authlogic scenario:
646
+ #
647
+ # ```
648
+ # User.log_in_after_password_change = true
649
+ # ben = User.find(1)
650
+ # UserSession.create(ben)
651
+ # ben.password = "newpasswd"
652
+ # ben.password_confirmation = "newpasswd"
653
+ # ben.save
654
+ # ```
655
+ #
656
+ # We've used one of Authlogic's session maintenance features,
657
+ # `log_in_after_password_change`. So, when we call `ben.save`, there is a
658
+ # `before_save` callback that logs Ben in (`UserSession.find`). Well, when
659
+ # we log Ben in, we update his user record, eg. `login_count`. When we're
660
+ # done logging Ben in, then the normal `ben.save` happens. So, there were
661
+ # two `update` queries. If those two updates came from different User
662
+ # instances, we would get a `StaleObjectError`.
663
+ #
664
+ # Our solution is to carefully pass around a single `User` instance, using
665
+ # it for all `update` queries, thus avoiding the `StaleObjectError`.
666
+ def find(id = nil, priority_record = nil)
667
+ session = new({ priority_record: priority_record }, id)
668
+ session.priority_record = priority_record
669
+ if session.persisting?
670
+ session
671
+ end
672
+ end
673
+
674
+ # @deprecated in favor of record_selection_method
675
+ def find_by_login_method(value = nil)
676
+ ::ActiveSupport::Deprecation.warn(E_DPR_FIND_BY_LOGIN_METHOD)
677
+ record_selection_method(value)
678
+ end
679
+ alias find_by_login_method= find_by_login_method
680
+
681
+ # The text used to identify credentials (username/password) combination
682
+ # when a bad login attempt occurs. When you show error messages for a
683
+ # bad login, it's considered good security practice to hide which field
684
+ # the user has entered incorrectly (the login field or the password
685
+ # field). For a full explanation, see
686
+ # http://www.gnucitizen.org/blog/username-enumeration-vulnerabilities/
687
+ #
688
+ # Example of use:
689
+ #
690
+ # class UserSession < Authlogic::Session::Base
691
+ # generalize_credentials_error_messages true
692
+ # end
693
+ #
694
+ # This would make the error message for bad logins and bad passwords
695
+ # look identical:
696
+ #
697
+ # Login/Password combination is not valid
698
+ #
699
+ # Alternatively you may use a custom message:
700
+ #
701
+ # class UserSession < AuthLogic::Session::Base
702
+ # generalize_credentials_error_messages "Your login information is invalid"
703
+ # end
704
+ #
705
+ # This will instead show your custom error message when the UserSession is invalid.
706
+ #
707
+ # The downside to enabling this is that is can be too vague for a user
708
+ # that has a hard time remembering their username and password
709
+ # combinations. It also disables the ability to to highlight the field
710
+ # with the error when you use form_for.
711
+ #
712
+ # If you are developing an app where security is an extreme priority
713
+ # (such as a financial application), then you should enable this.
714
+ # Otherwise, leaving this off is fine.
715
+ #
716
+ # * <tt>Default</tt> false
717
+ # * <tt>Accepts:</tt> Boolean
718
+ def generalize_credentials_error_messages(value = nil)
719
+ rw_config(:generalize_credentials_error_messages, value, false)
720
+ end
721
+ alias generalize_credentials_error_messages= generalize_credentials_error_messages
722
+
723
+ # HTTP authentication realm
724
+ #
725
+ # Sets the HTTP authentication realm.
726
+ #
727
+ # Note: This option has no effect unless request_http_basic_auth is true
728
+ #
729
+ # * <tt>Default:</tt> 'Application'
730
+ # * <tt>Accepts:</tt> String
731
+ def http_basic_auth_realm(value = nil)
732
+ rw_config(:http_basic_auth_realm, value, "Application")
733
+ end
734
+ alias http_basic_auth_realm= http_basic_auth_realm
735
+
736
+ # Should the cookie be set as httponly? If true, the cookie will not be
737
+ # accessible from javascript
738
+ #
739
+ # * <tt>Default:</tt> true
740
+ # * <tt>Accepts:</tt> Boolean
741
+ def httponly(value = nil)
742
+ rw_config(:httponly, value, true)
743
+ end
744
+ alias httponly= httponly
745
+
746
+ # How to name the class, works JUST LIKE ActiveRecord, except it uses
747
+ # the following namespace:
748
+ #
749
+ # authlogic.models.user_session
750
+ def human_name(*)
751
+ I18n.t("models.#{name.underscore}", count: 1, default: name.humanize)
752
+ end
753
+
754
+ def i18n_scope
755
+ I18n.scope
756
+ end
757
+
758
+ # The name of the class that this session is authenticating with. For
759
+ # example, the UserSession class will authenticate with the User class
760
+ # unless you specify otherwise in your configuration. See
761
+ # authenticate_with for information on how to change this value.
762
+ #
763
+ # @api public
764
+ def klass
765
+ @klass ||= klass_name ? klass_name.constantize : nil
766
+ end
767
+
768
+ # The model name, guessed from the session class name, e.g. "User",
769
+ # from "UserSession".
770
+ #
771
+ # TODO: This method can return nil. We should explore this. It seems
772
+ # likely to cause a NoMethodError later, so perhaps we should raise an
773
+ # error instead.
774
+ #
775
+ # @api private
776
+ def klass_name
777
+ return @klass_name if instance_variable_defined?(:@klass_name)
778
+ @klass_name = name.scan(/(.*)Session/)[0]&.first
779
+ end
780
+
781
+ # The name of the method you want Authlogic to create for storing the
782
+ # login / username. Keep in mind this is just for your
783
+ # Authlogic::Session, if you want it can be something completely
784
+ # different than the field in your model. So if you wanted people to
785
+ # login with a field called "login" and then find users by email this is
786
+ # completely doable. See the `record_selection_method` configuration
787
+ # option for details.
788
+ #
789
+ # * <tt>Default:</tt> klass.login_field || klass.email_field
790
+ # * <tt>Accepts:</tt> Symbol or String
791
+ def login_field(value = nil)
792
+ rw_config(:login_field, value, klass.login_field || klass.email_field)
793
+ end
794
+ alias login_field= login_field
795
+
796
+ # With acts_as_authentic you get a :logged_in_timeout configuration
797
+ # option. If this is set, after this amount of time has passed the user
798
+ # will be marked as logged out. Obviously, since web based apps are on a
799
+ # per request basis, we have to define a time limit threshold that
800
+ # determines when we consider a user to be "logged out". Meaning, if
801
+ # they login and then leave the website, when do mark them as logged
802
+ # out? I recommend just using this as a fun feature on your website or
803
+ # reports, giving you a ballpark number of users logged in and active.
804
+ # This is not meant to be a dead accurate representation of a user's
805
+ # logged in state, since there is really no real way to do this with web
806
+ # based apps. Think about a user that logs in and doesn't log out. There
807
+ # is no action that tells you that the user isn't technically still
808
+ # logged in and active.
809
+ #
810
+ # That being said, you can use that feature to require a new login if
811
+ # their session times out. Similar to how financial sites work. Just set
812
+ # this option to true and if your record returns true for stale? then
813
+ # they will be required to log back in.
814
+ #
815
+ # Lastly, UserSession.find will still return an object if the session is
816
+ # stale, but you will not get a record. This allows you to determine if
817
+ # the user needs to log back in because their session went stale, or
818
+ # because they just aren't logged in. Just call
819
+ # current_user_session.stale? as your flag.
820
+ #
821
+ # * <tt>Default:</tt> false
822
+ # * <tt>Accepts:</tt> Boolean
823
+ def logout_on_timeout(value = nil)
824
+ rw_config(:logout_on_timeout, value, false)
825
+ end
826
+ alias logout_on_timeout= logout_on_timeout
827
+
828
+ # Every time a session is found the last_request_at field for that record is
829
+ # updated with the current time, if that field exists. If you want to limit how
830
+ # frequent that field is updated specify the threshold here. For example, if your
831
+ # user is making a request every 5 seconds, and you feel this is too frequent, and
832
+ # feel a minute is a good threshold. Set this to 1.minute. Once a minute has
833
+ # passed in between requests the field will be updated.
834
+ #
835
+ # * <tt>Default:</tt> 0
836
+ # * <tt>Accepts:</tt> integer representing time in seconds
837
+ def last_request_at_threshold(value = nil)
838
+ rw_config(:last_request_at_threshold, value, 0)
839
+ end
840
+ alias last_request_at_threshold= last_request_at_threshold
841
+
842
+ # Works exactly like cookie_key, but for params. So a user can login via
843
+ # params just like a cookie or a session. Your URL would look like:
844
+ #
845
+ # http://www.domain.com?user_credentials=my_single_access_key
846
+ #
847
+ # You can change the "user_credentials" key above with this
848
+ # configuration option. Keep in mind, just like cookie_key, if you
849
+ # supply an id the id will be appended to the front. Check out
850
+ # cookie_key for more details. Also checkout the "Single Access /
851
+ # Private Feeds Access" section in the README.
852
+ #
853
+ # * <tt>Default:</tt> cookie_key
854
+ # * <tt>Accepts:</tt> String
855
+ def params_key(value = nil)
856
+ rw_config(:params_key, value, cookie_key)
857
+ end
858
+ alias params_key= params_key
859
+
860
+ # Works exactly like login_field, but for the password instead. Returns
861
+ # :password if a login_field exists.
862
+ #
863
+ # * <tt>Default:</tt> :password
864
+ # * <tt>Accepts:</tt> Symbol or String
865
+ def password_field(value = nil)
866
+ rw_config(:password_field, value, login_field && :password)
867
+ end
868
+ alias password_field= password_field
869
+
870
+ # Authlogic tries to validate the credentials passed to it. One part of
871
+ # validation is actually finding the user and making sure it exists.
872
+ # What method it uses the do this is up to you.
873
+ #
874
+ # ```
875
+ # # user_session.rb
876
+ # record_selection_method :find_by_email
877
+ # ```
878
+ #
879
+ # This is the recommended way to find the user by email address.
880
+ # The resulting query will be `User.find_by_email(send(login_field))`.
881
+ # (`login_field` will fall back to `email_field` if there's no `login`
882
+ # or `username` column).
883
+ #
884
+ # In your User model you can make that method do anything you want,
885
+ # giving you complete control of how users are found by the UserSession.
886
+ #
887
+ # Let's take an example: You want to allow users to login by username or
888
+ # email. Set this to the name of the class method that does this in the
889
+ # User model. Let's call it "find_by_username_or_email"
890
+ #
891
+ # ```
892
+ # class User < ActiveRecord::Base
893
+ # def self.find_by_username_or_email(login)
894
+ # find_by_username(login) || find_by_email(login)
895
+ # end
896
+ # end
897
+ # ```
898
+ #
899
+ # Now just specify the name of this method for this configuration option
900
+ # and you are all set. You can do anything you want here. Maybe you
901
+ # allow users to have multiple logins and you want to search a has_many
902
+ # relationship, etc. The sky is the limit.
903
+ #
904
+ # * <tt>Default:</tt> "find_by_smart_case_login_field"
905
+ # * <tt>Accepts:</tt> Symbol or String
906
+ def record_selection_method(value = nil)
907
+ rw_config(:record_selection_method, value, "find_by_smart_case_login_field")
908
+ end
909
+ alias record_selection_method= record_selection_method
910
+
911
+ # Whether or not to request HTTP authentication
912
+ #
913
+ # If set to true and no HTTP authentication credentials are sent with
914
+ # the request, the Rails controller method
915
+ # authenticate_or_request_with_http_basic will be used and a '401
916
+ # Authorization Required' header will be sent with the response. In
917
+ # most cases, this will cause the classic HTTP authentication popup to
918
+ # appear in the users browser.
919
+ #
920
+ # If set to false, the Rails controller method
921
+ # authenticate_with_http_basic is used and no 401 header is sent.
922
+ #
923
+ # Note: This parameter has no effect unless allow_http_basic_auth is
924
+ # true
925
+ #
926
+ # * <tt>Default:</tt> false
927
+ # * <tt>Accepts:</tt> Boolean
928
+ def request_http_basic_auth(value = nil)
929
+ rw_config(:request_http_basic_auth, value, false)
930
+ end
931
+ alias request_http_basic_auth= request_http_basic_auth
932
+
933
+ # If sessions should be remembered by default or not.
934
+ #
935
+ # * <tt>Default:</tt> false
936
+ # * <tt>Accepts:</tt> Boolean
937
+ def remember_me(value = nil)
938
+ rw_config(:remember_me, value, false)
939
+ end
940
+ alias remember_me= remember_me
941
+
942
+ # The length of time until the cookie expires.
943
+ #
944
+ # * <tt>Default:</tt> 3.months
945
+ # * <tt>Accepts:</tt> Integer, length of time in seconds, such as 60 or 3.months
946
+ def remember_me_for(value = nil)
947
+ rw_config(:remember_me_for, value, 3.months)
948
+ end
949
+ alias remember_me_for= remember_me_for
950
+
951
+ # Should the cookie be prevented from being send along with cross-site
952
+ # requests?
953
+ #
954
+ # * <tt>Default:</tt> nil
955
+ # * <tt>Accepts:</tt> String, one of nil, 'Lax' or 'Strict'
956
+ def same_site(value = nil)
957
+ unless VALID_SAME_SITE_VALUES.include?(value)
958
+ msg = "Invalid same_site value: #{value}. Valid: #{VALID_SAME_SITE_VALUES.inspect}"
959
+ raise ArgumentError, msg
960
+ end
961
+ rw_config(:same_site, value)
962
+ end
963
+ alias same_site= same_site
964
+
965
+ # The current scope set, should be used in the block passed to with_scope.
966
+ def scope
967
+ RequestStore.store[:authlogic_scope]
968
+ end
969
+
970
+ # Should the cookie be set as secure? If true, the cookie will only be sent over
971
+ # SSL connections
972
+ #
973
+ # * <tt>Default:</tt> true
974
+ # * <tt>Accepts:</tt> Boolean
975
+ def secure(value = nil)
976
+ rw_config(:secure, value, true)
977
+ end
978
+ alias secure= secure
979
+
980
+ # Should the Rack session ID be reset after authentication, to protect
981
+ # against Session Fixation attacks?
982
+ #
983
+ # * <tt>Default:</tt> true
984
+ # * <tt>Accepts:</tt> Boolean
985
+ def session_fixation_defense(value = nil)
986
+ rw_config(:session_fixation_defense, value, true)
987
+ end
988
+ alias session_fixation_defense= session_fixation_defense
989
+
990
+ # Should the cookie be signed? If the controller adapter supports it, this is a
991
+ # measure against cookie tampering.
992
+ def sign_cookie(value = nil)
993
+ if value && controller && !controller.cookies.respond_to?(:signed)
994
+ raise "Signed cookies not supported with #{controller.class}!"
995
+ end
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
+ if value && sign_cookie
1007
+ raise "It is recommended to use encrypt_cookie instead of sign_cookie. " \
1008
+ "You may not enable both options."
1009
+ end
1010
+ rw_config(:encrypt_cookie, value, false)
1011
+ end
1012
+ alias encrypt_cookie= encrypt_cookie
1013
+
1014
+ # Works exactly like cookie_key, but for sessions. See cookie_key for more info.
1015
+ #
1016
+ # * <tt>Default:</tt> cookie_key
1017
+ # * <tt>Accepts:</tt> Symbol or String
1018
+ def session_key(value = nil)
1019
+ rw_config(:session_key, value, cookie_key)
1020
+ end
1021
+ alias session_key= session_key
1022
+
1023
+ # Authentication is allowed via a single access token, but maybe this is
1024
+ # something you don't want for your application as a whole. Maybe this
1025
+ # is something you only want for specific request types. Specify a list
1026
+ # of allowed request types and single access authentication will only be
1027
+ # allowed for the ones you specify.
1028
+ #
1029
+ # * <tt>Default:</tt> ["application/rss+xml", "application/atom+xml"]
1030
+ # * <tt>Accepts:</tt> String of a request type, or :all or :any to
1031
+ # allow single access authentication for any and all request types
1032
+ def single_access_allowed_request_types(value = nil)
1033
+ rw_config(
1034
+ :single_access_allowed_request_types,
1035
+ value,
1036
+ ["application/rss+xml", "application/atom+xml"]
1037
+ )
1038
+ end
1039
+ alias single_access_allowed_request_types= single_access_allowed_request_types
1040
+
1041
+ # The name of the method in your model used to verify the password. This
1042
+ # should be an instance method. It should also be prepared to accept a
1043
+ # raw password and a crytped password.
1044
+ #
1045
+ # * <tt>Default:</tt> "valid_password?" defined in acts_as_authentic/password.rb
1046
+ # * <tt>Accepts:</tt> Symbol or String
1047
+ def verify_password_method(value = nil)
1048
+ rw_config(:verify_password_method, value, "valid_password?")
1049
+ end
1050
+ alias verify_password_method= verify_password_method
1051
+
1052
+ # What with_scopes focuses on is scoping the query when finding the
1053
+ # object and the name of the cookie / session. It works very similar to
1054
+ # ActiveRecord::Base#with_scopes. It accepts a hash with any of the
1055
+ # following options:
1056
+ #
1057
+ # * <tt>find_options:</tt> any options you can pass into ActiveRecord::Base.find.
1058
+ # This is used when trying to find the record.
1059
+ # * <tt>id:</tt> The id of the session, this gets merged with the real id. For
1060
+ # information ids see the id method.
1061
+ #
1062
+ # Here is how you use it:
1063
+ #
1064
+ # ```
1065
+ # UserSession.with_scope(find_options: User.where(account_id: 2), id: "account_2") do
1066
+ # UserSession.find
1067
+ # end
1068
+ # ```
1069
+ #
1070
+ # Essentially what the above does is scope the searching of the object
1071
+ # with the sql you provided. So instead of:
1072
+ #
1073
+ # ```
1074
+ # User.where("login = 'ben'").first
1075
+ # ```
1076
+ #
1077
+ # it would effectively be:
1078
+ #
1079
+ # ```
1080
+ # User.where("login = 'ben' and account_id = 2").first
1081
+ # ```
1082
+ #
1083
+ # You will also notice the :id option. This works just like the id
1084
+ # method. It scopes your cookies. So the name of your cookie will be:
1085
+ #
1086
+ # account_2_user_credentials
1087
+ #
1088
+ # instead of:
1089
+ #
1090
+ # user_credentials
1091
+ #
1092
+ # What is also nifty about scoping with an :id is that it merges your
1093
+ # id's. So if you do:
1094
+ #
1095
+ # UserSession.with_scope(
1096
+ # find_options: { conditions: "account_id = 2"},
1097
+ # id: "account_2"
1098
+ # ) do
1099
+ # session = UserSession.new
1100
+ # session.id = :secure
1101
+ # end
1102
+ #
1103
+ # The name of your cookies will be:
1104
+ #
1105
+ # secure_account_2_user_credentials
1106
+ def with_scope(options = {})
1107
+ raise ArgumentError, "You must provide a block" unless block_given?
1108
+ self.scope = options
1109
+ result = yield
1110
+ self.scope = nil
1111
+ result
1112
+ end
1113
+ end
1114
+
1115
+ # Constructor
1116
+ # ===========
1117
+
1118
+ def initialize(*args)
1119
+ @id = nil
1120
+ self.scope = self.class.scope
1121
+ define_record_alias_method
1122
+ raise Activation::NotActivatedError unless self.class.activated?
1123
+ unless self.class.configured_password_methods
1124
+ configure_password_methods
1125
+ self.class.configured_password_methods = true
1126
+ end
1127
+ instance_variable_set("@#{password_field}", nil)
1128
+ self.credentials = args
1129
+ end
1130
+
1131
+ # Public instance methods
1132
+ # =======================
1133
+
1134
+ # You should use this as a place holder for any records that you find
1135
+ # during validation. The main reason for this is to allow other modules to
1136
+ # use it if needed. Take the failed_login_count feature, it needs this in
1137
+ # order to increase the failed login count.
1138
+ def attempted_record
1139
+ @attempted_record
1140
+ end
1141
+
1142
+ # See attempted_record
1143
+ def attempted_record=(value)
1144
+ value = priority_record if value == priority_record # See notes in `.find`
1145
+ @attempted_record = value
1146
+ end
1147
+
1148
+ # Returns true when the consecutive_failed_logins_limit has been
1149
+ # exceeded and is being temporarily banned. Notice the word temporary,
1150
+ # the user will not be permanently banned unless you choose to do so
1151
+ # with configuration. By default they will be banned for 2 hours. During
1152
+ # that 2 hour period this method will return true.
1153
+ def being_brute_force_protected?
1154
+ exceeded_failed_logins_limit? &&
1155
+ (
1156
+ failed_login_ban_for <= 0 ||
1157
+ attempted_record.respond_to?(:updated_at) &&
1158
+ attempted_record.updated_at >= failed_login_ban_for.seconds.ago
1159
+ )
1160
+ end
1161
+
1162
+ # The credentials you passed to create your session, in a redacted format
1163
+ # intended for output (debugging, logging). See credentials= for more
1164
+ # info.
1165
+ #
1166
+ # @api private
1167
+ def credentials
1168
+ if authenticating_with_unauthorized_record?
1169
+ { unauthorized_record: "<protected>" }
1170
+ elsif authenticating_with_password?
1171
+ {
1172
+ login_field.to_sym => send(login_field),
1173
+ password_field.to_sym => "<protected>"
1174
+ }
1175
+ else
1176
+ {}
1177
+ end
1178
+ end
1179
+
1180
+ # Set your credentials before you save your session. There are many
1181
+ # method signatures.
1182
+ #
1183
+ # ```
1184
+ # # A hash of credentials is most common
1185
+ # session.credentials = { login: "foo", password: "bar", remember_me: true }
1186
+ #
1187
+ # # You must pass an actual Hash, `ActionController::Parameters` is
1188
+ # # specifically not allowed.
1189
+ #
1190
+ # # You can pass an array of objects:
1191
+ # session.credentials = [my_user_object, true]
1192
+ #
1193
+ # # If you need to set an id (see `#id`) pass it last.
1194
+ # session.credentials = [
1195
+ # {:login => "foo", :password => "bar", :remember_me => true},
1196
+ # :my_id
1197
+ # ]
1198
+ # session.credentials = [my_user_object, true, :my_id]
1199
+ #
1200
+ # The `id` is something that you control yourself, it should never be
1201
+ # set from a hash or a form.
1202
+ #
1203
+ # # Finally, there's priority_record
1204
+ # [{ priority_record: my_object }, :my_id]
1205
+ # ```
1206
+ #
1207
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
1208
+ def credentials=(value)
1209
+ normalized = Array.wrap(value)
1210
+ if normalized.first.class.name == "ActionController::Parameters"
1211
+ raise TypeError, E_AC_PARAMETERS
1212
+ end
1213
+
1214
+ # Allows you to set the remember_me option when passing credentials.
1215
+ values = value.is_a?(Array) ? value : [value]
1216
+ case values.first
1217
+ when Hash
1218
+ if values.first.with_indifferent_access.key?(:remember_me)
1219
+ self.remember_me = values.first.with_indifferent_access[:remember_me]
1220
+ end
1221
+ else
1222
+ r = values.find { |val| val.is_a?(TrueClass) || val.is_a?(FalseClass) }
1223
+ self.remember_me = r unless r.nil?
1224
+ end
1225
+
1226
+ # Accepts the login_field / password_field credentials combination in
1227
+ # hash form.
1228
+ #
1229
+ # You must pass an actual Hash, `ActionController::Parameters` is
1230
+ # specifically not allowed.
1231
+ values = Array.wrap(value)
1232
+ if values.first.is_a?(Hash)
1233
+ sliced = values
1234
+ .first
1235
+ .with_indifferent_access
1236
+ .slice(login_field, password_field)
1237
+ sliced.each do |field, val|
1238
+ next if val.blank?
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
+ @httponly = self.class.httponly
1287
+ end
1288
+
1289
+ # Accepts a boolean as to whether the cookie should be marked as
1290
+ # httponly. If true, the cookie will not be accessible from javascript
1291
+ def httponly=(value)
1292
+ @httponly = value
1293
+ end
1294
+
1295
+ # See httponly
1296
+ def httponly?
1297
+ httponly == true || httponly == "true" || httponly == "1"
1298
+ end
1299
+
1300
+ # Allows you to set a unique identifier for your session, so that you can
1301
+ # have more than 1 session at a time.
1302
+ #
1303
+ # For example, you may want to have simultaneous private and public
1304
+ # sessions. Or, a normal user session and a "secure" user session. The
1305
+ # secure user session would be created only when they want to modify their
1306
+ # billing information, or other sensitive information.
1307
+ #
1308
+ # You can set the id during initialization (see initialize for more
1309
+ # information), or as an attribute:
1310
+ #
1311
+ # session.id = :my_id
1312
+ #
1313
+ # Set your id before you save your session.
1314
+ #
1315
+ # Lastly, to retrieve your session with the id, use the `.find` method.
1316
+ def id
1317
+ @id
1318
+ end
1319
+
1320
+ def inspect
1321
+ format(
1322
+ "#<%s: %s>",
1323
+ self.class.name,
1324
+ credentials.blank? ? "no credentials provided" : credentials.inspect
1325
+ )
1326
+ end
1327
+
1328
+ def invalid_password?
1329
+ invalid_password == true
1330
+ end
1331
+
1332
+ # Don't use this yourself, this is to just trick some of the helpers
1333
+ # since this is the method it calls.
1334
+ def new_record?
1335
+ new_session?
1336
+ end
1337
+
1338
+ # Returns true if the session is new, meaning no action has been taken
1339
+ # on it and a successful save has not taken place.
1340
+ def new_session?
1341
+ new_session != false
1342
+ end
1343
+
1344
+ def persisted?
1345
+ !(new_record? || destroyed?)
1346
+ end
1347
+
1348
+ # Returns boolean indicating if the session is being persisted or not,
1349
+ # meaning the user does not have to explicitly log in in order to be
1350
+ # logged in.
1351
+ #
1352
+ # If the session has no associated record, it will try to find a record
1353
+ # and persist the session.
1354
+ #
1355
+ # This is the method that the class level method find uses to ultimately
1356
+ # persist the session.
1357
+ def persisting?
1358
+ return true unless record.nil?
1359
+ self.attempted_record = nil
1360
+ self.remember_me = cookie_credentials&.remember_me?
1361
+ run_callbacks :before_persisting
1362
+ run_callbacks :persist
1363
+ ensure_authentication_attempted
1364
+ if errors.empty? && !attempted_record.nil?
1365
+ self.record = attempted_record
1366
+ run_callbacks :after_persisting
1367
+ save_record
1368
+ self.new_session = false
1369
+ true
1370
+ else
1371
+ false
1372
+ end
1373
+ end
1374
+
1375
+ def save_record(alternate_record = nil)
1376
+ r = alternate_record || record
1377
+ if r != priority_record
1378
+ if r&.has_changes_to_save? && !r.readonly?
1379
+ r.save_without_session_maintenance(validate: false)
1380
+ end
1381
+ end
1382
+ end
1383
+
1384
+ # Tells you if the record is stale or not. Meaning the record has timed
1385
+ # out. This will only return true if you set logout_on_timeout to true
1386
+ # in your configuration. Basically how a bank website works. If you
1387
+ # aren't active over a certain period of time your session becomes stale
1388
+ # and requires you to log back in.
1389
+ def stale?
1390
+ if remember_me?
1391
+ remember_me_expired?
1392
+ else
1393
+ !stale_record.nil? || (logout_on_timeout? && record && record.logged_out?)
1394
+ end
1395
+ end
1396
+
1397
+ # Is the cookie going to expire after the session is over, or will it stick around?
1398
+ def remember_me
1399
+ return @remember_me if defined?(@remember_me)
1400
+ @remember_me = self.class.remember_me
1401
+ end
1402
+
1403
+ # Accepts a boolean as a flag to remember the session or not. Basically
1404
+ # to expire the cookie at the end of the session or keep it for
1405
+ # "remember_me_until".
1406
+ def remember_me=(value)
1407
+ @remember_me = value
1408
+ end
1409
+
1410
+ # See remember_me
1411
+ def remember_me?
1412
+ remember_me == true || remember_me == "true" || remember_me == "1"
1413
+ end
1414
+
1415
+ # Has the cookie expired due to current time being greater than remember_me_until.
1416
+ def remember_me_expired?
1417
+ return unless remember_me?
1418
+ cookie_credentials.remember_me_until < ::Time.now
1419
+ end
1420
+
1421
+ # How long to remember the user if remember_me is true. This is based on the class
1422
+ # level configuration: remember_me_for
1423
+ def remember_me_for
1424
+ return unless remember_me?
1425
+ self.class.remember_me_for
1426
+ end
1427
+
1428
+ # When to expire the cookie. See remember_me_for configuration option to change
1429
+ # this.
1430
+ def remember_me_until
1431
+ return unless remember_me?
1432
+ remember_me_for.from_now
1433
+ end
1434
+
1435
+ # After you have specified all of the details for your session you can
1436
+ # try to save it. This will run validation checks and find the
1437
+ # associated record, if all validation passes. If validation does not
1438
+ # pass, the save will fail and the errors will be stored in the errors
1439
+ # object.
1440
+ def save
1441
+ result = nil
1442
+ if valid?
1443
+ self.record = attempted_record
1444
+
1445
+ run_callbacks :before_save
1446
+ run_callbacks(new_session? ? :before_create : :before_update)
1447
+ run_callbacks(new_session? ? :after_create : :after_update)
1448
+ run_callbacks :after_save
1449
+
1450
+ save_record
1451
+ self.new_session = false
1452
+ result = true
1453
+ else
1454
+ result = false
1455
+ end
1456
+
1457
+ yield result if block_given?
1458
+ result
1459
+ end
1460
+
1461
+ # Same as save but raises an exception of validation errors when
1462
+ # validation fails
1463
+ def save!
1464
+ result = save
1465
+ raise Existence::SessionInvalidError, self unless result
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
+ @secure = self.class.secure
1473
+ end
1474
+
1475
+ # Accepts a boolean as to whether the cookie should be marked as secure. If true
1476
+ # the cookie will only ever be sent over an SSL connection.
1477
+ def secure=(value)
1478
+ @secure = value
1479
+ end
1480
+
1481
+ # See secure
1482
+ def secure?
1483
+ secure == true || secure == "true" || secure == "1"
1484
+ end
1485
+
1486
+ # If the cookie should be marked as SameSite with 'Lax' or 'Strict' flag.
1487
+ def same_site
1488
+ return @same_site if defined?(@same_site)
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
+ @sign_cookie = self.class.sign_cookie
1505
+ end
1506
+
1507
+ # Accepts a boolean as to whether the cookie should be signed. If true
1508
+ # the cookie will be saved and verified using a signature.
1509
+ def sign_cookie=(value)
1510
+ @sign_cookie = value
1511
+ end
1512
+
1513
+ # See sign_cookie
1514
+ def sign_cookie?
1515
+ sign_cookie == true || sign_cookie == "true" || sign_cookie == "1"
1516
+ end
1517
+
1518
+ # If the cookie should be encrypted
1519
+ def encrypt_cookie
1520
+ return @encrypt_cookie if defined?(@encrypt_cookie)
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
+ def encrypt_cookie=(value)
1527
+ @encrypt_cookie = value
1528
+ end
1529
+
1530
+ # See encrypt_cookie
1531
+ def encrypt_cookie?
1532
+ encrypt_cookie == true || encrypt_cookie == "true" || encrypt_cookie == "1"
1533
+ end
1534
+
1535
+ # The scope of the current object
1536
+ def scope
1537
+ @scope ||= {}
1538
+ end
1539
+
1540
+ def to_key
1541
+ new_record? ? nil : record.to_key
1542
+ end
1543
+
1544
+ # For rails >= 3.0
1545
+ def to_model
1546
+ self
1547
+ end
1548
+
1549
+ # Determines if the information you provided for authentication is valid
1550
+ # or not. If there is a problem with the information provided errors will
1551
+ # be added to the errors object and this method will return false.
1552
+ #
1553
+ # @api public
1554
+ def valid?
1555
+ errors.clear
1556
+ self.attempted_record = nil
1557
+ run_the_before_validation_callbacks
1558
+
1559
+ # Run the `validate` callbacks, eg. `validate_by_password`.
1560
+ # This is when `attempted_record` is set.
1561
+ run_callbacks(:validate)
1562
+
1563
+ ensure_authentication_attempted
1564
+ if errors.empty?
1565
+ run_the_after_validation_callbacks
1566
+ end
1567
+ save_record(attempted_record)
1568
+ errors.empty?
1569
+ end
1570
+
1571
+ # Private class methods
1572
+ # =====================
1573
+
1574
+ class << self
1575
+ private
1576
+
1577
+ def scope=(value)
1578
+ RequestStore.store[:authlogic_scope] = value
1579
+ end
1580
+ end
1581
+
1582
+ # Private instance methods
1583
+ # ========================
1584
+
1585
+ private
1586
+
1587
+ def add_general_credentials_error
1588
+ error_message =
1589
+ if self.class.generalize_credentials_error_messages.is_a? String
1590
+ self.class.generalize_credentials_error_messages
1591
+ else
1592
+ "#{login_field.to_s.humanize}/Password combination is not valid"
1593
+ end
1594
+ errors.add(
1595
+ :base,
1596
+ I18n.t("error_messages.general_credentials_error", default: error_message)
1597
+ )
1598
+ end
1599
+
1600
+ def add_invalid_password_error
1601
+ if generalize_credentials_error_messages?
1602
+ add_general_credentials_error
1603
+ else
1604
+ errors.add(
1605
+ password_field,
1606
+ I18n.t("error_messages.password_invalid", default: "is not valid")
1607
+ )
1608
+ end
1609
+ end
1610
+
1611
+ def add_login_not_found_error
1612
+ if generalize_credentials_error_messages?
1613
+ add_general_credentials_error
1614
+ else
1615
+ errors.add(
1616
+ login_field,
1617
+ I18n.t("error_messages.login_not_found", default: "is not valid")
1618
+ )
1619
+ end
1620
+ end
1621
+
1622
+ def allow_http_basic_auth?
1623
+ self.class.allow_http_basic_auth == true
1624
+ end
1625
+
1626
+ def authenticating_with_password?
1627
+ login_field && (!send(login_field).nil? || !send("protected_#{password_field}").nil?)
1628
+ end
1629
+
1630
+ def authenticating_with_unauthorized_record?
1631
+ !unauthorized_record.nil?
1632
+ end
1633
+
1634
+ # Used for things like cookie_key, session_key, etc.
1635
+ # Examples:
1636
+ # - user_credentials
1637
+ # - ziggity_zack_user_credentials
1638
+ # - ziggity_zack is an "id"
1639
+ # - see persistence_token_test.rb
1640
+ def build_key(last_part)
1641
+ [id, scope[:id], last_part].compact.join("_")
1642
+ end
1643
+
1644
+ def clear_failed_login_count
1645
+ if record.respond_to?(:failed_login_count)
1646
+ record.failed_login_count = 0
1647
+ end
1648
+ end
1649
+
1650
+ def consecutive_failed_logins_limit
1651
+ self.class.consecutive_failed_logins_limit
1652
+ end
1653
+
1654
+ def controller
1655
+ self.class.controller
1656
+ end
1657
+
1658
+ def cookie_key
1659
+ build_key(self.class.cookie_key)
1660
+ end
1661
+
1662
+ # Look in the `cookie_jar`, find the cookie that contains authlogic
1663
+ # credentials (`cookie_key`).
1664
+ #
1665
+ # @api private
1666
+ # @return ::Authlogic::CookieCredentials or if no cookie is found, nil
1667
+ def cookie_credentials
1668
+ return unless cookie_enabled?
1669
+
1670
+ cookie_value = cookie_jar[cookie_key]
1671
+ unless cookie_value.nil?
1672
+ ::Authlogic::CookieCredentials.parse(cookie_value)
1673
+ end
1674
+ end
1675
+
1676
+ def cookie_enabled?
1677
+ !controller.cookies.nil?
1678
+ end
1679
+
1680
+ def cookie_jar
1681
+ if self.class.encrypt_cookie
1682
+ controller.cookies.encrypted
1683
+ elsif self.class.sign_cookie
1684
+ controller.cookies.signed
1685
+ else
1686
+ controller.cookies
1687
+ end
1688
+ end
1689
+
1690
+ def configure_password_methods
1691
+ define_login_field_methods
1692
+ define_password_field_methods
1693
+ end
1694
+
1695
+ # Assign a new controller-session ID, to defend against Session Fixation.
1696
+ # https://guides.rubyonrails.org/v6.0/security.html#session-fixation
1697
+ def renew_session_id
1698
+ return unless self.class.session_fixation_defense
1699
+ controller.renew_session_id
1700
+ end
1701
+
1702
+ def define_login_field_methods
1703
+ return unless login_field
1704
+ self.class.send(:attr_writer, login_field) unless respond_to?("#{login_field}=")
1705
+ self.class.send(:attr_reader, login_field) unless respond_to?(login_field)
1706
+ end
1707
+
1708
+ # @api private
1709
+ def define_password_field_methods
1710
+ return unless password_field
1711
+ define_password_field_writer_method
1712
+ define_password_field_reader_methods
1713
+ end
1714
+
1715
+ # The password should not be accessible publicly. This way forms using
1716
+ # form_for don't fill the password with the attempted password. To prevent
1717
+ # this we just create this method that is private.
1718
+ #
1719
+ # @api private
1720
+ def define_password_field_reader_methods
1721
+ unless respond_to?(password_field)
1722
+ # Deliberate no-op method, see rationale above.
1723
+ self.class.send(:define_method, password_field) {}
1724
+ end
1725
+ self.class.class_eval(
1726
+ <<-EOS, __FILE__, __LINE__ + 1
1727
+ private
1728
+ def protected_#{password_field}
1729
+ @#{password_field}
1730
+ end
1731
+ EOS
1732
+ )
1733
+ end
1734
+
1735
+ def define_password_field_writer_method
1736
+ unless respond_to?("#{password_field}=")
1737
+ self.class.send(:attr_writer, password_field)
1738
+ end
1739
+ end
1740
+
1741
+ # Creating an alias method for the "record" method based on the klass
1742
+ # name, so that we can do:
1743
+ #
1744
+ # session.user
1745
+ #
1746
+ # instead of:
1747
+ #
1748
+ # session.record
1749
+ #
1750
+ # @api private
1751
+ def define_record_alias_method
1752
+ noun = klass_name.demodulize.underscore.to_sym
1753
+ return if respond_to?(noun)
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
+ if stale?
1767
+ self.stale_record = record
1768
+ self.record = nil
1769
+ end
1770
+ end
1771
+
1772
+ def ensure_authentication_attempted
1773
+ if errors.empty? && attempted_record.nil?
1774
+ errors.add(
1775
+ :base,
1776
+ I18n.t(
1777
+ "error_messages.no_authentication_details",
1778
+ default: "You did not provide any details for authentication."
1779
+ )
1780
+ )
1781
+ end
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 > 0 &&
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
+ ::Authlogic::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
+ if invalid_password? && attempted_record.respond_to?(:failed_login_count)
1844
+ attempted_record.failed_login_count ||= 0
1845
+ attempted_record.failed_login_count += 1
1846
+ end
1847
+ end
1848
+
1849
+ def increment_login_count
1850
+ if record.respond_to?(:login_count)
1851
+ record.login_count = (record.login_count.blank? ? 1 : record.login_count + 1)
1852
+ end
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
+ if !params_credentials || !klass.column_names.include?("single_access_token")
1881
+ return false
1882
+ end
1883
+ if controller.responds_to_single_access_allowed?
1884
+ return controller.single_access_allowed?
1885
+ end
1886
+ params_enabled_by_allowed_request_types?
1887
+ end
1888
+
1889
+ def params_enabled_by_allowed_request_types?
1890
+ case single_access_allowed_request_types
1891
+ when Array
1892
+ single_access_allowed_request_types.include?(controller.request_content_type) ||
1893
+ single_access_allowed_request_types.include?(:all)
1894
+ else
1895
+ %i[all any].include?(single_access_allowed_request_types)
1896
+ end
1897
+ end
1898
+
1899
+ def params_key
1900
+ build_key(self.class.params_key)
1901
+ end
1902
+
1903
+ def password_field
1904
+ self.class.password_field
1905
+ end
1906
+
1907
+ # Tries to validate the session from information in the cookie
1908
+ def persist_by_cookie
1909
+ creds = cookie_credentials
1910
+ if creds&.persistence_token.present?
1911
+ record = search_for_record("find_by_#{klass.primary_key}", creds.record_id)
1912
+ if record && record.persistence_token == creds.persistence_token
1913
+ self.unauthorized_record = record
1914
+ end
1915
+ valid?
1916
+ else
1917
+ false
1918
+ end
1919
+ end
1920
+
1921
+ def persist_by_params
1922
+ return false unless params_enabled?
1923
+ self.unauthorized_record = search_for_record(
1924
+ "find_by_single_access_token",
1925
+ params_credentials
1926
+ )
1927
+ self.single_access = valid?
1928
+ end
1929
+
1930
+ def persist_by_http_auth
1931
+ login_proc = http_auth_login_proc
1932
+
1933
+ if self.class.request_http_basic_auth
1934
+ controller.authenticate_or_request_with_http_basic(
1935
+ self.class.http_basic_auth_realm,
1936
+ &login_proc
1937
+ )
1938
+ else
1939
+ controller.authenticate_with_http_basic(&login_proc)
1940
+ end
1941
+
1942
+ false
1943
+ end
1944
+
1945
+ def persist_by_http_auth?
1946
+ allow_http_basic_auth? && login_field && password_field
1947
+ end
1948
+
1949
+ # Tries to validate the session from information in the session
1950
+ def persist_by_session
1951
+ persistence_token, record_id = session_credentials
1952
+ if !persistence_token.nil?
1953
+ record = persist_by_session_search(persistence_token, record_id)
1954
+ if record && record.persistence_token == persistence_token
1955
+ self.unauthorized_record = record
1956
+ end
1957
+ valid?
1958
+ else
1959
+ false
1960
+ end
1961
+ end
1962
+
1963
+ # Allow finding by persistence token, because when records are created
1964
+ # the session is maintained in a before_save, when there is no id.
1965
+ # This is done for performance reasons and to save on queries.
1966
+ def persist_by_session_search(persistence_token, record_id)
1967
+ if record_id.nil?
1968
+ search_for_record("find_by_persistence_token", persistence_token.to_s)
1969
+ else
1970
+ search_for_record("find_by_#{klass.primary_key}", record_id.to_s)
1971
+ end
1972
+ end
1973
+
1974
+ def reset_stale_state
1975
+ self.stale_record = nil
1976
+ end
1977
+
1978
+ def reset_perishable_token!
1979
+ if record.respond_to?(:reset_perishable_token) &&
1980
+ !record.disable_perishable_token_maintenance?
1981
+ record.reset_perishable_token
1982
+ end
1983
+ end
1984
+
1985
+ # @api private
1986
+ def required_magic_states_for(record)
1987
+ %i[active approved confirmed].select { |state|
1988
+ record.respond_to?("#{state}?")
1989
+ }
1990
+ end
1991
+
1992
+ def reset_failed_login_count?
1993
+ exceeded_failed_logins_limit? && !being_brute_force_protected?
1994
+ end
1995
+
1996
+ def reset_failed_login_count
1997
+ attempted_record.failed_login_count = 0
1998
+ end
1999
+
2000
+ # @api private
2001
+ def run_the_after_validation_callbacks
2002
+ run_callbacks(new_session? ? :after_validation_on_create : :after_validation_on_update)
2003
+ run_callbacks(:after_validation)
2004
+ end
2005
+
2006
+ # @api private
2007
+ def run_the_before_validation_callbacks
2008
+ run_callbacks(:before_validation)
2009
+ run_callbacks(new_session? ? :before_validation_on_create : :before_validation_on_update)
2010
+ end
2011
+
2012
+ # `args[0]` is the name of a model method, like
2013
+ # `find_by_single_access_token` or `find_by_smart_case_login_field`.
2014
+ def search_for_record(*args)
2015
+ search_scope.scoping do
2016
+ klass.send(*args)
2017
+ end
2018
+ end
2019
+
2020
+ # Returns an AR relation representing the scope of the search. The
2021
+ # relation is either provided directly by, or defined by
2022
+ # `find_options`.
2023
+ def search_scope
2024
+ if scope[:find_options].is_a?(ActiveRecord::Relation)
2025
+ scope[:find_options]
2026
+ else
2027
+ conditions = scope[:find_options] && scope[:find_options][:conditions] || {}
2028
+ klass.send(:where, conditions)
2029
+ end
2030
+ end
2031
+
2032
+ # @api private
2033
+ def set_last_request_at
2034
+ current_time = Time.current
2035
+ MagicColumn::AssignsLastRequestAt
2036
+ .new(current_time, record, controller, last_request_at_threshold)
2037
+ .assign
2038
+ end
2039
+
2040
+ def single_access?
2041
+ single_access == true
2042
+ end
2043
+
2044
+ def single_access_allowed_request_types
2045
+ self.class.single_access_allowed_request_types
2046
+ end
2047
+
2048
+ def save_cookie
2049
+ cookie_jar[cookie_key] = generate_cookie_for_saving
2050
+ end
2051
+
2052
+ # @api private
2053
+ # @return [String] - Examples:
2054
+ # - user_credentials_id
2055
+ # - ziggity_zack_user_credentials_id
2056
+ # - ziggity_zack is an "id", see `#id`
2057
+ # - see persistence_token_test.rb
2058
+ def session_compound_key
2059
+ "#{session_key}_#{klass.primary_key}"
2060
+ end
2061
+
2062
+ def session_credentials
2063
+ [
2064
+ controller.session[session_key],
2065
+ controller.session[session_compound_key]
2066
+ ].collect { |i| i.nil? ? i : i.to_s }.compact
2067
+ end
2068
+
2069
+ # @return [String] - Examples:
2070
+ # - user_credentials
2071
+ # - ziggity_zack_user_credentials
2072
+ # - ziggity_zack is an "id", see `#id`
2073
+ # - see persistence_token_test.rb
2074
+ def session_key
2075
+ build_key(self.class.session_key)
2076
+ end
2077
+
2078
+ def update_info
2079
+ increment_login_count
2080
+ clear_failed_login_count
2081
+ update_login_timestamps
2082
+ update_login_ip_addresses
2083
+ end
2084
+
2085
+ def update_login_ip_addresses
2086
+ if record.respond_to?(:current_login_ip)
2087
+ record.last_login_ip = record.current_login_ip if record.respond_to?(:last_login_ip)
2088
+ record.current_login_ip = controller.request.ip
2089
+ end
2090
+ end
2091
+
2092
+ def update_login_timestamps
2093
+ if record.respond_to?(:current_login_at)
2094
+ record.last_login_at = record.current_login_at if record.respond_to?(:last_login_at)
2095
+ record.current_login_at = Time.current
2096
+ end
2097
+ end
2098
+
2099
+ def update_session
2100
+ update_session_set_persistence_token
2101
+ update_session_set_primary_key
2102
+ end
2103
+
2104
+ # Updates the session, setting the primary key (usually `id`) of the
2105
+ # record.
2106
+ #
2107
+ # @api private
2108
+ def update_session_set_primary_key
2109
+ compound_key = session_compound_key
2110
+ controller.session[compound_key] = record && record.send(record.class.primary_key)
2111
+ end
2112
+
2113
+ # Updates the session, setting the `persistence_token` of the record.
2114
+ #
2115
+ # @api private
2116
+ def update_session_set_persistence_token
2117
+ controller.session[session_key] = record && record.persistence_token
2118
+ end
2119
+
2120
+ # In keeping with the metaphor of ActiveRecord, verification of the
2121
+ # password is referred to as a "validation".
2122
+ def validate_by_password
2123
+ self.invalid_password = false
2124
+ validate_by_password__blank_fields
2125
+ return if errors.count > 0
2126
+ self.attempted_record = search_for_record(
2127
+ self.class.record_selection_method,
2128
+ send(login_field)
2129
+ )
2130
+ if attempted_record.blank?
2131
+ add_login_not_found_error
2132
+ return
2133
+ end
2134
+ validate_by_password__invalid_password
2135
+ end
2136
+
2137
+ def validate_by_password__blank_fields
2138
+ if send(login_field).blank?
2139
+ errors.add(
2140
+ login_field,
2141
+ I18n.t("error_messages.login_blank", default: "cannot be blank")
2142
+ )
2143
+ end
2144
+ if send("protected_#{password_field}").blank?
2145
+ errors.add(
2146
+ password_field,
2147
+ I18n.t("error_messages.password_blank", default: "cannot be blank")
2148
+ )
2149
+ end
2150
+ end
2151
+
2152
+ # Verify the password, usually using `valid_password?` in
2153
+ # `acts_as_authentic/password.rb`. If it cannot be verified, we
2154
+ # refer to it as "invalid".
2155
+ def validate_by_password__invalid_password
2156
+ unless attempted_record.send(
2157
+ verify_password_method,
2158
+ send("protected_#{password_field}")
2159
+ )
2160
+ self.invalid_password = true
2161
+ add_invalid_password_error
2162
+ end
2163
+ end
2164
+
2165
+ def validate_by_unauthorized_record
2166
+ self.attempted_record = unauthorized_record
2167
+ end
2168
+
2169
+ def validate_magic_states
2170
+ return true if attempted_record.nil?
2171
+ required_magic_states_for(attempted_record).each do |required_status|
2172
+ next if attempted_record.send("#{required_status}?")
2173
+ errors.add(
2174
+ :base,
2175
+ I18n.t(
2176
+ "error_messages.not_#{required_status}",
2177
+ default: "Your account is not #{required_status}"
2178
+ )
2179
+ )
2180
+ return false
2181
+ end
2182
+ true
2183
+ end
2184
+
2185
+ def validate_failed_logins
2186
+ # Clear all other error messages, as they are irrelevant at this point and can
2187
+ # only provide additional information that is not needed
2188
+ errors.clear
2189
+ duration = failed_login_ban_for == 0 ? "" : " temporarily"
2190
+ errors.add(
2191
+ :base,
2192
+ I18n.t(
2193
+ "error_messages.consecutive_failed_logins_limit_exceeded",
2194
+ default: format(
2195
+ "Consecutive failed logins limit exceeded, account has been%s disabled.",
2196
+ duration
2197
+ )
2198
+ )
2199
+ )
2200
+ end
2201
+
2202
+ def verify_password_method
2203
+ self.class.verify_password_method
2204
+ end
47
2205
  end
48
2206
  end
49
2207
  end