rodauth 2.0.0 → 2.5.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.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +58 -0
  3. data/README.rdoc +14 -0
  4. data/doc/base.rdoc +2 -0
  5. data/doc/guides/admin_activation.rdoc +46 -0
  6. data/doc/guides/already_authenticated.rdoc +10 -0
  7. data/doc/guides/alternative_login.rdoc +46 -0
  8. data/doc/guides/create_account_programmatically.rdoc +38 -0
  9. data/doc/guides/delay_password.rdoc +25 -0
  10. data/doc/guides/email_only.rdoc +16 -0
  11. data/doc/guides/i18n.rdoc +26 -0
  12. data/doc/{internals.rdoc → guides/internals.rdoc} +0 -0
  13. data/doc/guides/links.rdoc +12 -0
  14. data/doc/guides/login_return.rdoc +37 -0
  15. data/doc/guides/password_column.rdoc +25 -0
  16. data/doc/guides/password_confirmation.rdoc +37 -0
  17. data/doc/guides/password_requirements.rdoc +30 -0
  18. data/doc/guides/paths.rdoc +36 -0
  19. data/doc/guides/query_params.rdoc +9 -0
  20. data/doc/guides/redirects.rdoc +17 -0
  21. data/doc/guides/registration_field.rdoc +68 -0
  22. data/doc/guides/require_mfa.rdoc +30 -0
  23. data/doc/guides/reset_password_autologin.rdoc +21 -0
  24. data/doc/guides/status_column.rdoc +28 -0
  25. data/doc/guides/totp_or_recovery.rdoc +16 -0
  26. data/doc/jwt_refresh.rdoc +11 -0
  27. data/doc/login.rdoc +8 -0
  28. data/doc/login_password_requirements_base.rdoc +3 -0
  29. data/doc/otp.rdoc +1 -0
  30. data/doc/password_pepper.rdoc +44 -0
  31. data/doc/release_notes/2.1.0.txt +31 -0
  32. data/doc/release_notes/2.2.0.txt +39 -0
  33. data/doc/release_notes/2.3.0.txt +37 -0
  34. data/doc/release_notes/2.4.0.txt +22 -0
  35. data/doc/release_notes/2.5.0.txt +20 -0
  36. data/doc/verify_login_change.rdoc +1 -0
  37. data/lib/rodauth.rb +5 -5
  38. data/lib/rodauth/features/active_sessions.rb +5 -7
  39. data/lib/rodauth/features/audit_logging.rb +2 -0
  40. data/lib/rodauth/features/base.rb +21 -2
  41. data/lib/rodauth/features/change_password.rb +1 -1
  42. data/lib/rodauth/features/close_account.rb +8 -6
  43. data/lib/rodauth/features/create_account.rb +1 -0
  44. data/lib/rodauth/features/disallow_password_reuse.rb +4 -2
  45. data/lib/rodauth/features/email_auth.rb +2 -2
  46. data/lib/rodauth/features/http_basic_auth.rb +15 -2
  47. data/lib/rodauth/features/jwt.rb +12 -8
  48. data/lib/rodauth/features/jwt_cors.rb +15 -15
  49. data/lib/rodauth/features/jwt_refresh.rb +39 -10
  50. data/lib/rodauth/features/login.rb +23 -12
  51. data/lib/rodauth/features/login_password_requirements_base.rb +9 -4
  52. data/lib/rodauth/features/otp.rb +5 -1
  53. data/lib/rodauth/features/password_complexity.rb +4 -2
  54. data/lib/rodauth/features/password_pepper.rb +45 -0
  55. data/lib/rodauth/features/remember.rb +2 -0
  56. data/lib/rodauth/features/session_expiration.rb +1 -6
  57. data/lib/rodauth/features/single_session.rb +1 -1
  58. data/lib/rodauth/features/sms_codes.rb +0 -1
  59. data/lib/rodauth/features/two_factor_base.rb +4 -4
  60. data/lib/rodauth/features/verify_account.rb +5 -0
  61. data/lib/rodauth/features/verify_account_grace_period.rb +3 -5
  62. data/lib/rodauth/features/verify_login_change.rb +2 -1
  63. data/lib/rodauth/features/webauthn.rb +1 -3
  64. data/lib/rodauth/features/webauthn_login.rb +1 -1
  65. data/lib/rodauth/migrations.rb +16 -5
  66. data/lib/rodauth/version.rb +1 -1
  67. data/templates/password-field.str +1 -1
  68. metadata +37 -5
