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