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,362 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Authentication
|
4
|
+
module Logic
|
5
|
+
module ActsAsAuthentic
|
6
|
+
# This module has a lot of neat functionality. It is responsible for encrypting your
|
7
|
+
# password, salting it, and verifying it. It can also help you transition to a new
|
8
|
+
# encryption algorithm. See the Config sub module for configuration options.
|
9
|
+
module Password
|
10
|
+
def self.included(klass)
|
11
|
+
klass.class_eval do
|
12
|
+
extend Config
|
13
|
+
add_acts_as_authentic_module(Callbacks)
|
14
|
+
add_acts_as_authentic_module(Methods)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# All configuration for the password aspect of acts_as_authentic.
|
19
|
+
module Config
|
20
|
+
# The name of the crypted_password field in the database.
|
21
|
+
#
|
22
|
+
# * <tt>Default:</tt> :crypted_password, :encrypted_password, :password_hash, or :pw_hash
|
23
|
+
# * <tt>Accepts:</tt> Symbol
|
24
|
+
def crypted_password_field(value = nil)
|
25
|
+
rw_config(
|
26
|
+
:crypted_password_field,
|
27
|
+
value,
|
28
|
+
first_column_to_exist(
|
29
|
+
nil,
|
30
|
+
:crypted_password,
|
31
|
+
:encrypted_password,
|
32
|
+
:password_hash,
|
33
|
+
:pw_hash
|
34
|
+
)
|
35
|
+
)
|
36
|
+
end
|
37
|
+
alias crypted_password_field= crypted_password_field
|
38
|
+
|
39
|
+
# The name of the password_salt field in the database.
|
40
|
+
#
|
41
|
+
# * <tt>Default:</tt> :password_salt, :pw_salt, :salt, nil if none exist
|
42
|
+
# * <tt>Accepts:</tt> Symbol
|
43
|
+
def password_salt_field(value = nil)
|
44
|
+
rw_config(
|
45
|
+
:password_salt_field,
|
46
|
+
value,
|
47
|
+
first_column_to_exist(nil, :password_salt, :pw_salt, :salt)
|
48
|
+
)
|
49
|
+
end
|
50
|
+
alias password_salt_field= password_salt_field
|
51
|
+
|
52
|
+
# Whether or not to require a password confirmation. If you don't want your users
|
53
|
+
# to confirm their password just set this to false.
|
54
|
+
#
|
55
|
+
# * <tt>Default:</tt> true
|
56
|
+
# * <tt>Accepts:</tt> Boolean
|
57
|
+
def require_password_confirmation(value = nil)
|
58
|
+
rw_config(:require_password_confirmation, value, true)
|
59
|
+
end
|
60
|
+
alias require_password_confirmation= require_password_confirmation
|
61
|
+
|
62
|
+
# By default passwords are required when a record is new or the crypted_password
|
63
|
+
# is blank, but if both of these things are met a password is not required. In
|
64
|
+
# this case, blank passwords are ignored.
|
65
|
+
#
|
66
|
+
# Think about a profile page, where the user can edit all of their information,
|
67
|
+
# including changing their password. If they do not want to change their password
|
68
|
+
# they just leave the fields blank. This will try to set the password to a blank
|
69
|
+
# value, in which case is incorrect behavior. As such, Authentication::Logic ignores this. But
|
70
|
+
# let's say you have a completely separate page for resetting passwords, you might
|
71
|
+
# not want to ignore blank passwords. If this is the case for you, then just set
|
72
|
+
# this value to false.
|
73
|
+
#
|
74
|
+
# * <tt>Default:</tt> true
|
75
|
+
# * <tt>Accepts:</tt> Boolean
|
76
|
+
def ignore_blank_passwords(value = nil)
|
77
|
+
rw_config(:ignore_blank_passwords, value, true)
|
78
|
+
end
|
79
|
+
alias ignore_blank_passwords= ignore_blank_passwords
|
80
|
+
|
81
|
+
# When calling valid_password?("some pass") do you want to check that password
|
82
|
+
# against what's in that object or whats in the database. Take this example:
|
83
|
+
#
|
84
|
+
# u = User.first
|
85
|
+
# u.password = "new pass"
|
86
|
+
# u.valid_password?("old pass")
|
87
|
+
#
|
88
|
+
# Should the last line above return true or false? The record hasn't been saved
|
89
|
+
# yet, so most would assume true. Other would assume false. So I let you decide by
|
90
|
+
# giving you this option.
|
91
|
+
#
|
92
|
+
# * <tt>Default:</tt> true
|
93
|
+
# * <tt>Accepts:</tt> Boolean
|
94
|
+
def check_passwords_against_database(value = nil)
|
95
|
+
rw_config(:check_passwords_against_database, value, true)
|
96
|
+
end
|
97
|
+
alias check_passwords_against_database= check_passwords_against_database
|
98
|
+
|
99
|
+
# The class you want to use to encrypt and verify your encrypted
|
100
|
+
# passwords. See the Authentication::Logic::CryptoProviders module for more info on
|
101
|
+
# the available methods and how to create your own.
|
102
|
+
#
|
103
|
+
# The family of adaptive hash functions (BCrypt, SCrypt, PBKDF2) is the
|
104
|
+
# best choice for password storage today. We recommend SCrypt. Other
|
105
|
+
# one-way functions like SHA512 are inferior, but widely used.
|
106
|
+
# Reversible functions like AES256 are the worst choice, and we no
|
107
|
+
# longer support them.
|
108
|
+
#
|
109
|
+
# You can use the `transition_from_crypto_providers` option to gradually
|
110
|
+
# transition to a better crypto provider without causing your users any
|
111
|
+
# pain.
|
112
|
+
#
|
113
|
+
# * <tt>Default:</tt> There is no longer a default value. Prior to
|
114
|
+
# Authentication::Logic 6, the default was `CryptoProviders::SCrypt`. If you try
|
115
|
+
# to read this config option before setting it, it will raise a
|
116
|
+
# `NilCryptoProvider` error. See that error's message for further
|
117
|
+
# details, and rationale for this change.
|
118
|
+
# * <tt>Accepts:</tt> Class
|
119
|
+
def crypto_provider
|
120
|
+
acts_as_authentic_config[:crypto_provider].tap do |provider|
|
121
|
+
raise NilCryptoProvider if provider.nil?
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def crypto_provider=(value)
|
126
|
+
raise NilCryptoProvider if value.nil?
|
127
|
+
|
128
|
+
CryptoProviders::Guidance.new(value).impart_wisdom
|
129
|
+
rw_config(:crypto_provider, value)
|
130
|
+
end
|
131
|
+
|
132
|
+
# Let's say you originally encrypted your passwords with Sha1. Sha1 is
|
133
|
+
# starting to join the party with MD5 and you want to switch to
|
134
|
+
# something stronger. No problem, just specify your new and improved
|
135
|
+
# algorithm with the crypt_provider option and then let Authentication::Logic know
|
136
|
+
# you are transitioning from Sha1 using this option. Authentication::Logic will take
|
137
|
+
# care of everything, including transitioning your users to the new
|
138
|
+
# algorithm. The next time a user logs in, they will be granted access
|
139
|
+
# using the old algorithm and their password will be resaved with the
|
140
|
+
# new algorithm. All new users will obviously use the new algorithm as
|
141
|
+
# well.
|
142
|
+
#
|
143
|
+
# Lastly, if you want to transition again, you can pass an array of
|
144
|
+
# crypto providers. So you can transition from as many algorithms as you
|
145
|
+
# want.
|
146
|
+
#
|
147
|
+
# * <tt>Default:</tt> nil
|
148
|
+
# * <tt>Accepts:</tt> Class or Array
|
149
|
+
def transition_from_crypto_providers(value = nil)
|
150
|
+
rw_config(
|
151
|
+
:transition_from_crypto_providers,
|
152
|
+
(!value.nil? && [value].flatten.compact) || value,
|
153
|
+
[]
|
154
|
+
)
|
155
|
+
end
|
156
|
+
alias transition_from_crypto_providers= transition_from_crypto_providers
|
157
|
+
end
|
158
|
+
|
159
|
+
# Callbacks / hooks to allow other modules to modify the behavior of this module.
|
160
|
+
module Callbacks
|
161
|
+
# Does the order of this array matter?
|
162
|
+
METHODS = %w[
|
163
|
+
password_set
|
164
|
+
password_verification
|
165
|
+
].freeze
|
166
|
+
|
167
|
+
def self.included(klass)
|
168
|
+
return if klass.crypted_password_field.nil?
|
169
|
+
|
170
|
+
klass.send :extend, ActiveModel::Callbacks
|
171
|
+
METHODS.each do |method|
|
172
|
+
klass.define_model_callbacks method, only: %i[before after]
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
# The methods related to the password field.
|
178
|
+
module Methods
|
179
|
+
def self.included(klass)
|
180
|
+
return if klass.crypted_password_field.nil?
|
181
|
+
|
182
|
+
klass.class_eval do
|
183
|
+
include InstanceMethods
|
184
|
+
after_save :reset_password_changed
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
# :nodoc:
|
189
|
+
module InstanceMethods
|
190
|
+
# The password
|
191
|
+
def password
|
192
|
+
return nil unless defined?(@password)
|
193
|
+
|
194
|
+
@password
|
195
|
+
end
|
196
|
+
|
197
|
+
# This is a virtual method. Once a password is passed to it, it will
|
198
|
+
# create new password salt as well as encrypt the password.
|
199
|
+
def password=(pass)
|
200
|
+
return if ignore_blank_passwords? && pass.blank?
|
201
|
+
|
202
|
+
run_callbacks :password_set do
|
203
|
+
@password = pass
|
204
|
+
send("#{password_salt_field}=", Authentication::Logic::Random.friendly_token) if password_salt_field
|
205
|
+
send(
|
206
|
+
"#{crypted_password_field}=",
|
207
|
+
crypto_provider.encrypt(*encrypt_arguments(@password, false))
|
208
|
+
)
|
209
|
+
@password_changed = true
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
# Accepts a raw password to determine if it is the correct password.
|
214
|
+
#
|
215
|
+
# - attempted_password [String] - password entered by user
|
216
|
+
# - check_against_database [boolean] - Should we check the password
|
217
|
+
# against the value in the database or the value in the object?
|
218
|
+
# Default taken from config option check_passwords_against_database.
|
219
|
+
# See config method for more information.
|
220
|
+
def valid_password?(
|
221
|
+
attempted_password,
|
222
|
+
check_against_database = check_passwords_against_database?
|
223
|
+
)
|
224
|
+
crypted = crypted_password_to_validate_against(check_against_database)
|
225
|
+
return false if attempted_password.blank? || crypted.blank?
|
226
|
+
|
227
|
+
run_callbacks :password_verification do
|
228
|
+
crypto_providers.each_with_index.any? do |encryptor, index|
|
229
|
+
if encryptor_matches?(
|
230
|
+
crypted,
|
231
|
+
encryptor,
|
232
|
+
attempted_password,
|
233
|
+
check_against_database
|
234
|
+
)
|
235
|
+
if transition_password?(index, encryptor, check_against_database)
|
236
|
+
transition_password(attempted_password)
|
237
|
+
end
|
238
|
+
true
|
239
|
+
else
|
240
|
+
false
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
# Resets the password to a random friendly token.
|
247
|
+
def reset_password
|
248
|
+
friendly_token = Authentication::Logic::Random.friendly_token
|
249
|
+
self.password = friendly_token
|
250
|
+
self.password_confirmation = friendly_token if self.class.require_password_confirmation
|
251
|
+
end
|
252
|
+
alias randomize_password reset_password
|
253
|
+
|
254
|
+
# Resets the password to a random friendly token and then saves the record.
|
255
|
+
def reset_password!
|
256
|
+
reset_password
|
257
|
+
save_without_session_maintenance(validate: false)
|
258
|
+
end
|
259
|
+
alias randomize_password! reset_password!
|
260
|
+
|
261
|
+
private
|
262
|
+
|
263
|
+
def crypted_password_to_validate_against(check_against_database)
|
264
|
+
if check_against_database && send("will_save_change_to_#{crypted_password_field}?")
|
265
|
+
send("#{crypted_password_field}_in_database")
|
266
|
+
else
|
267
|
+
send(crypted_password_field)
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
def check_passwords_against_database?
|
272
|
+
self.class.check_passwords_against_database == true
|
273
|
+
end
|
274
|
+
|
275
|
+
def crypto_providers
|
276
|
+
[crypto_provider] + transition_from_crypto_providers
|
277
|
+
end
|
278
|
+
|
279
|
+
# Returns an array of arguments to be passed to a crypto provider, either its
|
280
|
+
# `matches?` or its `encrypt` method.
|
281
|
+
def encrypt_arguments(raw_password, check_against_database)
|
282
|
+
salt = nil
|
283
|
+
if password_salt_field
|
284
|
+
salt =
|
285
|
+
if check_against_database && send("will_save_change_to_#{password_salt_field}?")
|
286
|
+
send("#{password_salt_field}_in_database")
|
287
|
+
else
|
288
|
+
send(password_salt_field)
|
289
|
+
end
|
290
|
+
end
|
291
|
+
[raw_password, salt].compact
|
292
|
+
end
|
293
|
+
|
294
|
+
# Given `encryptor`, does `attempted_password` match the `crypted` password?
|
295
|
+
def encryptor_matches?(crypted, encryptor, attempted_password, check_against_database)
|
296
|
+
encryptor_args = encrypt_arguments(attempted_password, check_against_database)
|
297
|
+
encryptor.matches?(crypted, *encryptor_args)
|
298
|
+
end
|
299
|
+
|
300
|
+
# Determines if we need to transition the password.
|
301
|
+
#
|
302
|
+
# - If the index > 0 then we are using a "transition from" crypto
|
303
|
+
# provider.
|
304
|
+
# - If the encryptor has a cost and the cost it outdated.
|
305
|
+
# - If we aren't using database values
|
306
|
+
# - If we are using database values, only if the password hasn't
|
307
|
+
# changed so we don't overwrite any changes
|
308
|
+
def transition_password?(index, encryptor, check_against_database)
|
309
|
+
(
|
310
|
+
index.positive? ||
|
311
|
+
(encryptor.respond_to?(:cost_matches?) &&
|
312
|
+
!encryptor.cost_matches?(send(crypted_password_field)))
|
313
|
+
) &&
|
314
|
+
(
|
315
|
+
!check_against_database ||
|
316
|
+
!send("will_save_change_to_#{crypted_password_field}?")
|
317
|
+
)
|
318
|
+
end
|
319
|
+
|
320
|
+
def transition_password(attempted_password)
|
321
|
+
self.password = attempted_password
|
322
|
+
save(validate: false)
|
323
|
+
end
|
324
|
+
|
325
|
+
def require_password?
|
326
|
+
# this is _not_ the activemodel changed? method, see below
|
327
|
+
new_record? || password_changed? || send(crypted_password_field).blank?
|
328
|
+
end
|
329
|
+
|
330
|
+
def ignore_blank_passwords?
|
331
|
+
self.class.ignore_blank_passwords == true
|
332
|
+
end
|
333
|
+
|
334
|
+
def password_changed?
|
335
|
+
defined?(@password_changed) && @password_changed == true
|
336
|
+
end
|
337
|
+
|
338
|
+
def reset_password_changed
|
339
|
+
@password_changed = nil
|
340
|
+
end
|
341
|
+
|
342
|
+
def crypted_password_field
|
343
|
+
self.class.crypted_password_field
|
344
|
+
end
|
345
|
+
|
346
|
+
def password_salt_field
|
347
|
+
self.class.password_salt_field
|
348
|
+
end
|
349
|
+
|
350
|
+
def crypto_provider
|
351
|
+
self.class.crypto_provider
|
352
|
+
end
|
353
|
+
|
354
|
+
def transition_from_crypto_providers
|
355
|
+
self.class.transition_from_crypto_providers
|
356
|
+
end
|
357
|
+
end
|
358
|
+
end
|
359
|
+
end
|
360
|
+
end
|
361
|
+
end
|
362
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Authentication
|
4
|
+
module Logic
|
5
|
+
module ActsAsAuthentic
|
6
|
+
# This provides a handy token that is "perishable", meaning the token is
|
7
|
+
# only good for a certain amount of time.
|
8
|
+
#
|
9
|
+
# This is useful for resetting password, confirming accounts, etc. Typically
|
10
|
+
# during these actions you send them this token in an email. Once they use
|
11
|
+
# the token and do what they need to do, that token should expire.
|
12
|
+
#
|
13
|
+
# Don't worry about maintaining the token, changing it, or expiring it
|
14
|
+
# yourself. Authentication::Logic does all of this for you. See the sub modules for all
|
15
|
+
# of the tools Authentication::Logic provides to you.
|
16
|
+
module PerishableToken
|
17
|
+
def self.included(klass)
|
18
|
+
klass.class_eval do
|
19
|
+
extend Config
|
20
|
+
add_acts_as_authentic_module(Methods)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Configure the perishable token.
|
25
|
+
module Config
|
26
|
+
# When using the find_using_perishable_token method the token can
|
27
|
+
# expire. If the token is expired, no record will be returned. Use this
|
28
|
+
# option to specify how long the token is valid for.
|
29
|
+
#
|
30
|
+
# * <tt>Default:</tt> 10.minutes
|
31
|
+
# * <tt>Accepts:</tt> Fixnum
|
32
|
+
def perishable_token_valid_for(value = nil)
|
33
|
+
rw_config(
|
34
|
+
:perishable_token_valid_for,
|
35
|
+
(!value.nil? && value.to_i) || value,
|
36
|
+
10.minutes.to_i
|
37
|
+
)
|
38
|
+
end
|
39
|
+
alias perishable_token_valid_for= perishable_token_valid_for
|
40
|
+
|
41
|
+
# Authentication::Logic tries to expire and change the perishable token as much as
|
42
|
+
# possible, without compromising its purpose. If you want to manage it
|
43
|
+
# yourself, set this to true.
|
44
|
+
#
|
45
|
+
# * <tt>Default:</tt> false
|
46
|
+
# * <tt>Accepts:</tt> Boolean
|
47
|
+
def disable_perishable_token_maintenance(value = nil)
|
48
|
+
rw_config(:disable_perishable_token_maintenance, value, false)
|
49
|
+
end
|
50
|
+
alias disable_perishable_token_maintenance= disable_perishable_token_maintenance
|
51
|
+
end
|
52
|
+
|
53
|
+
# All methods relating to the perishable token.
|
54
|
+
module Methods
|
55
|
+
def self.included(klass)
|
56
|
+
return unless klass.column_names.include?("perishable_token")
|
57
|
+
|
58
|
+
klass.class_eval do
|
59
|
+
extend ClassMethods
|
60
|
+
include InstanceMethods
|
61
|
+
|
62
|
+
validates_uniqueness_of :perishable_token, case_sensitive: true,
|
63
|
+
if: :will_save_change_to_perishable_token?
|
64
|
+
before_save :reset_perishable_token, unless: :disable_perishable_token_maintenance?
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# :nodoc:
|
69
|
+
module ClassMethods
|
70
|
+
# Use this method to find a record with a perishable token. This
|
71
|
+
# method does 2 things for you:
|
72
|
+
#
|
73
|
+
# 1. It ignores blank tokens
|
74
|
+
# 2. It enforces the perishable_token_valid_for configuration option.
|
75
|
+
#
|
76
|
+
# If you want to use a different timeout value, just pass it as the
|
77
|
+
# second parameter:
|
78
|
+
#
|
79
|
+
# User.find_using_perishable_token(token, 1.hour)
|
80
|
+
def find_using_perishable_token(token, age = perishable_token_valid_for)
|
81
|
+
return if token.blank?
|
82
|
+
|
83
|
+
age = age.to_i
|
84
|
+
|
85
|
+
conditions_sql = "perishable_token = ?"
|
86
|
+
conditions_subs = [token.to_s]
|
87
|
+
|
88
|
+
if column_names.include?("updated_at") && age.positive?
|
89
|
+
conditions_sql += " and updated_at > ?"
|
90
|
+
conditions_subs << age.seconds.ago
|
91
|
+
end
|
92
|
+
|
93
|
+
where(conditions_sql, *conditions_subs).first
|
94
|
+
end
|
95
|
+
|
96
|
+
# This method will raise ActiveRecord::NotFound if no record is found.
|
97
|
+
def find_using_perishable_token!(token, age = perishable_token_valid_for)
|
98
|
+
find_using_perishable_token(token, age) || raise(ActiveRecord::RecordNotFound)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# :nodoc:
|
103
|
+
module InstanceMethods
|
104
|
+
# Resets the perishable token to a random friendly token.
|
105
|
+
def reset_perishable_token
|
106
|
+
self.perishable_token = Random.friendly_token
|
107
|
+
end
|
108
|
+
|
109
|
+
# Same as reset_perishable_token, but then saves the record afterwards.
|
110
|
+
def reset_perishable_token!
|
111
|
+
reset_perishable_token
|
112
|
+
save_without_session_maintenance(validate: false)
|
113
|
+
end
|
114
|
+
|
115
|
+
# A convenience method based on the
|
116
|
+
# disable_perishable_token_maintenance configuration option.
|
117
|
+
def disable_perishable_token_maintenance?
|
118
|
+
self.class.disable_perishable_token_maintenance == true
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Authentication
|
4
|
+
module Logic
|
5
|
+
module ActsAsAuthentic
|
6
|
+
# Maintains the persistence token, the token responsible for persisting sessions. This token
|
7
|
+
# gets stored in the session and the cookie.
|
8
|
+
module PersistenceToken
|
9
|
+
def self.included(klass)
|
10
|
+
klass.class_eval do
|
11
|
+
add_acts_as_authentic_module(Methods)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# Methods for the persistence token.
|
16
|
+
module Methods
|
17
|
+
def self.included(klass)
|
18
|
+
klass.class_eval do
|
19
|
+
extend ClassMethods
|
20
|
+
include InstanceMethods
|
21
|
+
|
22
|
+
# If the table does not have a password column, then
|
23
|
+
# `after_password_set` etc. will not be defined. See
|
24
|
+
# `Authentication::Logic::ActsAsAuthentic::Password::Callbacks.included`
|
25
|
+
if respond_to?(:after_password_set) && respond_to?(:after_password_verification)
|
26
|
+
after_password_set :reset_persistence_token
|
27
|
+
after_password_verification :reset_persistence_token!, if: :reset_persistence_token?
|
28
|
+
end
|
29
|
+
|
30
|
+
validates_presence_of :persistence_token
|
31
|
+
validates_uniqueness_of :persistence_token, case_sensitive: true,
|
32
|
+
if: :will_save_change_to_persistence_token?
|
33
|
+
|
34
|
+
before_validation :reset_persistence_token, if: :reset_persistence_token?
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# :nodoc:
|
39
|
+
module ClassMethods
|
40
|
+
# Resets ALL persistence tokens in the database, which will require
|
41
|
+
# all users to re-authenticate.
|
42
|
+
def forget_all
|
43
|
+
# Paginate these to save on memory
|
44
|
+
find_each(batch_size: 50, &:forget!)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# :nodoc:
|
49
|
+
module InstanceMethods
|
50
|
+
# Resets the persistence_token field to a random hex value.
|
51
|
+
def reset_persistence_token
|
52
|
+
self.persistence_token = Authentication::Logic::Random.hex_token
|
53
|
+
end
|
54
|
+
|
55
|
+
# Same as reset_persistence_token, but then saves the record.
|
56
|
+
def reset_persistence_token!
|
57
|
+
reset_persistence_token
|
58
|
+
save_without_session_maintenance(validate: false)
|
59
|
+
end
|
60
|
+
alias forget! reset_persistence_token!
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def reset_persistence_token?
|
65
|
+
persistence_token.blank?
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Authentication
|
4
|
+
module Logic
|
5
|
+
module ActsAsAuthentic
|
6
|
+
module Queries
|
7
|
+
# @api private
|
8
|
+
class CaseSensitivity
|
9
|
+
E_UNABLE_TO_DETERMINE_SENSITIVITY = <<~EOS
|
10
|
+
Authentication::Logic was unable to determine what case-sensitivity to use when
|
11
|
+
searching for email/login. To specify a sensitivity, validate the
|
12
|
+
uniqueness of the email/login and use the `case_sensitive` option,
|
13
|
+
like this:
|
14
|
+
|
15
|
+
validates :email, uniqueness: { case_sensitive: false }
|
16
|
+
|
17
|
+
Authentication::Logic will now perform a case-insensitive query.
|
18
|
+
EOS
|
19
|
+
|
20
|
+
# @api private
|
21
|
+
def initialize(model_class, attribute)
|
22
|
+
@model_class = model_class
|
23
|
+
@attribute = attribute.to_sym
|
24
|
+
end
|
25
|
+
|
26
|
+
# @api private
|
27
|
+
def sensitive?
|
28
|
+
sensitive = uniqueness_validator_options[:case_sensitive]
|
29
|
+
if sensitive.nil?
|
30
|
+
::Kernel.warn(E_UNABLE_TO_DETERMINE_SENSITIVITY)
|
31
|
+
false
|
32
|
+
else
|
33
|
+
sensitive
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
# @api private
|
40
|
+
def uniqueness_validator
|
41
|
+
@model_class.validators.select do |v|
|
42
|
+
v.is_a?(::ActiveRecord::Validations::UniquenessValidator) &&
|
43
|
+
v.attributes == [@attribute]
|
44
|
+
end.first
|
45
|
+
end
|
46
|
+
|
47
|
+
# @api private
|
48
|
+
def uniqueness_validator_options
|
49
|
+
uniqueness_validator&.options || {}
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Authentication
|
4
|
+
module Logic
|
5
|
+
module ActsAsAuthentic
|
6
|
+
module Queries
|
7
|
+
# The query used by public-API method `find_by_smart_case_login_field`.
|
8
|
+
#
|
9
|
+
# We use the rails methods `case_insensitive_comparison` and
|
10
|
+
# `case_sensitive_comparison`. These methods nicely take into account
|
11
|
+
# MySQL collations. (Consider the case where a user *says* they want a
|
12
|
+
# case-sensitive uniqueness validation, but then they configure their
|
13
|
+
# database to have an insensitive collation. Rails will handle this for
|
14
|
+
# us, by downcasing, see
|
15
|
+
# `active_record/connection_adapters/abstract_mysql_adapter.rb`) So that's
|
16
|
+
# great! But, these methods are not part of rails' public API, so there
|
17
|
+
# are no docs. So, everything we know about how to use the methods
|
18
|
+
# correctly comes from mimicing what we find in
|
19
|
+
# `active_record/validations/uniqueness.rb`.
|
20
|
+
#
|
21
|
+
# @api private
|
22
|
+
class FindWithCase
|
23
|
+
# Dup ActiveRecord.gem_version before freezing, in case someone
|
24
|
+
# else wants to modify it. Freezing modifies an object in place.
|
25
|
+
# https://github.com/binarylogic/authlogic/pull/590
|
26
|
+
AR_GEM_VERSION = ::ActiveRecord.gem_version.dup.freeze
|
27
|
+
|
28
|
+
# @api private
|
29
|
+
def initialize(model_class, field, value, sensitive)
|
30
|
+
@model_class = model_class
|
31
|
+
@field = field.to_s
|
32
|
+
@value = value
|
33
|
+
@sensitive = sensitive
|
34
|
+
end
|
35
|
+
|
36
|
+
# @api private
|
37
|
+
def execute
|
38
|
+
@model_class.where(comparison).first
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
# @api private
|
44
|
+
# @return Arel::Nodes::Equality
|
45
|
+
def comparison
|
46
|
+
@sensitive ? sensitive_comparison : insensitive_comparison
|
47
|
+
end
|
48
|
+
|
49
|
+
# @api private
|
50
|
+
def insensitive_comparison
|
51
|
+
if Gem::Version.new("5.3") < AR_GEM_VERSION
|
52
|
+
@model_class.connection.case_insensitive_comparison(
|
53
|
+
@model_class.arel_table[@field], @value
|
54
|
+
)
|
55
|
+
else
|
56
|
+
@model_class.connection.case_insensitive_comparison(
|
57
|
+
@model_class.arel_table,
|
58
|
+
@field,
|
59
|
+
@model_class.columns_hash[@field],
|
60
|
+
@value
|
61
|
+
)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# @api private
|
66
|
+
def sensitive_comparison
|
67
|
+
bound_value = @model_class.predicate_builder.build_bind_attribute(@field, @value)
|
68
|
+
if Gem::Version.new("5.3") < AR_GEM_VERSION
|
69
|
+
@model_class.connection.case_sensitive_comparison(
|
70
|
+
@model_class.arel_table[@field], bound_value
|
71
|
+
)
|
72
|
+
else
|
73
|
+
@model_class.connection.case_sensitive_comparison(
|
74
|
+
@model_class.arel_table,
|
75
|
+
@field,
|
76
|
+
@model_class.columns_hash[@field],
|
77
|
+
bound_value
|
78
|
+
)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|