rodauth 2.12.0 → 2.16.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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +30 -0
  3. data/README.rdoc +50 -7
  4. data/doc/base.rdoc +1 -0
  5. data/doc/error_reasons.rdoc +73 -0
  6. data/doc/internal_request.rdoc +463 -0
  7. data/doc/path_class_methods.rdoc +10 -0
  8. data/doc/release_notes/2.13.0.txt +19 -0
  9. data/doc/release_notes/2.14.0.txt +17 -0
  10. data/doc/release_notes/2.15.0.txt +48 -0
  11. data/doc/release_notes/2.16.0.txt +20 -0
  12. data/doc/remember.rdoc +1 -0
  13. data/lib/rodauth/features/active_sessions.rb +1 -1
  14. data/lib/rodauth/features/base.rb +26 -1
  15. data/lib/rodauth/features/change_login.rb +6 -4
  16. data/lib/rodauth/features/change_password.rb +5 -3
  17. data/lib/rodauth/features/close_account.rb +3 -1
  18. data/lib/rodauth/features/confirm_password.rb +2 -2
  19. data/lib/rodauth/features/create_account.rb +6 -4
  20. data/lib/rodauth/features/disallow_common_passwords.rb +1 -1
  21. data/lib/rodauth/features/disallow_password_reuse.rb +1 -1
  22. data/lib/rodauth/features/email_auth.rb +6 -0
  23. data/lib/rodauth/features/internal_request.rb +371 -0
  24. data/lib/rodauth/features/jwt_refresh.rb +1 -1
  25. data/lib/rodauth/features/lockout.rb +15 -4
  26. data/lib/rodauth/features/login.rb +6 -3
  27. data/lib/rodauth/features/login_password_requirements_base.rb +15 -6
  28. data/lib/rodauth/features/otp.rb +13 -6
  29. data/lib/rodauth/features/password_complexity.rb +4 -4
  30. data/lib/rodauth/features/path_class_methods.rb +22 -0
  31. data/lib/rodauth/features/recovery_codes.rb +6 -2
  32. data/lib/rodauth/features/remember.rb +25 -10
  33. data/lib/rodauth/features/reset_password.rb +8 -4
  34. data/lib/rodauth/features/session_expiration.rb +1 -0
  35. data/lib/rodauth/features/single_session.rb +1 -0
  36. data/lib/rodauth/features/sms_codes.rb +17 -5
  37. data/lib/rodauth/features/two_factor_base.rb +6 -1
  38. data/lib/rodauth/features/verify_account.rb +8 -1
  39. data/lib/rodauth/features/verify_account_grace_period.rb +1 -1
  40. data/lib/rodauth/features/verify_login_change.rb +5 -2
  41. data/lib/rodauth/features/webauthn.rb +15 -14
  42. data/lib/rodauth/features/webauthn_login.rb +1 -1
  43. data/lib/rodauth/version.rb +1 -1
  44. data/lib/rodauth.rb +20 -2
  45. data/templates/button.str +1 -1
  46. data/templates/change-password.str +2 -2
  47. data/templates/global-logout-field.str +1 -1
  48. data/templates/login-confirm-field.str +2 -2
  49. data/templates/login-display.str +2 -2
  50. data/templates/login-field.str +2 -2
  51. data/templates/otp-auth-code-field.str +2 -2
  52. data/templates/otp-setup.str +2 -2
  53. data/templates/password-confirm-field.str +2 -2
  54. data/templates/password-field.str +2 -2
  55. data/templates/recovery-auth.str +2 -2
  56. data/templates/remember.str +1 -1
  57. data/templates/sms-code-field.str +2 -2
  58. data/templates/sms-setup.str +2 -2
  59. data/templates/webauthn-remove.str +1 -1
  60. metadata +19 -3
@@ -135,7 +135,7 @@ module Rodauth
135
135
  end
136
136
 
137
137
  def _jwt_decode_opts
