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