rodauth 2.11.0 → 2.15.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +28 -0
  3. data/README.rdoc +29 -7
  4. data/doc/active_sessions.rdoc +4 -0
  5. data/doc/base.rdoc +1 -0
  6. data/doc/error_reasons.rdoc +73 -0
  7. data/doc/internal_request.rdoc +463 -0
  8. data/doc/path_class_methods.rdoc +10 -0
  9. data/doc/release_notes/2.11.0.txt +1 -1
  10. data/doc/release_notes/2.12.0.txt +17 -0
  11. data/doc/release_notes/2.13.0.txt +19 -0
  12. data/doc/release_notes/2.14.0.txt +17 -0
  13. data/doc/release_notes/2.15.0.txt +48 -0
  14. data/doc/remember.rdoc +1 -0
  15. data/lib/rodauth.rb +9 -2
  16. data/lib/rodauth/features/active_sessions.rb +29 -8
  17. data/lib/rodauth/features/base.rb +26 -1
  18. data/lib/rodauth/features/change_login.rb +6 -4
  19. data/lib/rodauth/features/change_password.rb +5 -3
  20. data/lib/rodauth/features/close_account.rb +3 -1
  21. data/lib/rodauth/features/confirm_password.rb +2 -2
  22. data/lib/rodauth/features/create_account.rb +6 -4
  23. data/lib/rodauth/features/disallow_common_passwords.rb +1 -1
  24. data/lib/rodauth/features/disallow_password_reuse.rb +1 -1
  25. data/lib/rodauth/features/email_auth.rb +6 -0
  26. data/lib/rodauth/features/internal_request.rb +367 -0
  27. data/lib/rodauth/features/jwt_refresh.rb +1 -1
  28. data/lib/rodauth/features/lockout.rb +15 -4
  29. data/lib/rodauth/features/login.rb +6 -3
  30. data/lib/rodauth/features/login_password_requirements_base.rb +15 -6
  31. data/lib/rodauth/features/otp.rb +13 -6
  32. data/lib/rodauth/features/password_complexity.rb +4 -4
  33. data/lib/rodauth/features/path_class_methods.rb +22 -0
  34. data/lib/rodauth/features/recovery_codes.rb +6 -2
  35. data/lib/rodauth/features/remember.rb +25 -10
  36. data/lib/rodauth/features/reset_password.rb +8 -4
  37. data/lib/rodauth/features/session_expiration.rb +1 -0
  38. data/lib/rodauth/features/single_session.rb +1 -0
  39. data/lib/rodauth/features/sms_codes.rb +17 -5
  40. data/lib/rodauth/features/two_factor_base.rb +6 -1
  41. data/lib/rodauth/features/verify_account.rb +8 -1
  42. data/lib/rodauth/features/verify_account_grace_period.rb +1 -1
  43. data/lib/rodauth/features/verify_login_change.rb +5 -2
  44. data/lib/rodauth/features/webauthn.rb +15 -14
  45. data/lib/rodauth/features/webauthn_login.rb +1 -1
  46. data/lib/rodauth/version.rb +1 -1
  47. data/templates/button.str +1 -1
  48. data/templates/change-password.str +2 -2
  49. data/templates/global-logout-field.str +1 -1
  50. data/templates/login-confirm-field.str +2 -2
  51. data/templates/login-display.str +2 -2
  52. data/templates/login-field.str +2 -2
  53. data/templates/otp-auth-code-field.str +2 -2
  54. data/templates/otp-setup.str +2 -2
  55. data/templates/password-confirm-field.str +2 -2
  56. data/templates/password-field.str +2 -2
  57. data/templates/recovery-auth.str +2 -2
  58. data/templates/remember.str +1 -1
  59. data/templates/sms-code-field.str +2 -2
  60. data/templates/sms-setup.str +2 -2
  61. data/templates/unlock-account-email.str +1 -1
  62. data/templates/webauthn-remove.str +1 -1
  63. 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)