138
- if allow_refresh_with_expired_jwt_access_token? && @jwt_refresh_route
138
+ if allow_refresh_with_expired_jwt_access_token? && (@jwt_refresh_route || request.path == jwt_refresh_path)
139
139
  Hash[super].merge!(:verify_expiration=>false)
140
140
  else
141
141
  super
@@ -62,6 +62,10 @@ module Rodauth
62
62
  )
63
63
  auth_private_methods :account_from_unlock_key
64
64
 
65
+ internal_request_method(:lock_account)
66
+ internal_request_method(:unlock_account_request)
67
+ internal_request_method(:unlock_account)
68
+
65
69
  route(:unlock_account_request) do |r|
66
70
  check_already_logged_in
67
71
  before_unlock_account_request_route
@@ -84,6 +88,7 @@ module Rodauth
84
88
  set_notice_flash unlock_account_request_notice_flash
85
89
  else
86
90
  set_redirect_error_status(no_matching_login_error_status)
91
+ set_error_reason :no_matching_login
87
92
  set_redirect_error_flash no_matching_login_message.to_s.capitalize
88
93
  end
89
94
 
@@ -116,6 +121,7 @@ module Rodauth
116
121
  key = session[unlock_account_session_key] || param(unlock_account_key_param)
117
122
  unless account_from_unlock_key(key)
118
123
  set_redirect_error_status invalid_key_error_status
124
+ set_error_reason :invalid_unlock_account_key
119
125
  set_redirect_error_flash no_matching_unlock_account_key_error_flash
120
126
  redirect unlock_account_request_redirect
121
127
  end
@@ -134,7 +140,7 @@ module Rodauth
134
140
  set_notice_flash unlock_account_notice_flash
135
141
  redirect unlock_account_redirect
136
142
  else
137
- set_response_error_status(invalid_password_error_status)
143
+ set_response_error_reason_status(:invalid_password, invalid_password_error_status)
138
144
  set_field_error(password_param, invalid_password_message)
139
145
  set_error_flash unlock_account_error_flash
140
146
  unlock_account_view
@@ -165,6 +171,12 @@ module Rodauth
165
171
  unlock_account
166
172
  end
167
173
 
174
+ def _setup_account_lockouts_hash(account_id, key)
175
+ hash = {account_lockouts_id_column=>account_id, account_lockouts_key_column=>key}
176
+ set_deadline_value(hash, account_lockouts_deadline_column, account_lockouts_deadline_interval)
177
+ hash
178
+ end
179
+
168
180
  def invalid_login_attempted
169
181
  ds = account_login_failures_ds.
170
182
  where(account_login_failures_id_column=>account_id)
@@ -190,8 +202,7 @@ module Rodauth
190
202
 
191
203
  if number >= max_invalid_logins
192
204
  @unlock_account_key_value = generate_unlock_account_key
193
- hash = {account_lockouts_id_column=>account_id, account_lockouts_key_column=>unlock_account_key_value}
194
- set_deadline_value(hash, account_lockouts_deadline_column, account_lockouts_deadline_interval)
205
+ hash = _setup_account_lockouts_hash(account_id, unlock_account_key_value)
195
206
 
196
207
  if e = raised_uniqueness_violation{account_lockouts_ds.insert(hash)}
197
208
  # If inserting into the lockout table raises a violation, we should just be able to pull the already inserted
@@ -271,7 +282,7 @@ module Rodauth
271
282
  end
272
283
 
273
284
  def show_lockout_page
274
- set_response_error_status lockout_error_status
285
+ set_response_error_reason_status(:account_locked_out, lockout_error_status)
275
286
  set_error_flash login_lockout_error_flash
276
287
  response.write unlock_account_request_view
277
288
  request.halt
@@ -25,6 +25,9 @@ module Rodauth
25
25
 
26
26
  auth_value_methods :login_return_to_requested_location_path
27
27
 
28
+ internal_request_method
29
+ internal_request_method :valid_login_and_password?
30
+
28
31
  route do |r|
29
32
  check_already_logged_in
30
33
  before_login_route