@@ -23,6 +23,8 @@ module Rodauth
23
23
  auth_cached_method :login_form_footer_links
24
24
  auth_cached_method :login_form_footer
25
25
 
26
+ auth_value_methods :login_return_to_requested_location_path
27
+
26
28
  route do |r|
27
29
  check_already_logged_in
28
30
  before_login_route
@@ -62,7 +64,7 @@ module Rodauth
62
64
  throw_error_status(login_error_status, password_param, invalid_password_message)
63
65
  end
64
66
 
65
- _login('password')
67
+ login('password')
66
68
  end
67
69
 
68
70
  set_error_flash login_error_flash unless skip_error_flash
@@ -72,13 +74,29 @@ module Rodauth
72
74
 
73
75
  attr_reader :login_form_header
74
76
 
77
+ def login(auth_type)
78
+ saved_login_redirect = remove_session_value(login_redirect_session_key)
79
+ transaction do
80
+ before_login
81
+ login_session(auth_type)
82
+ yield if block_given?
83
+ after_login
84
+ end
85
+ set_notice_flash login_notice_flash
86
+ redirect(saved_login_redirect || login_redirect)
87
+ end
88
+
75
89
  def login_required
76
- if login_return_to_requested_location?
77
- set_session_value(login_redirect_session_key, request.fullpath)
90
+ if login_return_to_requested_location? && (path = login_return_to_requested_location_path)
91
+ set_session_value(login_redirect_session_key, path)
78
92
  end
79
93
  super
80
94
  end
81
95
 
96
+ def login_return_to_requested_location_path
97
+ request.fullpath if request.get?
98
+ end
99
+
82
100
  def after_login_entered_during_multi_phase_login
83
101
  set_notice_now_flash need_password_notice_flash
84
102
  if multi_phase_login_forms.length == 1 && (meth = multi_phase_login_forms[0][2])
@@ -126,15 +144,8 @@ module Rodauth
126
144
  end
127
145
 
128
146
  def _login(auth_type)
129
- saved_login_redirect = remove_session_value(login_redirect_session_key)
130
- transaction do
131
- before_login
132
- login_session(auth_type)
133
- yield if block_given?
134
- after_login
135
- end
136
- set_notice_flash login_notice_flash
137
- redirect(saved_login_redirect || login_redirect)
147
+ warn("Deprecated #_login method called, use #login instead.")
148
+ login(auth_type)
138
149
  end
139
150
  end
140
151
  end
@@ -4,8 +4,10 @@ module Rodauth
4
4
  Feature.define(:login_password_requirements_base, :LoginPasswordRequirementsBase) do
5
5
  translatable_method :already_an_account_with_this_login_message, 'already an account with this login'
6
6
  auth_value_method :login_confirm_param, 'login-confirm'
7
+ auth_value_method :login_email_regexp, /\A[^,;@ \r\n]+@[^,@; \r\n]+\.[^,@; \r\n]+\z/
7
8
  auth_value_method :login_minimum_length, 3
8
9
  auth_value_method :login_maximum_length, 255
10
+ translatable_method :login_not_valid_email_message, 'not a valid email address'
9
11
  translatable_method :logins_do_not_match_message, 'logins do not match'
10
12
  auth_value_method :password_confirm_param, 'password-confirm'
11
13
  auth_value_method :password_minimum_length, 6
