authlogic 5.0.4 → 6.4.1

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.
@@ -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,7 +351,14 @@ module Authlogic
351
351
  - https://github.com/binarylogic/authlogic/pull/558
352
352
  - https://github.com/binarylogic/authlogic/pull/577
353
353
  EOS
354
- VALID_SAME_SITE_VALUES = [nil, "Lax", "Strict"].freeze
354
+ E_DPR_FIND_BY_LOGIN_METHOD = <<~EOS.squish.freeze
355
+ find_by_login_method is deprecated in favor of record_selection_method,
356
+ to avoid confusion with ActiveRecord's "Dynamic Finders".
357
+ (https://guides.rubyonrails.org/v6.0/active_record_querying.html#dynamic-finders)
358
+ For example, rubocop-rails is confused by the deprecated method.
359
+ (https://github.com/rubocop-hq/rubocop-rails/blob/master/lib/rubocop/cop/rails/dynamic_find_by.rb)
360
+ EOS
361
+ VALID_SAME_SITE_VALUES = [nil, "Lax", "Strict", "None"].freeze
355
362
 
356
363
  # Callbacks
357
364
  # =========
@@ -415,10 +422,11 @@ 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
427
+ after_create :renew_session_id
420
428
 
421
- after_destroy :destroy_cookie
429
+ after_destroy :destroy_cookie, if: :cookie_enabled?
422
430
  after_destroy :update_session
423
431
 
424
432
  # `validate` callbacks, in deliberate order. For example,
@@ -438,8 +446,7 @@ module Authlogic
438
446
 
439
447
  class << self
440
448
  attr_accessor(
441
- :configured_password_methods,
442
- :configured_klass_methods
449
+ :configured_password_methods
443
450
  )
444
451
  end
445
452
  attr_accessor(
@@ -472,14 +479,9 @@ module Authlogic
472
479
  !controller.nil?
473
480
  end
474
481
 
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.
482
+ # Allow users to log in via HTTP basic authentication.
481
483
  #
482
- # * <tt>Default:</tt> true
484
+ # * <tt>Default:</tt> false
483
485
  # * <tt>Accepts:</tt> Boolean
484
486
  def allow_http_basic_auth(value = nil)
485
487
  rw_config(:allow_http_basic_auth, value, false)
@@ -669,35 +671,10 @@ module Authlogic
669
671
  end
670
672
  end
671
673
 
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
674
+ # @deprecated in favor of record_selection_method
699
675
  def find_by_login_method(value = nil)
700
- rw_config(:find_by_login_method, value, "find_by_smart_case_login_field")
676
+ ::ActiveSupport::Deprecation.warn(E_DPR_FIND_BY_LOGIN_METHOD)
677
+ record_selection_method(value)
701
678
  end
702
679
  alias find_by_login_method= find_by_login_method
703
680
 
@@ -782,15 +759,23 @@ module Authlogic
782
759
  # example, the UserSession class will authenticate with the User class
783
760
  # unless you specify otherwise in your configuration. See
784
761
  # authenticate_with for information on how to change this value.
762
+ #
763
+ # @api public
785
764
  def klass
786
765
  @klass ||= klass_name ? klass_name.constantize : nil
787
766
  end
788
767
 
789
- # The string of the model name class guessed from the actual session class name.
768
+ # The model name, guessed from the session class name, e.g. "User",
769
+ # from "UserSession".
770
+ #
771
+ # TODO: This method can return nil. We should explore this. It seems
772
+ # likely to cause a NoMethodError later, so perhaps we should raise an
773
+ # error instead.
774
+ #
775
+ # @api private
790
776
  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
777
+ return @klass_name if instance_variable_defined?(:@klass_name)
778
+ @klass_name = name.scan(/(.*)Session/)[0]&.first
794
779
  end
795
780
 
796
781
  # The name of the method you want Authlogic to create for storing the
@@ -798,8 +783,8 @@ module Authlogic
798
783
  # Authlogic::Session, if you want it can be something completely
799
784
  # different than the field in your model. So if you wanted people to
800
785
  # 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.
786
+ # completely doable. See the `record_selection_method` configuration
787
+ # option for details.
803
788
  #
804
789
  # * <tt>Default:</tt> klass.login_field || klass.email_field
805
790
  # * <tt>Accepts:</tt> Symbol or String
@@ -882,6 +867,47 @@ module Authlogic
882
867
  end
883
868
  alias password_field= password_field
884
869
 
870
+ # Authlogic tries to validate the credentials passed to it. One part of
871
+ # validation is actually finding the user and making sure it exists.
872
+ # What method it uses the do this is up to you.
873
+ #
874
+ # ```
875
+ # # user_session.rb
876
+ # record_selection_method :find_by_email
877
+ # ```
878
+ #
879
+ # This is the recommended way to find the user by email address.
880
+ # The resulting query will be `User.find_by_email(send(login_field))`.
881
+ # (`login_field` will fall back to `email_field` if there's no `login`
882
+ # or `username` column).
883
+ #
884
+ # In your User model you can make that method do anything you want,
885
+ # giving you complete control of how users are found by the UserSession.
886
+ #
887
+ # Let's take an example: You want to allow users to login by username or
888
+ # email. Set this to the name of the class method that does this in the
889
+ # User model. Let's call it "find_by_username_or_email"
890
+ #
891
+ # ```
892
+ # class User < ActiveRecord::Base
893
+ # def self.find_by_username_or_email(login)
894
+ # find_by_username(login) || find_by_email(login)
895
+ # end
896
+ # end
897
+ # ```
898
+ #
899
+ # Now just specify the name of this method for this configuration option
900
+ # and you are all set. You can do anything you want here. Maybe you
901
+ # allow users to have multiple logins and you want to search a has_many
902
+ # relationship, etc. The sky is the limit.
903
+ #
904
+ # * <tt>Default:</tt> "find_by_smart_case_login_field"
905
+ # * <tt>Accepts:</tt> Symbol or String
906
+ def record_selection_method(value = nil)
907
+ rw_config(:record_selection_method, value, "find_by_smart_case_login_field")
908
+ end
909
+ alias record_selection_method= record_selection_method
910
+
885
911
  # Whether or not to request HTTP authentication
886
912
  #
887
913
  # If set to true and no HTTP authentication credentials are sent with
@@ -951,16 +977,40 @@ module Authlogic
951
977
  end
952
978
  alias secure= secure
953
979
 
980
+ # Should the Rack session ID be reset after authentication, to protect
981
+ # against Session Fixation attacks?
982
+ #
983
+ # * <tt>Default:</tt> true
984
+ # * <tt>Accepts:</tt> Boolean
985
+ def session_fixation_defense(value = nil)
986
+ rw_config(:session_fixation_defense, value, true)
987
+ end
988
+ alias session_fixation_defense= session_fixation_defense
989
+
954
990
  # Should the cookie be signed? If the controller adapter supports it, this is a
955
991
  # measure against cookie tampering.
956
992
  def sign_cookie(value = nil)
957
- if value && !controller.cookies.respond_to?(:signed)
993
+ if value && controller && !controller.cookies.respond_to?(:signed)
958
994
  raise "Signed cookies not supported with #{controller.class}!"
959
995
  end
960
996
  rw_config(:sign_cookie, value, false)
961
997
  end
962
998
  alias sign_cookie= sign_cookie
963
999
 
1000
+ # Should the cookie be encrypted? If the controller adapter supports it, this is a
1001
+ # measure to hide the contents of the cookie (e.g. persistence_token)
1002
+ def encrypt_cookie(value = nil)
1003
+ if value && controller && !controller.cookies.respond_to?(:encrypted)
1004
+ raise "Encrypted cookies not supported with #{controller.class}!"
1005
+ end
1006
+ if value && sign_cookie
1007
+ raise "It is recommended to use encrypt_cookie instead of sign_cookie. " \
1008
+ "You may not enable both options."
1009
+ end
1010
+ rw_config(:encrypt_cookie, value, false)
1011
+ end
1012
+ alias encrypt_cookie= encrypt_cookie
1013
+
964
1014
  # Works exactly like cookie_key, but for sessions. See cookie_key for more info.
965
1015
  #
966
1016
  # * <tt>Default:</tt> cookie_key
@@ -1065,24 +1115,10 @@ module Authlogic
1065
1115
  # Constructor
1066
1116
  # ===========
1067
1117
 
1068
- # rubocop:disable Metrics/AbcSize
1069
1118
  def initialize(*args)
1070
1119
  @id = nil
1071
1120
  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
-
1121
+ define_record_alias_method
1086
1122
  raise Activation::NotActivatedError unless self.class.activated?
1087
1123
  unless self.class.configured_password_methods
1088
1124
  configure_password_methods
@@ -1091,7 +1127,6 @@ module Authlogic
1091
1127
  instance_variable_set("@#{password_field}", nil)
1092
1128
  self.credentials = args
1093
1129
  end
1094
- # rubocop:enable Metrics/AbcSize
1095
1130
 
1096
1131
  # Public instance methods
1097
1132
  # =======================
@@ -1480,6 +1515,23 @@ module Authlogic
1480
1515
  sign_cookie == true || sign_cookie == "true" || sign_cookie == "1"
1481
1516
  end
1482
1517
 
1518
+ # If the cookie should be encrypted
1519
+ def encrypt_cookie
1520
+ return @encrypt_cookie if defined?(@encrypt_cookie)
1521
+ @encrypt_cookie = self.class.encrypt_cookie
1522
+ end
1523
+
1524
+ # Accepts a boolean as to whether the cookie should be encrypted. If true
1525
+ # the cookie will be saved in an encrypted state.
1526
+ def encrypt_cookie=(value)
1527
+ @encrypt_cookie = value
1528
+ end
1529
+
1530
+ # See encrypt_cookie
1531
+ def encrypt_cookie?
1532
+ encrypt_cookie == true || encrypt_cookie == "true" || encrypt_cookie == "1"
1533
+ end
1534
+
1483
1535
  # The scope of the current object
1484
1536
  def scope
1485
1537
  @scope ||= {}
@@ -1497,24 +1549,21 @@ module Authlogic
1497
1549
  # Determines if the information you provided for authentication is valid
1498
1550
  # or not. If there is a problem with the information provided errors will
1499
1551
  # be added to the errors object and this method will return false.
1552
+ #
1553
+ # @api public
1500
1554
  def valid?
1501
1555
  errors.clear
1502
1556
  self.attempted_record = nil
1503
-
1504
- run_callbacks(:before_validation)
1505
- run_callbacks(new_session? ? :before_validation_on_create : :before_validation_on_update)
1557
+ run_the_before_validation_callbacks
1506
1558
 
1507
1559
  # Run the `validate` callbacks, eg. `validate_by_password`.
1508
1560
  # This is when `attempted_record` is set.
1509
1561
  run_callbacks(:validate)
1510
1562
 
1511
1563
  ensure_authentication_attempted
1512
-
1513
1564
  if errors.empty?
1514
- run_callbacks(new_session? ? :after_validation_on_create : :after_validation_on_update)
1515
- run_callbacks(:after_validation)
1565
+ run_the_after_validation_callbacks
1516
1566
  end
1517
-
1518
1567
  save_record(attempted_record)
1519
1568
  errors.empty?
1520
1569
  end
@@ -1616,14 +1665,22 @@ module Authlogic
1616
1665
  # @api private
1617
1666
  # @return ::Authlogic::CookieCredentials or if no cookie is found, nil
1618
1667
  def cookie_credentials
1668
+ return unless cookie_enabled?
1669
+
1619
1670
  cookie_value = cookie_jar[cookie_key]
1620
1671
  unless cookie_value.nil?
1621
1672
  ::Authlogic::CookieCredentials.parse(cookie_value)
1622
1673
  end
1623
1674
  end
1624
1675
 
1676
+ def cookie_enabled?
1677
+ !controller.cookies.nil?
1678
+ end
1679
+
1625
1680
  def cookie_jar
1626
- if self.class.sign_cookie
1681
+ if self.class.encrypt_cookie
1682
+ controller.cookies.encrypted
1683
+ elsif self.class.sign_cookie
1627
1684
  controller.cookies.signed
1628
1685
  else
1629
1686
  controller.cookies
@@ -1635,21 +1692,36 @@ module Authlogic
1635
1692
  define_password_field_methods
1636
1693
  end
1637
1694
 
1695
+ # Assign a new controller-session ID, to defend against Session Fixation.
1696
+ # https://guides.rubyonrails.org/v6.0/security.html#session-fixation
1697
+ def renew_session_id
1698
+ return unless self.class.session_fixation_defense
1699
+ controller.renew_session_id
1700
+ end
1701
+
1638
1702
  def define_login_field_methods
1639
1703
  return unless login_field
1640
1704
  self.class.send(:attr_writer, login_field) unless respond_to?("#{login_field}=")
1641
1705
  self.class.send(:attr_reader, login_field) unless respond_to?(login_field)
1642
1706
  end
1643
1707
 
1708
+ # @api private
1644
1709
  def define_password_field_methods
1645
1710
  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)
1711
+ define_password_field_writer_method
1712
+ define_password_field_reader_methods
1713
+ end
1648
1714
 
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.
1715
+ # The password should not be accessible publicly. This way forms using
1716
+ # form_for don't fill the password with the attempted password. To prevent
1717
+ # this we just create this method that is private.
1718
+ #
1719
+ # @api private
1720
+ def define_password_field_reader_methods
1721
+ unless respond_to?(password_field)
1722
+ # Deliberate no-op method, see rationale above.
1723
+ self.class.send(:define_method, password_field) {}
1724
+ end
1653
1725
  self.class.class_eval(
1654
1726
  <<-EOS, __FILE__, __LINE__ + 1
1655
1727
  private
@@ -1660,6 +1732,28 @@ module Authlogic
1660
1732
  )
1661
1733
  end
1662
1734
 
1735
+ def define_password_field_writer_method
1736
+ unless respond_to?("#{password_field}=")
1737
+ self.class.send(:attr_writer, password_field)
1738
+ end
1739
+ end
1740
+
1741
+ # Creating an alias method for the "record" method based on the klass
1742
+ # name, so that we can do:
1743
+ #
1744
+ # session.user
1745
+ #
1746
+ # instead of:
1747
+ #
1748
+ # session.record
1749
+ #
1750
+ # @api private
1751
+ def define_record_alias_method
1752
+ noun = klass_name.demodulize.underscore.to_sym
1753
+ return if respond_to?(noun)
1754
+ self.class.send(:alias_method, noun, :record)
1755
+ end
1756
+
1663
1757
  def destroy_cookie
1664
1758
  controller.cookies.delete cookie_key, domain: controller.cookie_domain
1665
1759
  end
@@ -1695,8 +1789,10 @@ module Authlogic
1695
1789
  attempted_record.failed_login_count >= consecutive_failed_logins_limit
1696
1790
  end
1697
1791
 
1792
+ # @deprecated in favor of `self.class.record_selection_method`
1698
1793
  def find_by_login_method
1699
- self.class.find_by_login_method
1794
+ ::ActiveSupport::Deprecation.warn(E_DPR_FIND_BY_LOGIN_METHOD)
1795
+ self.class.record_selection_method
1700
1796
  end
1701
1797
 
1702
1798
  def generalize_credentials_error_messages?
@@ -1705,13 +1801,8 @@ module Authlogic
1705
1801
 
1706
1802
  # @api private
1707
1803
  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
1804
  {
1714
- value: creds.to_s,
1805
+ value: generate_cookie_value.to_s,
1715
1806
  expires: remember_me_until,
1716
1807
  secure: secure,
1717
1808
  httponly: httponly,
@@ -1720,6 +1811,14 @@ module Authlogic
1720
1811
  }
1721
1812
  end
1722
1813
 
1814
+ def generate_cookie_value
1815
+ ::Authlogic::CookieCredentials.new(
1816
+ record.persistence_token,
1817
+ record.send(record.class.primary_key),
1818
+ remember_me? ? remember_me_until : nil
1819
+ )
1820
+ end
1821
+
1723
1822
  # Returns a Proc to be executed by
1724
1823
  # `ActionController::HttpAuthentication::Basic` when credentials are
1725
1824
  # present in the HTTP request.
@@ -1747,7 +1846,7 @@ module Authlogic
1747
1846
  end
1748
1847
  end
1749
1848
 
1750
- def increment_login_cout
1849
+ def increment_login_count
1751
1850
  if record.respond_to?(:login_count)
1752
1851
  record.login_count = (record.login_count.blank? ? 1 : record.login_count + 1)
1753
1852
  end
@@ -1898,6 +1997,18 @@ module Authlogic
1898
1997
  attempted_record.failed_login_count = 0
1899
1998
  end
1900
1999
 
2000
+ # @api private
2001
+ def run_the_after_validation_callbacks
2002
+ run_callbacks(new_session? ? :after_validation_on_create : :after_validation_on_update)
2003
+ run_callbacks(:after_validation)
2004
+ end
2005
+
2006
+ # @api private
2007
+ def run_the_before_validation_callbacks
2008
+ run_callbacks(:before_validation)
2009
+ run_callbacks(new_session? ? :before_validation_on_create : :before_validation_on_update)
2010
+ end
2011
+
1901
2012
  # `args[0]` is the name of a model method, like
1902
2013
  # `find_by_single_access_token` or `find_by_smart_case_login_field`.
1903
2014
  def search_for_record(*args)
@@ -1935,11 +2046,7 @@ module Authlogic
1935
2046
  end
1936
2047
 
1937
2048
  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
2049
+ cookie_jar[cookie_key] = generate_cookie_for_saving
1943
2050
  end
1944
2051
 
1945
2052
  # @api private
@@ -1969,7 +2076,7 @@ module Authlogic
1969
2076
  end
1970
2077
 
1971
2078
  def update_info
1972
- increment_login_cout
2079
+ increment_login_count
1973
2080
  clear_failed_login_count
1974
2081
  update_login_timestamps
1975
2082
  update_login_ip_addresses
@@ -2016,7 +2123,10 @@ module Authlogic
2016
2123
  self.invalid_password = false
2017
2124
  validate_by_password__blank_fields
2018
2125
  return if errors.count > 0
2019
- self.attempted_record = search_for_record(find_by_login_method, send(login_field))
2126
+ self.attempted_record = search_for_record(
2127
+ self.class.record_selection_method,
2128
+ send(login_field)
2129
+ )
2020
2130
  if attempted_record.blank?
2021
2131
  add_login_not_found_error
2022
2132
  return