@@ -39,13 +42,13 @@ module Rodauth
39
42
 
40
43
  catch_error do
41
44
  unless account_from_login(param(login_param))
42
- throw_error_status(no_matching_login_error_status, login_param, no_matching_login_message)
45
+ throw_error_reason(:no_matching_login, no_matching_login_error_status, login_param, no_matching_login_message)
43
46
  end
44
47
 
45
48
  before_login_attempt
46
49
 
47
50
  unless open_account?
48
- throw_error_status(unopen_account_error_status, login_param, unverified_account_message)
51
+ throw_error_reason(:unverified_account, unopen_account_error_status, login_param, unverified_account_message)
49
52
  end
50
53
 
51
54
  if use_multi_phase_login?
@@ -61,7 +64,7 @@ module Rodauth
61
64
 
62
65
  unless password_match?(param(password_param))
63
66
  after_login_failure
64
- throw_error_status(login_error_status, password_param, invalid_password_message)
67
+ throw_error_reason(:invalid_password, login_error_status, password_param, invalid_password_message)
65
68
  end
66
69
 
67
70
  login('password')
@@ -81,6 +81,11 @@ module Rodauth
81
81
  def password_too_short_message
82
82
  "minimum #{password_minimum_length} characters"
83
83
  end
84
+
85
+ def set_password_requirement_error_message(reason, message)
86
+ set_error_reason(reason)
87
+ @password_requirement_message = message
88
+ end
84
89
 
85
90
  def login_does_not_meet_requirements_message
86
91
  "invalid login#{", #{login_requirement_message}" if login_requirement_message}"
@@ -93,13 +98,18 @@ module Rodauth
93
98
  def login_too_short_message
94
99
  "minimum #{login_minimum_length} characters"
95
100
  end
101
+
102
+ def set_login_requirement_error_message(reason, message)
103
+ set_error_reason(reason)
104
+ @login_requirement_message = message
105
+ end
96
106
 
97
107
  def login_meets_length_requirements?(login)
98
108
  if login_minimum_length > login.length
99
- @login_requirement_message = login_too_short_message
109
+ set_login_requirement_error_message(:login_too_short, login_too_short_message)
100
110
  false
101
111
  elsif login_maximum_length < login.length
102
- @login_requirement_message = login_too_long_message
112
+ set_login_requirement_error_message(:login_too_long, login_too_long_message)
103
113
  false
104
114
  else
105
115
  true
@@ -109,7 +119,7 @@ module Rodauth
109
119
  def login_meets_email_requirements?(login)
110
120
  return true unless require_email_address_logins?
111
121
  return true if login_valid_email?(login)
112
- @login_requirement_message = login_not_valid_email_message
122
+ set_login_requirement_error_message(:login_not_valid_email, login_not_valid_email_message)
113
123
  return false
114
124
  end
115
125
 
@@ -119,13 +129,13 @@ module Rodauth
119
129
 
120
130
  def password_meets_length_requirements?(password)
121
131
  return true if password_minimum_length <= password.length
122
- @password_requirement_message = password_too_short_message
132
+ set_password_requirement_error_message(:password_too_short, password_too_short_message)
123
133
  false
124
134
  end
125
135
 
126
136
  def password_does_not_contain_null_byte?(password)
127
137
  return true unless password.include?("\0")
128
- @password_requirement_message = contains_null_byte_message
138
+ set_password_requirement_error_message(:password_contains_null_byte, contains_null_byte_message)
129
139
  false
130
140
  end
131
141
 
@@ -150,4 +160,3 @@ module Rodauth
150
160
  end
151
161
  end
152
162
  end
153
-
@@ -96,6 +96,12 @@ module Rodauth
96
96
  :otp_tmp_key
97
97
  )
98
98
 
99
+ internal_request_method :otp_setup_params
100
+ internal_request_method :otp_setup
101
+ internal_request_method :otp_auth
102
+ internal_request_method :valid_otp_auth?
103
+ internal_request_method :otp_disable
104
+
99
105
  route(:otp_auth) do |r|