@@ -28,6 +30,7 @@ module Rodauth
28
30
 
29
31
  auth_methods(
30
32
  :login_meets_requirements?,
33
+ :login_valid_email?,
31
34
  :password_hash,
32
35
  :password_meets_requirements?,
33
36
  :set_password
@@ -104,13 +107,15 @@ module Rodauth
104
107
 
105
108
  def login_meets_email_requirements?(login)
106
109
  return true unless require_email_address_logins?
107
- if login =~ /\A[^,;@ \r\n]+@[^,@; \r\n]+\.[^,@; \r\n]+\z/
108
- return true
109
- end
110
- @login_requirement_message = 'not a valid email address'
110
+ return true if login_valid_email?(login)
111
+ @login_requirement_message = login_not_valid_email_message
111
112
  return false
112
113
  end
113
114
 
115
+ def login_valid_email?(login)
116
+ login =~ login_email_regexp
117
+ end
118
+
114
119
  def password_meets_length_requirements?(password)
115
120
  return true if password_minimum_length <= password.length
116
121
  @password_requirement_message = password_too_short_message
@@ -79,6 +79,7 @@ module Rodauth
79
79
  :otp,
80
80
  :otp_exists?,
81
81
  :otp_key,
82
+ :otp_last_use,
82
83
  :otp_locked_out?,
83
84
  :otp_new_secret,
84
85
  :otp_provisioning_name,
@@ -255,7 +256,6 @@ module Rodauth
255
256
  def otp_remove
256
257
  otp_key_ds.delete
257
258
  @otp_key = nil
258
- super if defined?(super)
259
259
  end
260
260
 
261
261
  def otp_add_key
@@ -269,6 +269,10 @@ module Rodauth
269
269
  update(otp_keys_last_use_column=>Sequel::CURRENT_TIMESTAMP) == 1
270
270
  end
271
271
 
272
+ def otp_last_use
273
+ convert_timestamp(otp_key_ds.get(otp_keys_last_use_column))
274
+ end
275
+
272
276
  def otp_record_authentication_failure
273
277
  otp_key_ds.update(otp_keys_failures_column=>Sequel.identifier(otp_keys_failures_column) + 1)
274
278
  end
@@ -26,14 +26,16 @@ module Rodauth
26
26
 
27
27
  def post_configure
28
28
  super
29
- return if singleton_methods.map(&:to_sym).include?(:password_dictionary)
29
+ return if method(:password_dictionary).owner != Rodauth::PasswordComplexity
30
30
 
31
31
  case password_dictionary_file
32
32
  when false
33
- return
33
+ # nothing
34
34
  when nil
35
35
  default_dictionary_file = '/usr/share/dict/words'
36
+ # :nocov:
36
37
  if File.file?(default_dictionary_file)
38
+ # :nocov:
37
39
  words = File.read(default_dictionary_file)
38
40
  end
39
41
  else
@@ -0,0 +1,45 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Rodauth
4
+ Feature.define(:password_pepper, :PasswordPepper) do
5
+ depends :login_password_requirements_base
6
+
7
+ auth_value_method :password_pepper, nil
8
+ auth_value_method :previous_password_peppers, [""]
9
+ auth_value_method :password_pepper_update?, true
10
+
11
+ def password_match?(password)
12
+ if (result = super) && @previous_pepper_matched && password_pepper_update?
13
+ set_password(password)
14
+ end
15
+
16
+ result
17
+ end
18
+
19
+ private
20
+
21
+ def password_hash(password)
22
+ super(password + password_pepper.to_s)
23
+ end
24
+
25
+ def password_hash_match?(hash, password)
26
+ return super if password_pepper.nil?
27
+
28
+ return true if super(hash, password + password_pepper)
29
+
30
+ @previous_pepper_matched = previous_password_peppers.any? do |pepper|
31
+ super(hash, password + pepper)
32
+ end
33
+ end
34
+
35
+ def database_function_password_match?(name, hash_id, password, salt)
36
+ return super if password_pepper.nil?
37
+
38
+ return true if super(name, hash_id, password + password_pepper, salt)
39
+
40
+ @previous_pepper_matched = previous_password_peppers.any? do |pepper|
41
+ super(name, hash_id, password + pepper, salt)
42
+ end
43
+ end
44
+ end
45
+ end
@@ -58,7 +58,9 @@ module Rodauth
58
58
  if [remember_remember_param_value, remember_forget_param_value, remember_disable_param_value].include?(remember)
59
59
  transaction do
60
60
  before_remember
61
+ # :nocov:
61
62
  case remember
63
+ # :nocov:
62
64
  when remember_remember_param_value
63
65
  remember_login
64
66
  when remember_forget_param_value
@@ -3,6 +3,7 @@
3
3
  module Rodauth
4
4
  Feature.define(:session_expiration, :SessionExpiration) do
5
5
  error_flash "This session has expired, please login again"
6
+ redirect{require_login_redirect}
6
7
 
7
8
  auth_value_method :max_session_lifetime, 86400
8
9
  session_key :session_created_session_key, :session_created_at
@@ -11,8 +12,6 @@ module Rodauth
11
12
  auth_value_method :session_inactivity_timeout, 1800
12
13
  session_key :session_last_activity_session_key, :last_session_activity_at
13
14
 
14
- auth_value_methods :session_expiration_redirect
15
-
16
15
  def check_session_expiration
17
16
  return unless logged_in?
18
17
 
@@ -43,10 +42,6 @@ module Rodauth
43
42
  redirect session_expiration_redirect
44
43
  end
45
44
 
46
- def session_expiration_redirect
47
- require_login_redirect
48
- end
49
-
50
45
  def update_session
51
46
  super
52
47
  t = Time.now.to_i
@@ -85,7 +85,7 @@ module Rodauth
85
85
  end
86
86
 
87
87
  def before_logout
88
- reset_single_session_key if request.post?
88
+ reset_single_session_key
89
89
  super if defined?(super)
90
90
  end
91
91
 
@@ -337,7 +337,6 @@ module Rodauth
337
337
  def sms_disable
338
338
  sms_ds.delete
339
339
  @sms = nil
340
- super if defined?(super)
341
340
  end
342
341
 
343
342
  def sms_confirm_failure
@@ -125,7 +125,7 @@ module Rodauth
125
125
  return true if two_factor_authenticated?
126
126
 
127
127
  # True if authenticated via single factor and 2nd factor not setup
128
- !two_factor_authentication_setup?
128
+ !uses_two_factor_authentication?
129
129
  end
130
130
 
131
131
  def require_authentication
@@ -134,7 +134,7 @@ module Rodauth
134
134
  # Avoid database query if already authenticated via 2nd factor
135
135
  return if two_factor_authenticated?
136
136
 
137
- require_two_factor_authenticated if two_factor_authentication_setup?
137
+ require_two_factor_authenticated if uses_two_factor_authentication?
138
138
  end
139
139
 
140
140
  def require_two_factor_setup
@@ -208,11 +208,11 @@ module Rodauth
208
208
  end
209
209
 
210
210
  def _two_factor_setup_links
211
- (super if defined?(super)) || []
211
+ []
212
212
  end
213
213
 
214
214
  def _two_factor_remove_links
215
- (super if defined?(super)) || []
215
+ []
216
216
  end
217
217
 
218
218
  def _two_factor_remove_all_from_session
@@ -70,6 +70,7 @@ module Rodauth
70
70
  end
71
71
 
72
72
  r.post do
73
+ verified = false
73
74
  if account_from_login(param(login_param)) && allow_resending_verify_account_email?
74
75
  if verify_account_email_recently_sent?
75
76
  set_redirect_error_flash verify_account_email_recently_sent_error_flash
@@ -79,8 +80,11 @@ module Rodauth
79
80
  before_verify_account_email_resend
80
81
  if verify_account_email_resend
81
82
  after_verify_account_email_resend
83
+ verified = true
82
84
  end
85
+ end
83
86
 
87
+ if verified
84
88
  set_notice_flash verify_account_email_sent_notice_flash
85
89
  else
86
90
  set_redirect_error_status(no_matching_login_error_status)
@@ -94,6 +98,7 @@ module Rodauth
94
98
  route do |r|
95
99
  verify_account_check_already_logged_in
96
100
  before_verify_account_route
101
+ @password_field_autocomplete_value = 'new-password'
97
102
 
98
103
  r.get do
99
104
  if key = param_or_nil(verify_account_key_param)
@@ -23,7 +23,7 @@ module Rodauth
23
23
  end
24
24
 
25
25
  def open_account?
26
- super || account_in_unverified_grace_period?
26
+ super || (account_in_unverified_grace_period? && has_password?)
27
27
  end
28
28
 
29
29
  def verify_account_set_password?
@@ -53,10 +53,7 @@ module Rodauth
53
53
  end
54
54
 
55
55
  def allow_email_auth?
56
- if defined?(super)
57
- return false unless super
58
- end
59
- !account_in_unverified_grace_period?
56
+ (defined?(super) ? super : true) && !account_in_unverified_grace_period?
60
57
  end
61
58
 
62
59
  def verify_account_check_already_logged_in
@@ -75,6 +72,7 @@ module Rodauth
75
72
  end
76
73
 
77
74
  def account_in_unverified_grace_period?
75
+ account || account_from_session
78
76
  account[account_status_column] == account_unverified_status_value &&
79
77
  verify_account_grace_period &&
80
78
  !verify_account_ds.where(Sequel.date_add(verification_requested_at_column, :seconds=>verify_account_grace_period) > Sequel::CURRENT_TIMESTAMP).empty?
@@ -8,6 +8,7 @@ module Rodauth
8
8
  error_flash "Unable to change login as there is already an account with the new login", 'verify_login_change_duplicate_account'
9
9
  error_flash "There was an error verifying your login change: invalid verify login change key", 'no_matching_verify_login_change_key'
10
10
  notice_flash "Your login change has been verified"
11
+ notice_flash "An email has been sent to you with a link to verify your login change", 'change_login_needs_verification'
11
12
  loaded_templates %w'verify-login-change verify-login-change-email'
12
13
  view 'verify-login-change', 'Verify Login Change'
13
14
  additional_form_tags
@@ -131,7 +132,7 @@ module Rodauth
131
132
  end
132
133
 
133
134
  def change_login_notice_flash
134
- "An email has been sent to you with a link to verify your login change"
135
+ change_login_needs_verification_notice_flash
135
136
  end
136
137
 
137
138
  def verify_login_change_old_login
@@ -377,9 +377,7 @@ module Rodauth
377
377
  end
378
378
 
379
379
  def remove_webauthn_key(webauthn_id)
380
- ret = webauthn_keys_ds.where(webauthn_keys_webauthn_id_column=>webauthn_id).delete == 1
381
- super if defined?(super)
382
- ret
380
+ webauthn_keys_ds.where(webauthn_keys_webauthn_id_column=>webauthn_id).delete == 1
383
381
  end
384
382
 
385
383
  def remove_all_webauthn_keys_and_user_ids
@@ -22,7 +22,7 @@ module Rodauth
22
22
 
23
23
  webauthn_credential = webauthn_auth_credential_from_form_submission
24
24
  before_webauthn_login
25
- _login('webauthn') do
25
+ login('webauthn') do
26
26
  webauthn_update_session(webauthn_credential.id)
27
27
  end
28
28
  end
@@ -9,9 +9,14 @@ module Rodauth
9
9
  case db.database_type
10
10
  when :postgres
11
11
  search_path = opts[:search_path] || 'public, pg_temp'
12
+ primary_key_type =
13
+ case db.schema(table_name).find { |row| row.first == :id }[1][:db_type]
14
+ when 'uuid' then :uuid
15
+ else :int8
16
+ end
12
17
 
13
18
  db.run <<END
14
- CREATE OR REPLACE FUNCTION #{get_salt_name}(acct_id int8) RETURNS text AS $$
19
+ CREATE OR REPLACE FUNCTION #{get_salt_name}(acct_id #{primary_key_type}) RETURNS text AS $$
15
20
  DECLARE salt text;
16
21
  BEGIN
17
22
  SELECT substr(password_hash, 0, 30) INTO salt
@@ -25,7 +30,7 @@ SET search_path = #{search_path};
25
30
  END
26
31
 
27
32
  db.run <<END
28
- CREATE OR REPLACE FUNCTION #{valid_hash_name}(acct_id int8, hash text) RETURNS boolean AS $$
33
+ CREATE OR REPLACE FUNCTION #{valid_hash_name}(acct_id #{primary_key_type}, hash text) RETURNS boolean AS $$
29
34
  DECLARE valid boolean;
30
35
  BEGIN
31
36
  SELECT password_hash = hash INTO valid
@@ -100,13 +105,19 @@ END
100
105
  end
101
106
 
102
107
  def self.drop_database_authentication_functions(db, opts={})
108
+ table_name = opts[:table_name] || :account_password_hashes
103
109
  get_salt_name = opts[:get_salt_name] || :rodauth_get_salt
104
110
  valid_hash_name = opts[:valid_hash_name] || :rodauth_valid_password_hash
105
111
 
106
112
  case db.database_type
107
113
  when :postgres
108
- db.run "DROP FUNCTION #{get_salt_name}(int8)"
109
- db.run "DROP FUNCTION #{valid_hash_name}(int8, text)"
114
+ primary_key_type =
115
+ case db.schema(table_name).find { |row| row.first == :id }[1][:db_type]
116
+ when 'uuid' then :uuid
117
+ else :int8
118
+ end
119
+ db.run "DROP FUNCTION #{get_salt_name}(#{primary_key_type})"
120
+ db.run "DROP FUNCTION #{valid_hash_name}(#{primary_key_type}, text)"
110
121
  when :mysql, :mssql
111
122
  db.run "DROP FUNCTION #{get_salt_name}"
112
123
  db.run "DROP FUNCTION #{valid_hash_name}"
@@ -118,6 +129,6 @@ END
118
129
  end
119
130
 
120
131
  def self.drop_database_previous_password_check_functions(db, opts={})
121
- drop_database_authentication_functions(db, {:get_salt_name=>:rodauth_get_previous_salt, :valid_hash_name=>:rodauth_previous_password_hash_match}.merge(opts))
132
+ drop_database_authentication_functions(db, {:table_name=>:account_previous_password_hashes, :get_salt_name=>:rodauth_get_previous_salt, :valid_hash_name=>:rodauth_previous_password_hash_match}.merge(opts))
122
133
  end
123
134
  end
@@ -6,7 +6,7 @@ module Rodauth
6
6
  MAJOR = 2
7
7
 
8
8
  # The minor version of Rodauth, updated for new feature releases of Rodauth.
9
- MINOR = 0
9
+ MINOR = 5
10
10
 
11
11
  # The patch version of Rodauth, updated only for bug fixes from the last
12
12
  # feature release.
@@ -1,4 +1,4 @@
1
1
  <div class="form-group">
2
2
  <label for="password">#{rodauth.password_label}#{rodauth.input_field_label_suffix}</label>
3
- #{rodauth.input_field_string(rodauth.password_param, 'password', :type => 'password', :autocomplete=>'current-password')}
3
+ #{rodauth.input_field_string(rodauth.password_param, 'password', :type => 'password', :autocomplete=>rodauth.password_field_autocomplete_value)}
4
4
  </div>