authlogic 5.0.4 → 6.4.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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