100
106
  require_login
101
107
  require_account_session
@@ -103,7 +109,7 @@ module Rodauth
103
109
  require_otp_setup
104
110
 
105
111
  if otp_locked_out?
106
- set_response_error_status(lockout_error_status)
112
+ set_response_error_reason_status(:otp_locked_out, lockout_error_status)
107
113
  set_redirect_error_flash otp_lockout_error_flash
108
114
  redirect otp_lockout_redirect
109
115
  end
@@ -122,7 +128,7 @@ module Rodauth
122
128
 
123
129
  otp_record_authentication_failure
124
130
  after_otp_authentication_failure
125
- set_response_error_status(invalid_key_error_status)
131
+ set_response_error_reason_status(:invalid_otp_auth_code, invalid_key_error_status)
126
132
  set_field_error(otp_auth_param, otp_invalid_auth_code_message)
127
133
  set_error_flash otp_auth_error_flash
128
134
  otp_auth_view
@@ -149,7 +155,7 @@ module Rodauth
149
155
  catch_error do
150
156
  unless otp_valid_key?(secret)
151
157
  otp_tmp_key(otp_new_secret)
152
- throw_error_status(invalid_field_error_status, otp_setup_param, otp_invalid_secret_message)
158
+ throw_error_reason(:invalid_otp_secret, invalid_field_error_status, otp_setup_param, otp_invalid_secret_message)
153
159
  end
154
160
 
155
161
  if otp_keys_use_hmac?
@@ -159,11 +165,11 @@ module Rodauth
159
165
  end
160
166
 
161
167
  unless two_factor_password_match?(param(password_param))
162
- throw_error_status(invalid_password_error_status, password_param, invalid_password_message)
168
+ throw_error_reason(:invalid_password, invalid_password_error_status, password_param, invalid_password_message)
163
169
  end
164
170
 
165
171
  unless otp_valid_code?(param(otp_auth_param))
166
- throw_error_status(invalid_key_error_status, otp_auth_param, otp_invalid_auth_code_message)
172
+ throw_error_reason(:invalid_otp_auth_code, invalid_key_error_status, otp_auth_param, otp_invalid_auth_code_message)
167
173
  end
168
174
 
169
175
  transaction do
@@ -206,7 +212,7 @@ module Rodauth
206
212
  redirect otp_disable_redirect
207
213
  end
208
214
 
209
- set_response_error_status(invalid_password_error_status)
215
+ set_response_error_reason_status(:invalid_password, invalid_password_error_status)
210
216
  set_field_error(password_param, invalid_password_message)
211
217
  set_error_flash otp_disable_error_flash
212
218
  otp_disable_view
@@ -226,6 +232,7 @@ module Rodauth
226
232
  def require_otp_setup
227
233
  unless otp_exists?
228
234
  set_redirect_error_status(two_factor_not_setup_error_status)
235
+ set_error_reason :two_factor_not_setup
229
236
  set_redirect_error_flash two_factor_not_setup_error_flash
230
237
  redirect two_factor_need_setup_redirect
231
238
  end
@@ -54,21 +54,21 @@ module Rodauth
54
54
  def password_has_enough_character_groups?(password)
55
55
  return true if password.length > password_max_length_for_groups_check
56
56
  return true if password_character_groups.select{|re| password =~ re}.length >= password_min_groups
57
- @password_requirement_message = password_not_enough_character_groups_message
57
+ set_password_requirement_error_message(:not_enough_character_groups_in_password, password_not_enough_character_groups_message)
58
58
  false
59
59
  end
60
60
 
61
61
  def password_has_no_invalid_pattern?(password)
62
62
  return true unless password_invalid_pattern
63
63
  return true if password !~ password_invalid_pattern
64
- @password_requirement_message = password_invalid_pattern_message
64
+ set_password_requirement_error_message(:invalid_password_pattern, password_invalid_pattern_message)
65
65
  false
66
66
  end
67
67
 
68
68
  def password_not_too_many_repeating_characters?(password)
