authlogic 4.5.0 → 6.4.2

Sign up to get free protection for your applications and to get access to all the features.
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