authlogic 5.1.0 → 6.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 599fa6150e2129f6366ed5cee0db158d6e7152a523174c1df72cd28b4877bf07
4
- data.tar.gz: 4bb06ac0d0d6d34ff7ced64861823c03920256df29961fd19ebd0ac820e04cd0
3
+ metadata.gz: f51d7731ff8fa94fae297857416a4f8e2d3ff6a36be6c95151100928f4bde5a1
4
+ data.tar.gz: 1cc8a04722128c14023fb31b2109ad31138e74b887a8e89bf4a7400b841cc708
5
5
  SHA512:
6
- metadata.gz: 35e71d8dc041a482b80127e36a0bd60296df1b472db130b481c843cccaa2969c946f936ca9e9faf2603de62c4dae1f20967fa0aa929e24429b7204b331761204
7
- data.tar.gz: dcf067170c35323dd8159bef69e2053af0b9c4fee96d5886d0b00404dd63b8eafc44601799c6f1a4261f9680b231fd5c4fa05eb64121eea9cb20ad8433df256f
6
+ metadata.gz: bb9684e8af955d1bff59dd4f3b4f803cea8e405f411d3faff59d47fd228520be810bc220a1d39434539b416b844ce901044c742f3dc4a4a6fa0fdcbdc4637f89
7
+ data.tar.gz: b0a2dba042bd7802dc33d837c14fa103f5e7f0f0eb28d1c26f66eb0d5dd8ce950aab37cddca7d75956b2afa9e4c6588b3790cd781157200c4b1fee7394d1232b
@@ -13,6 +13,7 @@ require "active_record"
13
13
  path = File.dirname(__FILE__) + "/authlogic/"
14
14
 