69
69
  return true if password_max_repeating_characters < 2
70
70
  return true if password !~ /(.)(\1){#{password_max_repeating_characters-1}}/
71
- @password_requirement_message = password_too_many_repeating_characters_message
71
+ set_password_requirement_error_message(:too_many_repeating_characters_in_password, password_too_many_repeating_characters_message)
72
72
  false
73
73
  end
74
74
 
@@ -77,7 +77,7 @@ module Rodauth
77
77
  return true unless password =~ /\A(?:\d*)([A-Za-z!@$+|][A-Za-z!@$+|0134578]+[A-Za-z!@$+|])(?:\d*)\z/
78
78
  word = $1.downcase.tr('!@$+|0134578', 'iastloleastb')
79
79
  return true if !dict.include?(word)
80
- @password_requirement_message = password_in_dictionary_message
80
+ set_password_requirement_error_message(:password_in_dictionary, password_in_dictionary_message)
81
81
  false
82
82
  end
83
83
  end
@@ -0,0 +1,22 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Rodauth
4
+ Feature.define(:path_class_methods, :PathClassMethods) do
5
+ def post_configure
6
+ super
7
+
8
+ klass = self.class
9
+ klass.features.each do |feature_name|
10
+ feature = FEATURES[feature_name]
11
+ feature.routes.each do |handle_meth|
12
+ route = handle_meth.to_s.sub(/\Ahandle_/, '')
13
+ path_meth = :"#{route}_path"
14
+ url_meth = :"#{route}_url"
15
+ instance = klass.allocate.freeze
16
+ klass.define_singleton_method(path_meth){|opts={}| instance.send(path_meth, opts)}
17
+ klass.define_singleton_method(url_meth){|opts={}| instance.send(url_meth, opts)}
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -59,6 +59,10 @@ module Rodauth
59
59
  :recovery_code_match?,
60
60
  )
61
61
 
62
+ internal_request_method :recovery_codes
63
+ internal_request_method :recovery_auth
64
+ internal_request_method :valid_recovery_auth?
65
+
62
66
  route(:recovery_auth) do |r|
63
67
  require_login
64
68
  require_account_session
@@ -76,7 +80,7 @@ module Rodauth
76
80
  two_factor_authenticate('recovery_code')
77
81
  end
78
82
 
79
- set_response_error_status(invalid_key_error_status)
83
+ set_response_error_reason_status(:invalid_recovery_code, invalid_key_error_status)
80
84
  set_field_error(recovery_codes_param, invalid_recovery_code_message)
81
85
  set_error_flash invalid_recovery_code_error_flash
82
86
  recovery_auth_view
@@ -119,7 +123,7 @@ module Rodauth
119
123
  set_error_flash view_recovery_codes_error_flash
120
124
  end
121
125
 
122
- set_response_error_status(invalid_password_error_status)
126
+ set_response_error_reason_status(:invalid_password, invalid_password_error_status)
123
127
  set_field_error(password_param, invalid_password_message)
124
128
  recovery_codes_view
125
129
  end
@@ -39,12 +39,17 @@ module Rodauth
39
39
  :generate_remember_key_value,
40
40
  :get_remember_key,
41
41
  :load_memory,
42
+ :remembered_session_id,
42
43
  :logged_in_via_remember_key?,
43
44
  :remember_key_value,
44
45
  :remember_login,
45
46
  :remove_remember_key
46
47
  )
47
48
 
49
+ internal_request_method :remember_setup
50
+ internal_request_method :remember_disable
51
+ internal_request_method :account_id_for_remember_key
52
+
48
53
  route do |r|
49
54
  require_account
50
55
  before_remember_route
@@ -74,36 +79,42 @@ module Rodauth
74
79
  set_notice_flash remember_notice_flash
75
80
  redirect remember_redirect
76
81
  else
77
- set_response_error_status(invalid_field_error_status)
82
+ set_response_error_reason_status(:invalid_remember_param, invalid_field_error_status)
78
83
  set_error_flash remember_error_flash
79
84
  remember_view
80
85
  end
81
86
  end
82
87
  end
83
88
 
84
- def load_memory
85
- return if session[session_key]
86
- return unless cookie = request.cookies[remember_cookie_key]
89
+ def remembered_session_id
90
+ return unless cookie = _get_remember_cookie
87
91
  id, key = cookie.split('_', 2)
88
92
  return unless id && key
89
93
 
90
94
  actual, deadline = active_remember_key_ds(id).get([remember_key_column, remember_deadline_column])
91
- unless actual
92
- forget_login
93
- return
94
- end
95
+ return unless actual
95
96
 
96
97
  if hmac_secret
97
98
  unless valid = timing_safe_eql?(key, compute_hmac(actual))
98
99
  unless raw_remember_token_deadline && raw_remember_token_deadline > convert_timestamp(deadline)
99
- forget_login
100
100
  return
101
101
  end
102
102
  end
103
103
  end
104
104
 
105
105
  unless valid || timing_safe_eql?(key, actual)
106
- forget_login
106
+ return
107
+ end
108
+
109
+ id
110
+ end
111
+
112
+ def load_memory
113
+ return if session[session_key]
114
+
115
+ unless id = remembered_session_id
116
+ # Only set expired cookie if there is already a cookie set.
117
+ forget_login if _get_remember_cookie
107
118
  return
108
119
  end
109
120
 
@@ -180,6 +191,10 @@ module Rodauth
180
191
 
181
192
  private
182
193
 
194
+ def _get_remember_cookie
195
+ request.cookies[remember_cookie_key]
196
+ end
197
+
183
198
  def after_logout
184
199
  forget_login
185
200
  super if defined?(super)
@@ -57,6 +57,9 @@ module Rodauth
57
57
  :account_from_reset_password_key
58
58
  )
59
59
 
60
+ internal_request_method(:reset_password_request)
61
+ internal_request_method
62
+
60
63
  route(:reset_password_request) do |r|
61
64
  check_already_logged_in
62
65
  before_reset_password_request_route
@@ -68,11 +71,11 @@ module Rodauth
68
71
  r.post do
69
72
  catch_error do
70
73
  unless account_from_login(param(login_param))
71
- throw_error_status(no_matching_login_error_status, login_param, no_matching_login_message)
74
+ throw_error_reason(:no_matching_login, no_matching_login_error_status, login_param, no_matching_login_message)
72
75
  end
73
76
 
74
77
  unless open_account?
75
- throw_error_status(unopen_account_error_status, login_param, unverified_account_message)
78
+ throw_error_reason(:unverified_account, unopen_account_error_status, login_param, unverified_account_message)
76
79
  end
77
80
 
78
81
  if reset_password_email_recently_sent?
@@ -123,6 +126,7 @@ module Rodauth
123
126
  key = session[reset_password_session_key] || param(reset_password_key_param)
124
127
  unless account_from_reset_password_key(key)
125
128
  set_redirect_error_status(invalid_key_error_status)
129
+ set_error_reason :invalid_reset_password_key
126
130
  set_redirect_error_flash reset_password_error_flash
127
131
  redirect reset_password_email_sent_redirect
128
132
  end
@@ -130,11 +134,11 @@ module Rodauth
130
134
  password = param(password_param)
131
135
  catch_error do
132
136
  if password_match?(password)
133
- throw_error_status(invalid_field_error_status, password_param, same_as_existing_password_message)
137
+ throw_error_reason(:same_as_existing_password, invalid_field_error_status, password_param, same_as_existing_password_message)
134
138
  end
135
139
 
136
140
  if require_password_confirmation? && password != param(password_confirm_param)
137
- throw_error_status(unmatched_field_error_status, password_param, passwords_do_not_match_message)
141
+ throw_error_reason(:passwords_do_not_match, unmatched_field_error_status, password_param, passwords_do_not_match_message)
138
142
  end
139
143
 
140
144
  unless password_meets_requirements?(password)