15
15
  [
16
+ "errors",
16
17
  "i18n",
17
18
  "random",
18
19
  "config",
@@ -31,8 +31,8 @@ module Authlogic
31
31
  #
32
32
  # See the various sub modules for the configuration they provide.
33
33
  def acts_as_authentic
34
- return unless db_setup?
35
34
  yield self if block_given?
35
+ return unless db_setup?
36
36
  acts_as_authentic_modules.each { |mod| include mod }
37
37
  end
38
38
 
@@ -65,12 +65,27 @@ module Authlogic
65
65
  self.acts_as_authentic_modules = modules
66
66
  end
67
67
 
68
+ # Some Authlogic modules requires a database connection with a existing
69
+ # users table by the moment when you call the `acts_as_authentic`
70
+ # method. If you try to call `acts_as_authentic` without a database
71
+ # connection, it will raise a `Authlogic::ModelSetupError`.
72
+ #
73
+ # If you rely on the User model before the database is setup correctly,
74
+ # set this field to false.
75
+ # * <tt>Default:</tt> false
76
+ # * <tt>Accepts:</tt> Boolean
77
+ def raise_on_model_setup_error(value = nil)
78
+ rw_config(:raise_on_model_setup_error, value, false)
79
+ end
80
+ alias raise_on_model_setup_error= raise_on_model_setup_error
81
+
68
82
  private
69
83
 
70
84
  def db_setup?
71
85
  column_names
72
86
  true
73
87
  rescue StandardError
88
+ raise ModelSetupError if raise_on_model_setup_error
74
89
  false
75
90
  end
76
91
 
@@ -102,20 +102,30 @@ module Authlogic
102
102
  # The family of adaptive hash functions (BCrypt, SCrypt, PBKDF2) is the
103
103
  # best choice for password storage today. We recommend SCrypt. Other
104
104
  # one-way functions like SHA512 are inferior, but widely used.
105
- # Reverisbile functions like AES256 are the worst choice, and we no
105
+ # Reversible functions like AES256 are the worst choice, and we no
106
106
  # longer support them.
107
107
  #
108
108
  # You can use the `transition_from_crypto_providers` option to gradually
109
109
  # transition to a better crypto provider without causing your users any
110
110
  # pain.
111
111
  #
112
- # * <tt>Default:</tt> CryptoProviders::SCrypt
112
+ # * <tt>Default:</tt> There is no longer a default value. Prior to
113
+ # Authlogic 6, the default was `CryptoProviders::SCrypt`. If you try
114
+ # to read this config option before setting it, it will raise a
115
+ # `NilCryptoProvider` error. See that error's message for further
116
+ # details, and rationale for this change.
113
117
  # * <tt>Accepts:</tt> Class
114
- def crypto_provider(value = nil)
118
+ def crypto_provider
119
+ acts_as_authentic_config[:crypto_provider].tap { |provider|
120
+ raise NilCryptoProvider if provider.nil?
121
+ }
122
+ end
123
+
124
+ def crypto_provider=(value)
125
+ raise NilCryptoProvider if value.nil?
115
126
  CryptoProviders::Guidance.new(value).impart_wisdom
116
- rw_config(:crypto_provider, value, CryptoProviders::SCrypt)
127
+ rw_config(:crypto_provider, value)
117
128
  end
118
- alias crypto_provider= crypto_provider
119
129
 
120
130
  # Let's say you originally encrypted your passwords with Sha1. Sha1 is
121
131
  # starting to join the party with MD5 and you want to switch to
@@ -93,9 +93,9 @@ module Authlogic
93
93
  end
94
94
 
95
95
  # Save the record and skip session maintenance all together.
96
- def save_without_session_maintenance(*args)
96
+ def save_without_session_maintenance(**options)
97
97
  self.skip_session_maintenance = true
98
- result = save(*args)
98
+ result = save(**options)
99
99
  self.skip_session_maintenance = false
100
100
  result
101
101
  end
@@ -176,7 +176,9 @@ module Authlogic
176
176
  end
177
177
 
178
178
  def log_in_after_password_change?
179
- will_save_change_to_persistence_token? && self.class.log_in_after_password_change
179
+ persisted? &&
180
+ will_save_change_to_persistence_token? &&
181
+ self.class.log_in_after_password_change
180
182
  end
181
183
  end
182
184
  end
@@ -14,7 +14,7 @@ module Authlogic
14
14
  # Returns a `ActionDispatch::Cookies::CookieJar`. See the AC guide
15
15
  # http://guides.rubyonrails.org/action_controller_overview.html#cookies
16
16
  def cookies
17
- controller.send(:cookies)
17
+ controller.respond_to?(:cookies, true) ? controller.send(:cookies) : nil
18
18
  end
19
19
 
20
20
  def cookie_domain
@@ -6,6 +6,9 @@ module Authlogic
6
6
  module CryptoProviders
7
7
  # A poor choice. There are known attacks against this algorithm.
8
8
  class MD5
9
+ # V2 hashes the digest bytes in repeated stretches instead of hex characters.
10
+ autoload :V2, File.join(__dir__, "md5", "v2")
11
+
9
12
  class << self
10
13
  attr_accessor :join_token
11
14
 
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest/md5"
4
+
5
+ module Authlogic
6
+ module CryptoProviders
7
+ class MD5
8
+ # A poor choice. There are known attacks against this algorithm.
9
+ class V2
10
+ class << self
11
+ attr_accessor :join_token
12
+
13
+ # The number of times to loop through the encryption.
14
+ def stretches
15
+ @stretches ||= 1
16
+ end
17
+ attr_writer :stretches
18
+
19
+ # Turns your raw password into a MD5 hash.
20
+ def encrypt(*tokens)
21
+ digest = tokens.flatten.join(join_token)
22
+ stretches.times { digest = Digest::MD5.digest(digest) }
23
+ digest.unpack1("H*")
24
+ end
25
+
26
+ # Does the crypted password match the tokens? Uses the same tokens that
27
+ # were used to encrypt.
28
+ def matches?(crypted, *tokens)
29
+ encrypt(*tokens) == crypted
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -6,6 +6,9 @@ module Authlogic
6
6
  module CryptoProviders
7
7
  # A poor choice. There are known attacks against this algorithm.
8
8
  class Sha1
9
+ # V2 hashes the digest bytes in repeated stretches instead of hex characters.
10
+ autoload :V2, File.join(__dir__, "sha1", "v2")
11
+
9
12
  class << self
10
13
  def join_token
11
14
  @join_token ||= "--"
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest/sha1"
4
+
5
+ module Authlogic
6
+ module CryptoProviders
7
+ class Sha1
8
+ # A poor choice. There are known attacks against this algorithm.
9
+ class V2
10
+ class << self
11
+ def join_token
12
+ @join_token ||= "--"
13
+ end
14
+ attr_writer :join_token
15
+
16
+ # The number of times to loop through the encryption.
17
+ def stretches
18
+ @stretches ||= 10
19
+ end
20
+ attr_writer :stretches
21
+
22
+ # Turns your raw password into a Sha1 hash.
23
+ def encrypt(*tokens)
24
+ tokens = tokens.flatten
25
+ digest = tokens.shift
26
+ stretches.times do
27
+ digest = Digest::SHA1.digest([digest, *tokens].join(join_token))
28
+ end
29
+ digest.unpack1("H*")
30
+ end
31
+
32
+ # Does the crypted password match the tokens? Uses the same tokens that
33
+ # were used to encrypt.
34
+ def matches?(crypted, *tokens)
35
+ encrypt(*tokens) == crypted
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -29,6 +29,9 @@ module Authlogic
29
29
  #
30
30
  # Uses the Sha256 hash algorithm to encrypt passwords.
31
31
  class Sha256
32
+ # V2 hashes the digest bytes in repeated stretches instead of hex characters.
33
+ autoload :V2, File.join(__dir__, "sha256", "v2")
34
+
32
35
  class << self
33
36
  attr_accessor :join_token
34
37
 
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest/sha2"
4
+
5
+ module Authlogic
6
+ # The acts_as_authentic method has a crypto_provider option. This allows you
7
+ # to use any type of encryption you like. Just create a class with a class
8
+ # level encrypt and matches? method. See example below.
9
+ #
10
+ # === Example
11
+ #
12
+ # class MyAwesomeEncryptionMethod
13
+ # def self.encrypt(*tokens)
14
+ # # the tokens passed will be an array of objects, what type of object
15
+ # # is irrelevant, just do what you need to do with them and return a
16
+ # # single encrypted string. for example, you will most likely join all
17
+ # # of the objects into a single string and then encrypt that string
18
+ # end
19
+ #
20
+ # def self.matches?(crypted, *tokens)
21
+ # # return true if the crypted string matches the tokens. Depending on
22
+ # # your algorithm you might decrypt the string then compare it to the
23
+ # # token, or you might encrypt the tokens and make sure it matches the
24
+ # # crypted string, its up to you.
25
+ # end
26
+ # end
27
+ module CryptoProviders
28
+ class Sha256
29
+ # = Sha256
30
+ #
31
+ # Uses the Sha256 hash algorithm to encrypt passwords.
32
+ class V2
33
+ class << self
34
+ attr_accessor :join_token
35
+
36
+ # The number of times to loop through the encryption.
37
+ def stretches
38
+ @stretches ||= 20
39
+ end
40
+ attr_writer :stretches
41
+
42
+ # Turns your raw password into a Sha256 hash.
43
+ def encrypt(*tokens)
44
+ digest = tokens.flatten.join(join_token)
45
+ stretches.times { digest = Digest::SHA256.digest(digest) }
46
+ digest.unpack1("H*")
47
+ end
48
+
49
+ # Does the crypted password match the tokens? Uses the same tokens that
50
+ # were used to encrypt.
51
+ def matches?(crypted, *tokens)
52
+ encrypt(*tokens) == crypted
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -8,6 +8,9 @@ module Authlogic
8
8
  # there are better choices. We recommend transitioning to a more secure,
9
9
  # adaptive hashing algorithm, like scrypt.
10
10
  class Sha512
11
+ # V2 hashes the digest bytes in repeated stretches instead of hex characters.
12
+ autoload :V2, File.join(__dir__, "sha512", "v2")
13
+
11
14
  class << self
12
15
  attr_accessor :join_token
13
16
 
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest/sha2"
4
+
5
+ module Authlogic
6
+ module CryptoProviders
7
+ class Sha512
8
+ # SHA-512 does not have any practical known attacks against it. However,
9
+ # there are better choices. We recommend transitioning to a more secure,
10
+ # adaptive hashing algorithm, like scrypt.
11
+ class V2
12
+ class << self
13
+ attr_accessor :join_token
14
+
15
+ # The number of times to loop through the encryption.
16
+ def stretches
17
+ @stretches ||= 20
18
+ end
19
+ attr_writer :stretches
20
+
21
+ # Turns your raw password into a Sha512 hash.
22
+ def encrypt(*tokens)
23
+ digest = tokens.flatten.join(join_token)
24
+ stretches.times do
25
+ digest = Digest::SHA512.digest(digest)
26
+ end
27
+ digest.unpack1("H*")
28
+ end
29
+
30
+ # Does the crypted password match the tokens? Uses the same tokens that
31
+ # were used to encrypt.
32
+ def matches?(crypted, *tokens)
33
+ encrypt(*tokens) == crypted
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Authlogic
4
+ # Parent class of all Authlogic errors.
5
+ class Error < StandardError
6
+ end
7
+
8
+ # :nodoc:
9
+ class InvalidCryptoProvider < Error
10
+ end
11
+
12
+ # :nodoc:
13
+ class NilCryptoProvider < InvalidCryptoProvider
14
+ def message
15
+ <<~EOS
16
+ In version 5, Authlogic used SCrypt by default. As of version 6, there
17
+ is no default. We still recommend SCrypt. If you previously relied on
18
+ this default, then, in your User model (or equivalent), please set the
19
+ following:
20
+
21
+ acts_as_authentic do |c|
22
+ c.crypto_provider = ::Authlogic::CryptoProviders::SCrypt
23
+ end
24
+
25
+ Furthermore, the authlogic gem no longer depends on the scrypt gem. In
26
+ your Gemfile, please add scrypt.
27
+
28
+ gem "scrypt", "~> 3.0"
29
+
30
+ We have made this change in Authlogic 6 so that users of other crypto
31
+ providers no longer need to install the scrypt gem.
32
+ EOS
33
+ end
34
+ end
35
+
36
+ # :nodoc:
37
+ class ModelSetupError < Error
38
+ def message
39
+ <<-EOS
40
+ You must establish a database connection and run the migrations before
41
+ using acts_as_authentic. If you need to load the User model before the
42
+ database is set up correctly, please set the following:
43
+
44
+ acts_as_authentic do |c|
45
+ c.raise_on_model_setup_error = false
46
+ end
47
+ EOS
48
+ end
49
+ end
50
+ end
@@ -8,7 +8,7 @@ module Authlogic
8
8
  # arguments, else returns +options[:default]+.
9
9
  def translate(key, options = {})
10
10
  if defined?(::I18n)
11
- ::I18n.translate key, options
11
+ ::I18n.translate key, **options
12
12
  else
13
13
  options[:default]
14
14
  end
@@ -198,7 +198,7 @@ module Authlogic
198
198
  # 2. Enable logging out on timeouts
199
199
  #
200
200
  # class UserSession < Authlogic::Session::Base
201
- # logout_on_timeout true # default if false
201
+ # logout_on_timeout true # default is false
202
202
  # end
203
203
  #
204
204
  # This will require a user to log back in if they are inactive for more than
@@ -351,6 +351,13 @@ module Authlogic
351
351
  - https://github.com/binarylogic/authlogic/pull/558
352
352
  - https://github.com/binarylogic/authlogic/pull/577
353
353
  EOS
354
+ E_DPR_FIND_BY_LOGIN_METHOD = <<~EOS.squish.freeze
355
+ find_by_login_method is deprecated in favor of record_selection_method,
356
+ to avoid confusion with ActiveRecord's "Dynamic Finders".
357
+ (https://guides.rubyonrails.org/v6.0/active_record_querying.html#dynamic-finders)
358
+ For example, rubocop-rails is confused by the deprecated method.
359
+ (https://github.com/rubocop-hq/rubocop-rails/blob/master/lib/rubocop/cop/rails/dynamic_find_by.rb)
360
+ EOS
354
361
  VALID_SAME_SITE_VALUES = [nil, "Lax", "Strict", "None"].freeze
355
362
 
356
363
  # Callbacks
@@ -415,10 +422,10 @@ module Authlogic
415
422
  before_save :set_last_request_at
416
423
 
417
424
  after_save :reset_perishable_token!
418
- after_save :save_cookie
425
+ after_save :save_cookie, if: :cookie_enabled?
419
426
  after_save :update_session
420
427
 
421
- after_destroy :destroy_cookie
428
+ after_destroy :destroy_cookie, if: :cookie_enabled?
422
429
  after_destroy :update_session
423
430
 
424
431
  # `validate` callbacks, in deliberate order. For example,
@@ -438,8 +445,7 @@ module Authlogic
438
445
 
439
446
  class << self
440
447
  attr_accessor(
441
- :configured_password_methods,
442
- :configured_klass_methods
448
+ :configured_password_methods
443
449
  )
444
450
  end
445
451
  attr_accessor(
@@ -472,14 +478,9 @@ module Authlogic
472
478
  !controller.nil?
473
479
  end
474
480
 
475
- # Do you want to allow your users to log in via HTTP basic auth?
476
- #
477
- # I recommend keeping this enabled. The only time I feel this should be
478
- # disabled is if you are not comfortable having your users provide their
479
- # raw username and password. Whatever the reason, you can disable it
480
- # here.
481
+ # Allow users to log in via HTTP basic authentication.
481
482
  #
482
- # * <tt>Default:</tt> true
483
+ # * <tt>Default:</tt> false
483
484
  # * <tt>Accepts:</tt> Boolean
484
485
  def allow_http_basic_auth(value = nil)
485
486
  rw_config(:allow_http_basic_auth, value, false)
@@ -669,35 +670,10 @@ module Authlogic
669
670
  end
670
671
  end
671
672
 
672
- # Authlogic tries to validate the credentials passed to it. One part of
673
- # validation is actually finding the user and making sure it exists.
674
- # What method it uses the do this is up to you.
675
- #
676
- # Let's say you have a UserSession that is authenticating a User. By
677
- # default UserSession will call User.find_by_login(login). You can
678
- # change what method UserSession calls by specifying it here. Then in
679
- # your User model you can make that method do anything you want, giving
680
- # you complete control of how users are found by the UserSession.
681
- #
682
- # Let's take an example: You want to allow users to login by username or
683
- # email. Set this to the name of the class method that does this in the
684
- # User model. Let's call it "find_by_username_or_email"
685
- #
686
- # class User < ActiveRecord::Base
687
- # def self.find_by_username_or_email(login)
688
- # find_by_username(login) || find_by_email(login)
689
- # end
690
- # end
691
- #
692
- # Now just specify the name of this method for this configuration option
693
- # and you are all set. You can do anything you want here. Maybe you
694
- # allow users to have multiple logins and you want to search a has_many
695
- # relationship, etc. The sky is the limit.
696
- #
697
- # * <tt>Default:</tt> "find_by_smart_case_login_field"
698
- # * <tt>Accepts:</tt> Symbol or String
673
+ # @deprecated in favor of record_selection_method
699
674
  def find_by_login_method(value = nil)
700
- rw_config(:find_by_login_method, value, "find_by_smart_case_login_field")
675
+ ::ActiveSupport::Deprecation.warn(E_DPR_FIND_BY_LOGIN_METHOD)
676
+ record_selection_method(value)
701
677
  end
702
678
  alias find_by_login_method= find_by_login_method
703
679
 
@@ -782,15 +758,23 @@ module Authlogic
782
758
  # example, the UserSession class will authenticate with the User class
783
759
  # unless you specify otherwise in your configuration. See
784
760
  # authenticate_with for information on how to change this value.
761
+ #
762
+ # @api public
785
763
  def klass
786
764
  @klass ||= klass_name ? klass_name.constantize : nil
787
765
  end
788
766
 
789
- # The string of the model name class guessed from the actual session class name.
767
+ # The model name, guessed from the session class name, e.g. "User",
768
+ # from "UserSession".
769
+ #
770
+ # TODO: This method can return nil. We should explore this. It seems
771
+ # likely to cause a NoMethodError later, so perhaps we should raise an
772
+ # error instead.
773
+ #
774
+ # @api private
790
775
  def klass_name
791
- return @klass_name if defined?(@klass_name)
792
- @klass_name = name.scan(/(.*)Session/)[0]
793
- @klass_name = klass_name ? klass_name[0] : nil
776
+ return @klass_name if instance_variable_defined?(:@klass_name)
777
+ @klass_name = name.scan(/(.*)Session/)[0]&.first
794
778
  end
795
779
 
796
780
  # The name of the method you want Authlogic to create for storing the
@@ -798,8 +782,8 @@ module Authlogic
798
782
  # Authlogic::Session, if you want it can be something completely
799
783
  # different than the field in your model. So if you wanted people to
800
784
  # login with a field called "login" and then find users by email this is
801
- # completely doable. See the find_by_login_method configuration option
802
- # for more details.
785
+ # completely doable. See the `record_selection_method` configuration
786
+ # option for details.
803
787
  #
804
788
  # * <tt>Default:</tt> klass.login_field || klass.email_field
805
789
  # * <tt>Accepts:</tt> Symbol or String
@@ -882,6 +866,47 @@ module Authlogic
882
866
  end
883
867
  alias password_field= password_field
884
868
 
869
+ # Authlogic tries to validate the credentials passed to it. One part of
870
+ # validation is actually finding the user and making sure it exists.
871
+ # What method it uses the do this is up to you.
872
+ #
873
+ # ```
874
+ # # user_session.rb
875
+ # record_selection_method :find_by_email
876
+ # ```
877
+ #
878
+ # This is the recommended way to find the user by email address.
879
+ # The resulting query will be `User.find_by_email(send(login_field))`.
880
+ # (`login_field` will fall back to `email_field` if there's no `login`
881
+ # or `username` column).
882
+ #
883
+ # In your User model you can make that method do anything you want,
884
+ # giving you complete control of how users are found by the UserSession.
885
+ #
886
+ # Let's take an example: You want to allow users to login by username or
887
+ # email. Set this to the name of the class method that does this in the
888
+ # User model. Let's call it "find_by_username_or_email"
889
+ #
890
+ # ```
891
+ # class User < ActiveRecord::Base
892
+ # def self.find_by_username_or_email(login)
893
+ # find_by_username(login) || find_by_email(login)
894
+ # end
895
+ # end
896
+ # ```
897
+ #
898
+ # Now just specify the name of this method for this configuration option
899
+ # and you are all set. You can do anything you want here. Maybe you
900
+ # allow users to have multiple logins and you want to search a has_many
901
+ # relationship, etc. The sky is the limit.
902
+ #
903
+ # * <tt>Default:</tt> "find_by_smart_case_login_field"
904
+ # * <tt>Accepts:</tt> Symbol or String
905
+ def record_selection_method(value = nil)
906
+ rw_config(:record_selection_method, value, "find_by_smart_case_login_field")
907
+ end
908
+ alias record_selection_method= record_selection_method
909
+
885
910
  # Whether or not to request HTTP authentication
886
911
  #
887
912
  # If set to true and no HTTP authentication credentials are sent with
@@ -954,13 +979,27 @@ module Authlogic
954
979
  # Should the cookie be signed? If the controller adapter supports it, this is a
955
980
  # measure against cookie tampering.
956
981
  def sign_cookie(value = nil)
957
- if value && !controller.cookies.respond_to?(:signed)
982
+ if value && controller && !controller.cookies.respond_to?(:signed)
958
983
  raise "Signed cookies not supported with #{controller.class}!"
959
984
  end
960
985
  rw_config(:sign_cookie, value, false)
961
986
  end
962
987
  alias sign_cookie= sign_cookie
963
988
 
989
+ # Should the cookie be encrypted? If the controller adapter supports it, this is a
990
+ # measure to hide the contents of the cookie (e.g. persistence_token)
991
+ def encrypt_cookie(value = nil)
992
+ if value && controller && !controller.cookies.respond_to?(:encrypted)
993
+ raise "Encrypted cookies not supported with #{controller.class}!"
994
+ end
995
+ if value && sign_cookie
996
+ raise "It is recommended to use encrypt_cookie instead of sign_cookie. " \
997
+ "You may not enable both options."
998
+ end
999
+ rw_config(:encrypt_cookie, value, false)
1000
+ end
1001
+ alias encrypt_cookie= encrypt_cookie
1002
+
964
1003
  # Works exactly like cookie_key, but for sessions. See cookie_key for more info.
965
1004
  #
966
1005
  # * <tt>Default:</tt> cookie_key
@@ -1065,24 +1104,10 @@ module Authlogic
1065
1104
  # Constructor
1066
1105
  # ===========
1067
1106
 
1068
- # rubocop:disable Metrics/AbcSize
1069
1107
  def initialize(*args)
1070
1108
  @id = nil
1071
1109
  self.scope = self.class.scope
1072
-
1073
- # Creating an alias method for the "record" method based on the klass
1074
- # name, so that we can do:
1075
- #
1076
- # session.user
1077
- #
1078
- # instead of:
1079
- #
1080
- # session.record
1081
- unless self.class.configured_klass_methods
1082
- self.class.send(:alias_method, klass_name.demodulize.underscore.to_sym, :record)
1083
- self.class.configured_klass_methods = true
1084
- end
1085
-
1110
+ define_record_alias_method
1086
1111
  raise Activation::NotActivatedError unless self.class.activated?
1087
1112
  unless self.class.configured_password_methods
1088
1113
  configure_password_methods
@@ -1091,7 +1116,6 @@ module Authlogic
1091
1116
  instance_variable_set("@#{password_field}", nil)
1092
1117
  self.credentials = args
1093
1118
  end
1094
- # rubocop:enable Metrics/AbcSize
1095
1119
 
1096
1120
  # Public instance methods
1097
1121
  # =======================
@@ -1480,6 +1504,23 @@ module Authlogic
1480
1504
  sign_cookie == true || sign_cookie == "true" || sign_cookie == "1"
1481
1505
  end
1482
1506
 
1507
+ # If the cookie should be encrypted
1508
+ def encrypt_cookie
1509
+ return @encrypt_cookie if defined?(@encrypt_cookie)
1510
+ @encrypt_cookie = self.class.encrypt_cookie
1511
+ end
1512
+
1513
+ # Accepts a boolean as to whether the cookie should be encrypted. If true
1514
+ # the cookie will be saved in an encrypted state.
1515
+ def encrypt_cookie=(value)
1516
+ @encrypt_cookie = value
1517
+ end
1518
+
1519
+ # See encrypt_cookie
1520
+ def encrypt_cookie?
1521
+ encrypt_cookie == true || encrypt_cookie == "true" || encrypt_cookie == "1"
1522
+ end
1523
+
1483
1524
  # The scope of the current object
1484
1525
  def scope
1485
1526
  @scope ||= {}
@@ -1497,24 +1538,21 @@ module Authlogic
1497
1538
  # Determines if the information you provided for authentication is valid
1498
1539
  # or not. If there is a problem with the information provided errors will
1499
1540
  # be added to the errors object and this method will return false.
1541
+ #
1542
+ # @api public
1500
1543
  def valid?
1501
1544
  errors.clear
1502
1545
  self.attempted_record = nil
1503
-
1504
- run_callbacks(:before_validation)
1505
- run_callbacks(new_session? ? :before_validation_on_create : :before_validation_on_update)
1546
+ run_the_before_validation_callbacks
1506
1547
 
1507
1548
  # Run the `validate` callbacks, eg. `validate_by_password`.
1508
1549
  # This is when `attempted_record` is set.
1509
1550
  run_callbacks(:validate)
1510
1551
 
1511
1552
  ensure_authentication_attempted
1512
-
1513
1553
  if errors.empty?
1514
- run_callbacks(new_session? ? :after_validation_on_create : :after_validation_on_update)
1515
- run_callbacks(:after_validation)
1554
+ run_the_after_validation_callbacks
1516
1555
  end
1517
-
1518
1556
  save_record(attempted_record)
1519
1557
  errors.empty?
1520
1558
  end
@@ -1616,14 +1654,22 @@ module Authlogic
1616
1654
  # @api private
1617
1655
  # @return ::Authlogic::CookieCredentials or if no cookie is found, nil
1618
1656
  def cookie_credentials
1657
+ return unless cookie_enabled?
1658
+
1619
1659
  cookie_value = cookie_jar[cookie_key]
1620
1660
  unless cookie_value.nil?
1621
1661
  ::Authlogic::CookieCredentials.parse(cookie_value)
1622
1662
  end
1623
1663
  end
1624
1664
 
1665
+ def cookie_enabled?
1666
+ !controller.cookies.nil?
1667
+ end
1668
+
1625
1669
  def cookie_jar
1626
- if self.class.sign_cookie
1670
+ if self.class.encrypt_cookie
1671
+ controller.cookies.encrypted
1672
+ elsif self.class.sign_cookie
1627
1673
  controller.cookies.signed
1628
1674
  else
1629
1675
  controller.cookies
@@ -1641,15 +1687,23 @@ module Authlogic
1641
1687
  self.class.send(:attr_reader, login_field) unless respond_to?(login_field)
1642
1688
  end
1643
1689
 
1690
+ # @api private
1644
1691
  def define_password_field_methods
1645
1692
  return unless password_field
1646
- self.class.send(:attr_writer, password_field) unless respond_to?("#{password_field}=")
1647
- self.class.send(:define_method, password_field) {} unless respond_to?(password_field)
1693
+ define_password_field_writer_method
1694
+ define_password_field_reader_methods
1695
+ end
1648
1696
 
1649
- # The password should not be accessible publicly. This way forms
1650
- # using form_for don't fill the password with the attempted
1651
- # password. To prevent this we just create this method that is
1652
- # private.
1697
+ # The password should not be accessible publicly. This way forms using
1698
+ # form_for don't fill the password with the attempted password. To prevent
1699
+ # this we just create this method that is private.
1700
+ #
1701
+ # @api private
1702
+ def define_password_field_reader_methods
1703
+ unless respond_to?(password_field)
1704
+ # Deliberate no-op method, see rationale above.
1705
+ self.class.send(:define_method, password_field) {}
1706
+ end
1653
1707
  self.class.class_eval(
1654
1708
  <<-EOS, __FILE__, __LINE__ + 1
1655
1709
  private
@@ -1660,6 +1714,28 @@ module Authlogic
1660
1714
  )
1661
1715
  end
1662
1716
 
1717
+ def define_password_field_writer_method
1718
+ unless respond_to?("#{password_field}=")
1719
+ self.class.send(:attr_writer, password_field)
1720
+ end
1721
+ end
1722
+
1723
+ # Creating an alias method for the "record" method based on the klass
1724
+ # name, so that we can do:
1725
+ #
1726
+ # session.user
1727
+ #
1728
+ # instead of:
1729
+ #
1730
+ # session.record
1731
+ #
1732
+ # @api private
1733
+ def define_record_alias_method
1734
+ noun = klass_name.demodulize.underscore.to_sym
1735
+ return if respond_to?(noun)
1736
+ self.class.send(:alias_method, noun, :record)
1737
+ end
1738
+
1663
1739
  def destroy_cookie
1664
1740
  controller.cookies.delete cookie_key, domain: controller.cookie_domain
1665
1741
  end
@@ -1695,8 +1771,10 @@ module Authlogic
1695
1771
  attempted_record.failed_login_count >= consecutive_failed_logins_limit
1696
1772
  end
1697
1773
 
1774
+ # @deprecated in favor of `self.class.record_selection_method`
1698
1775
  def find_by_login_method
1699
- self.class.find_by_login_method
1776
+ ::ActiveSupport::Deprecation.warn(E_DPR_FIND_BY_LOGIN_METHOD)
1777
+ self.class.record_selection_method
1700
1778
  end
1701
1779
 
1702
1780
  def generalize_credentials_error_messages?
@@ -1705,13 +1783,8 @@ module Authlogic
1705
1783
 
1706
1784
  # @api private
1707
1785
  def generate_cookie_for_saving
1708
- creds = ::Authlogic::CookieCredentials.new(
1709
- record.persistence_token,
1710
- record.send(record.class.primary_key),
1711
- remember_me? ? remember_me_until : nil
1712
- )
1713
1786
  {
1714
- value: creds.to_s,
1787
+ value: generate_cookie_value.to_s,
1715
1788
  expires: remember_me_until,
1716
1789
  secure: secure,
1717
1790
  httponly: httponly,
@@ -1720,6 +1793,14 @@ module Authlogic
1720
1793
  }
1721
1794
  end
1722
1795
 
1796
+ def generate_cookie_value
1797
+ ::Authlogic::CookieCredentials.new(
1798
+ record.persistence_token,
1799
+ record.send(record.class.primary_key),
1800
+ remember_me? ? remember_me_until : nil
1801
+ )
1802
+ end
1803
+
1723
1804
  # Returns a Proc to be executed by
1724
1805
  # `ActionController::HttpAuthentication::Basic` when credentials are
1725
1806
  # present in the HTTP request.
@@ -1747,7 +1828,7 @@ module Authlogic
1747
1828
  end
1748
1829
  end
1749
1830
 
1750
- def increment_login_cout
1831
+ def increment_login_count
1751
1832
  if record.respond_to?(:login_count)
1752
1833
  record.login_count = (record.login_count.blank? ? 1 : record.login_count + 1)
1753
1834
  end
@@ -1898,6 +1979,18 @@ module Authlogic
1898
1979
  attempted_record.failed_login_count = 0
1899
1980
  end
1900
1981
 
1982
+ # @api private
1983
+ def run_the_after_validation_callbacks
1984
+ run_callbacks(new_session? ? :after_validation_on_create : :after_validation_on_update)
1985
+ run_callbacks(:after_validation)
1986
+ end
1987
+
1988
+ # @api private
1989
+ def run_the_before_validation_callbacks
1990
+ run_callbacks(:before_validation)
1991
+ run_callbacks(new_session? ? :before_validation_on_create : :before_validation_on_update)
1992
+ end
1993
+
1901
1994
  # `args[0]` is the name of a model method, like
1902
1995
  # `find_by_single_access_token` or `find_by_smart_case_login_field`.
1903
1996
  def search_for_record(*args)
@@ -1935,11 +2028,7 @@ module Authlogic
1935
2028
  end
1936
2029
 
1937
2030
  def save_cookie
1938
- if sign_cookie?
1939
- controller.cookies.signed[cookie_key] = generate_cookie_for_saving
1940
- else
1941
- controller.cookies[cookie_key] = generate_cookie_for_saving
1942
- end
2031
+ cookie_jar[cookie_key] = generate_cookie_for_saving
1943
2032
  end
1944
2033
 
1945
2034
  # @api private
@@ -1969,7 +2058,7 @@ module Authlogic
1969
2058
  end
1970
2059
 
1971
2060
  def update_info
1972
- increment_login_cout
2061
+ increment_login_count
1973
2062
  clear_failed_login_count
1974
2063
  update_login_timestamps
1975
2064
  update_login_ip_addresses
@@ -2016,7 +2105,10 @@ module Authlogic
2016
2105
  self.invalid_password = false
2017
2106
  validate_by_password__blank_fields
2018
2107
  return if errors.count > 0
2019
- self.attempted_record = search_for_record(find_by_login_method, send(login_field))
2108
+ self.attempted_record = search_for_record(
2109
+ self.class.record_selection_method,
2110
+ send(login_field)
2111
+ )
2020
2112
  if attempted_record.blank?
2021
2113
  add_login_not_found_error
2022
2114
  return
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require File.dirname(__FILE__) + "/test_case/rails_request_adapter"
4
+ require File.dirname(__FILE__) + "/test_case/mock_api_controller"
4
5
  require File.dirname(__FILE__) + "/test_case/mock_cookie_jar"
5
6
  require File.dirname(__FILE__) + "/test_case/mock_controller"
6
7
  require File.dirname(__FILE__) + "/test_case/mock_logger"
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Authlogic
4
+ module TestCase
5
+ # Basically acts like an API controller but doesn't do anything.
6
+ # Authlogic can interact with this, do it's thing and then you can look at
7
+ # the controller object to see if anything changed.
8
+ class MockAPIController < ControllerAdapters::AbstractAdapter
9
+ attr_writer :request_content_type
10
+
11
+ def initialize
12
+ end
13
+
14
+ # Expected API controller has no cookies method.
15
+ undef :cookies
16
+
17
+ def cookie_domain
18
+ nil
19
+ end
20
+
21
+ def logger
22
+ @logger ||= MockLogger.new
23
+ end
24
+
25
+ def params
26
+ @params ||= {}
27
+ end
28
+
29
+ def request
30
+ @request ||= MockRequest.new(self)
31
+ end
32
+
33
+ def request_content_type
34
+ @request_content_type ||= "text/html"
35
+ end
36
+
37
+ def session
38
+ @session ||= {}
39
+ end
40
+
41
+ # If method is defined, it causes below behavior...
42
+ # controller = Authlogic::ControllerAdapters::RailsAdapter.new(
43
+ # Authlogic::TestCase::MockAPIController.new
44
+ # )
45
+ # controller.responds_to_single_access_allowed? #=> true
46
+ # controller.single_access_allowed?
47
+ # #=> NoMethodError: undefined method `single_access_allowed?' for nil:NilClass
48
+ #
49
+ undef :single_access_allowed?
50
+ end
51
+ end
52
+ end
@@ -39,7 +39,7 @@ module Authlogic
39
39
  end
40
40
 
41
41
  def request
42
- @request ||= MockRequest.new(controller)
42
+ @request ||= MockRequest.new(self)
43
43
  end
44
44
 
45
45
  def request_content_type
@@ -23,6 +23,10 @@ module Authlogic
23
23
  def signed
24
24
  @signed ||= MockSignedCookieJar.new(self)
25
25
  end
26
+
27
+ def encrypted
28
+ @encrypted ||= MockEncryptedCookieJar.new(self)
29
+ end
26
30
  end
27
31
 
28
32
  # A mock of `ActionDispatch::Cookies::SignedKeyRotatingCookieJar`
@@ -35,6 +39,7 @@ module Authlogic
35
39
 
36
40
  def initialize(parent_jar)
37
41
  @parent_jar = parent_jar
42
+ parent_jar.each { |k, v| self[k] = v }
38
43
  end
39
44
 
40
45
  def [](val)
@@ -51,5 +56,37 @@ module Authlogic
51
56
  @parent_jar[key] = options
52
57
  end
53
58
  end
59
+
60
+ # Which ActionDispatch class is this a mock of?
61
+ # TODO: Document as with other mocks above.
62
+ class MockEncryptedCookieJar < MockCookieJar
63
+ attr_reader :parent_jar # helper for testing
64
+
65
+ def initialize(parent_jar)
66
+ @parent_jar = parent_jar
67
+ parent_jar.each { |k, v| self[k] = v }
68
+ end
69
+
70
+ def [](val)
71
+ encrypted_message = @parent_jar[val]
72
+ if encrypted_message
73
+ self.class.decrypt(encrypted_message)
74
+ end
75
+ end
76
+
77
+ def []=(key, options)
78
+ options[:value] = self.class.encrypt(options[:value])
79
+ @parent_jar[key] = options
80
+ end
81
+
82
+ # simple caesar cipher for testing
83
+ def self.encrypt(str)
84
+ str.unpack("U*").map(&:succ).pack("U*")
85
+ end
86
+
87
+ def self.decrypt(str)
88
+ str.unpack("U*").map(&:pred).pack("U*")
89
+ end
90
+ end
54
91
  end
55
92
  end
@@ -9,6 +9,10 @@ module Authlogic
9
9
  self.controller = controller
10
10
  end
11
11
 
12
+ def format
13
+ controller.request_content_type if controller.respond_to? :request_content_type
14
+ end
15
+
12
16
  def ip
13
17
  controller&.respond_to?(:env) &&
14
18
  controller.env.is_a?(Hash) &&
@@ -17,6 +17,6 @@ module Authlogic
17
17
  #
18
18
  # @api public
19
19
  def self.gem_version
20
- ::Gem::Version.new("5.1.0")
20
+ ::Gem::Version.new("6.3.0")
21
21
  end
22
22
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: authlogic
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.1.0
4
+ version: 6.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ben Johnson
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2020-03-24 00:00:00.000000000 Z
13
+ date: 2020-12-18 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activemodel
@@ -21,7 +21,7 @@ dependencies:
21
21
  version: '5.2'
22
22
  - - "<"
23
23
  - !ruby/object:Gem::Version
24
- version: '6.1'
24
+ version: '6.2'
25
25
  type: :runtime
26
26
  prerelease: false
27
27
  version_requirements: !ruby/object:Gem::Requirement
@@ -31,7 +31,7 @@ dependencies:
31
31
  version: '5.2'
32
32
  - - "<"
33
33
  - !ruby/object:Gem::Version
34
- version: '6.1'
34
+ version: '6.2'
35
35
  - !ruby/object:Gem::Dependency
36
36
  name: activerecord
37
37
  requirement: !ruby/object:Gem::Requirement
@@ -41,7 +41,7 @@ dependencies:
41
41
  version: '5.2'
42
42
  - - "<"
43
43
  - !ruby/object:Gem::Version
44
- version: '6.1'
44
+ version: '6.2'
45
45
  type: :runtime
46
46
  prerelease: false
47
47
  version_requirements: !ruby/object:Gem::Requirement
@@ -51,7 +51,7 @@ dependencies:
51
51
  version: '5.2'
52
52
  - - "<"
53
53
  - !ruby/object:Gem::Version
54
- version: '6.1'
54
+ version: '6.2'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: activesupport
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -61,7 +61,7 @@ dependencies:
61
61
  version: '5.2'
62
62
  - - "<"
63
63
  - !ruby/object:Gem::Version
64
- version: '6.1'
64
+ version: '6.2'
65
65
  type: :runtime
66
66
  prerelease: false
67
67
  version_requirements: !ruby/object:Gem::Requirement
@@ -71,7 +71,7 @@ dependencies:
71
71
  version: '5.2'
72
72
  - - "<"
73
73
  - !ruby/object:Gem::Version
74
- version: '6.1'
74
+ version: '6.2'
75
75
  - !ruby/object:Gem::Dependency
76
76
  name: request_store
77
77
  requirement: !ruby/object:Gem::Requirement
@@ -86,26 +86,6 @@ dependencies:
86
86
  - - "~>"
87
87
  - !ruby/object:Gem::Version
88
88
  version: '1.0'
89
- - !ruby/object:Gem::Dependency
90
- name: scrypt
91
- requirement: !ruby/object:Gem::Requirement
92
- requirements:
93
- - - ">="
94
- - !ruby/object:Gem::Version
95
- version: '1.2'
96
- - - "<"
97
- - !ruby/object:Gem::Version
98
- version: '4.0'
99
- type: :runtime
100
- prerelease: false
101
- version_requirements: !ruby/object:Gem::Requirement
102
- requirements:
103
- - - ">="
104
- - !ruby/object:Gem::Version
105
- version: '1.2'
106
- - - "<"
107
- - !ruby/object:Gem::Version
108
- version: '4.0'
109
89
  - !ruby/object:Gem::Dependency
110
90
  name: bcrypt
111
91
  requirement: !ruby/object:Gem::Requirement
@@ -196,14 +176,14 @@ dependencies:
196
176
  requirements:
197
177
  - - "~>"
198
178
  - !ruby/object:Gem::Version
199
- version: 0.67.2
179
+ version: 0.80.1
200
180
  type: :development
201
181
  prerelease: false
202
182
  version_requirements: !ruby/object:Gem::Requirement
203
183
  requirements:
204
184
  - - "~>"
205
185
  - !ruby/object:Gem::Version
206
- version: 0.67.2
186
+ version: 0.80.1
207
187
  - !ruby/object:Gem::Dependency
208
188
  name: rubocop-performance
209
189
  requirement: !ruby/object:Gem::Requirement
@@ -218,6 +198,26 @@ dependencies:
218
198
  - - "~>"
219
199
  - !ruby/object:Gem::Version
220
200
  version: '1.1'
201
+ - !ruby/object:Gem::Dependency
202
+ name: scrypt
203
+ requirement: !ruby/object:Gem::Requirement
204
+ requirements:
205
+ - - ">="
206
+ - !ruby/object:Gem::Version
207
+ version: '1.2'
208
+ - - "<"
209
+ - !ruby/object:Gem::Version
210
+ version: '4.0'
211
+ type: :development
212
+ prerelease: false
213
+ version_requirements: !ruby/object:Gem::Requirement
214
+ requirements:
215
+ - - ">="
216
+ - !ruby/object:Gem::Version
217
+ version: '1.2'
218
+ - - "<"
219
+ - !ruby/object:Gem::Version
220
+ version: '4.0'
221
221
  - !ruby/object:Gem::Dependency
222
222
  name: simplecov
223
223
  requirement: !ruby/object:Gem::Requirement
@@ -252,14 +252,14 @@ dependencies:
252
252
  requirements:
253
253
  - - "~>"
254
254
  - !ruby/object:Gem::Version
255
- version: 1.3.13
255
+ version: 1.4.0
256
256
  type: :development
257
257
  prerelease: false
258
258
  version_requirements: !ruby/object:Gem::Requirement
259
259
  requirements:
260
260
  - - "~>"
261
261
  - !ruby/object:Gem::Version
262
- version: 1.3.13
262
+ version: 1.4.0
263
263
  - !ruby/object:Gem::Dependency
264
264
  name: timecop
265
265
  requirement: !ruby/object:Gem::Requirement
@@ -305,16 +305,22 @@ files:
305
305
  - lib/authlogic/crypto_providers.rb
306
306
  - lib/authlogic/crypto_providers/bcrypt.rb
307
307
  - lib/authlogic/crypto_providers/md5.rb
308
+ - lib/authlogic/crypto_providers/md5/v2.rb
308
309
  - lib/authlogic/crypto_providers/scrypt.rb
309
310
  - lib/authlogic/crypto_providers/sha1.rb
311
+ - lib/authlogic/crypto_providers/sha1/v2.rb
310
312
  - lib/authlogic/crypto_providers/sha256.rb
313
+ - lib/authlogic/crypto_providers/sha256/v2.rb
311
314
  - lib/authlogic/crypto_providers/sha512.rb
315
+ - lib/authlogic/crypto_providers/sha512/v2.rb
316
+ - lib/authlogic/errors.rb
312
317
  - lib/authlogic/i18n.rb
313
318
  - lib/authlogic/i18n/translator.rb
314
319
  - lib/authlogic/random.rb
315
320
  - lib/authlogic/session/base.rb
316
321
  - lib/authlogic/session/magic_column/assigns_last_request_at.rb
317
322
  - lib/authlogic/test_case.rb
323
+ - lib/authlogic/test_case/mock_api_controller.rb
318
324
  - lib/authlogic/test_case/mock_controller.rb
319
325
  - lib/authlogic/test_case/mock_cookie_jar.rb
320
326
  - lib/authlogic/test_case/mock_logger.rb
@@ -333,7 +339,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
333
339
  requirements:
334
340
  - - ">="
335
341
  - !ruby/object:Gem::Version
336
- version: 2.3.0
342
+ version: 2.4.0
337
343
  required_rubygems_version: !ruby/object:Gem::Requirement
338
344
  requirements:
339
345
  - - ">="