rodauth 2.11.0 → 2.15.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 (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
@@ -27,6 +27,8 @@ module Rodauth
27
27
  :new_account
28
28
  )
29
29
 
30
+ internal_request_method
31
+
30
32
  route do |r|
31
33
  check_already_logged_in
32
34
  before_create_account_route
@@ -43,7 +45,7 @@ module Rodauth
43
45
 
44
46
  catch_error do
45
47
  if require_login_confirmation? && login != param(login_confirm_param)
46
- throw_error_status(unmatched_field_error_status, login_param, logins_do_not_match_message)
48
+ throw_error_reason(:logins_do_not_match, unmatched_field_error_status, login_param, logins_do_not_match_message)
47
49
  end
48
50
 
49
51
  unless login_meets_requirements?(login)
@@ -52,11 +54,11 @@ module Rodauth
52
54
 
53
55
  if create_account_set_password?
54
56
  if require_password_confirmation? && password != param(password_confirm_param)
55
- throw_error_status(unmatched_field_error_status, password_param, passwords_do_not_match_message)
57
+ throw_error_reason(:passwords_do_not_match, unmatched_field_error_status, password_param, passwords_do_not_match_message)
56
58
  end
57
59
 
58
60
  unless password_meets_requirements?(password)
59
- throw_error_status(invalid_field_error_status, password_param, password_does_not_meet_requirements_message)
61
+ throw_error_reason(:password_does_not_meet_requirements, invalid_field_error_status, password_param, password_does_not_meet_requirements_message)
60
62
  end
61
63
 
62
64
  if account_password_hash_column
@@ -100,7 +102,7 @@ module Rodauth
100
102
  raised = raises_uniqueness_violation?{id = db[accounts_table].insert(account)}
101
103
 
102
104
  if raised
103
- @login_requirement_message = already_an_account_with_this_login_message
105
+ set_login_requirement_error_message(:already_an_account_with_this_login, already_an_account_with_this_login_message)
104
106
  end
105
107
 
106
108
  if id
@@ -32,7 +32,7 @@ module Rodauth
32
32
 
33
33
  def password_not_one_of_the_most_common?(password)
34
34
  return true unless password_one_of_most_common?(password)
35
- @password_requirement_message = password_is_one_of_the_most_common_message
35
+ set_password_requirement_error_message(:password_is_one_of_the_most_common, password_is_one_of_the_most_common_message)
36
36
  false
37
37
  end
38
38
  end
@@ -65,7 +65,7 @@ module Rodauth
65
65
  end
66
66
 
67
67
  return true unless match
68
- @password_requirement_message = password_same_as_previous_password_message
68
+ set_password_requirement_error_message(:password_same_as_previous_password, password_same_as_previous_password_message)
69
69
  false
70
70
  end
71
71
 
@@ -49,6 +49,10 @@ module Rodauth
49
49
 
50
50
  auth_private_methods :account_from_email_auth_key
51
51
 
52
+ internal_request_method
53
+ internal_request_method :email_auth_request
54
+ internal_request_method :valid_email_auth?
55
+
52
56
  route(:email_auth_request) do |r|
53
57
  check_already_logged_in
54
58
  before_email_auth_request_route
@@ -58,6 +62,7 @@ module Rodauth
58
62
  _email_auth_request
59
63
  else
60
64
  set_redirect_error_status(no_matching_login_error_status)
65
+ set_error_reason :no_matching_login
61
66
  set_redirect_error_flash email_auth_request_error_flash
62
67
  end
63
68
 
@@ -90,6 +95,7 @@ module Rodauth
90
95
  key = session[email_auth_session_key] || param(email_auth_key_param)
91
96
  unless account_from_email_auth_key(key)
92
97
  set_redirect_error_status(invalid_key_error_status)
98
+ set_error_reason :invalid_email_auth_key
93
99
  set_redirect_error_flash email_auth_error_flash
94
100
  redirect email_auth_email_sent_redirect
95
101
  end
@@ -0,0 +1,367 @@
1
+ # frozen-string-literal: true
2
+
3
+ require 'stringio'
4
+
5
+ module Rodauth
6
+ INVALID_DOMAIN = "invalidurl @@.com"
7
+
8
+ class InternalRequestError < StandardError
9
+ attr_accessor :flash
10
+ attr_accessor :reason
11
+ attr_accessor :field_errors
12
+
13
+ def initialize(attrs)
14
+ return super if attrs.is_a?(String)
15
+
16
+ @flash = attrs[:flash]
17
+ @reason = attrs[:reason]
18
+ @field_errors = attrs[:field_errors] || {}
19
+
20
+ super(build_message)
21
+ end
22
+
23
+ private
24
+
25
+ def build_message
26
+ extras = []
27
+ extras << reason if reason
28
+ extras << field_errors unless field_errors.empty?
29
+ extras = (" (#{extras.join(", ")})" unless extras.empty?)
30
+
31
+ "#{flash}#{extras}"
32
+ end
33
+ end
34
+
35
+ module InternalRequestMethods
36
+ attr_accessor :session
37
+ attr_accessor :params
38
+ attr_reader :flash
39
+ attr_accessor :internal_request_block
40
+
41
+ def domain
42
+ d = super
43
+ if d == INVALID_DOMAIN
44
+ raise InternalRequestError, "must set domain in configuration, as it cannot be determined from internal request"
45
+ end
46
+ d
47
+ end
48
+
49
+ def raw_param(k)
50
+ @params[k]
51
+ end
52
+
53
+ def set_error_flash(message)
54
+ @flash = message
55
+ _handle_internal_request_error
56
+ end
57
+ alias set_redirect_error_flash set_error_flash
58
+
59
+ def set_notice_flash(message)
60
+ @flash = message
61
+ end
62
+ alias set_notice_now_flash set_notice_flash
63
+
64
+ def modifications_require_password?
65
+ false
66
+ end
67
+ alias require_login_confirmation? modifications_require_password?
68
+ alias require_password_confirmation? modifications_require_password?
69
+ alias change_login_requires_password? modifications_require_password?
70
+ alias change_password_requires_password? modifications_require_password?
71
+ alias close_account_requires_password? modifications_require_password?
72
+ alias two_factor_modifications_require_password? modifications_require_password?
73
+
74
+ def otp_setup_view
75
+ hash = {:otp_setup=>otp_user_key}
76
+ hash[:otp_setup_raw] = otp_key if hmac_secret
77
+ _return_from_internal_request(hash)
78
+ end
79
+
80
+ def add_recovery_codes_view
81
+ _return_from_internal_request(recovery_codes)
82
+ end
83
+
84
+ def handle_internal_request(meth)
85
+ catch(:halt) do
86
+ _around_rodauth do
87
+ before_rodauth
88
+ send(meth, request)
89
+ end
90
+ end
91
+
92
+ @internal_request_return_value
93
+ end
94
+
95
+ private
96
+
97
+ def internal_request?
98
+ true
99
+ end
100
+
101
+ def set_error_reason(reason)
102
+ @error_reason = reason
103
+ end
104
+
105
+ def after_login
106
+ super
107
+ _set_internal_request_return_value(account_id) unless @return_false_on_error
108
+ end
109
+
110
+ def after_remember
111
+ super
112
+ if params[remember_param] == remember_remember_param_value
113
+ _set_internal_request_return_value("#{account_id}_#{convert_token_key(remember_key_value)}")
114
+ end
115
+ end
116
+
117
+ def after_load_memory
118
+ super
119
+ _return_from_internal_request(session_value)
120
+ end
121
+
122
+ def before_change_password_route
123
+ super
124
+ params[new_password_param] ||= params[password_param]
125
+ end
126
+
127
+ def before_email_auth_request_route
128
+ super
129
+ _set_login_param_from_account
130
+ end
131
+
132
+ def before_login_route
133
+ super
134
+ _set_login_param_from_account
135
+ end
136
+
137
+ def before_unlock_account_request_route
138
+ super
139
+ _set_login_param_from_account
140
+ end
141
+
142
+ def before_reset_password_request_route
143
+ super
144
+ _set_login_param_from_account
145
+ end
146
+
147
+ def before_verify_account_resend_route
148
+ super
149
+ _set_login_param_from_account
150
+ end
151
+
152
+ def account_from_key(token, status_id=nil)
153
+ return super unless session_value
154
+ return unless yield session_value
155
+ ds = account_ds(session_value)
156
+ ds = ds.where(account_status_column=>status_id) if status_id && !skip_status_checks?
157
+ ds.first
158
+ end
159
+
160
+ def _set_internal_request_return_value(value)
161
+ @internal_request_return_value = value
162
+ end
163
+
164
+ def _return_from_internal_request(value)
165
+ _set_internal_request_return_value(value)
166
+ throw(:halt)
167
+ end
168
+
169
+ def _handle_internal_request_error
170
+ if @return_false_on_error
171
+ _return_from_internal_request(false)
172
+ else
173
+ raise InternalRequestError.new(flash: @flash, reason: @error_reason, field_errors: @field_errors)
174
+ end
175
+ end
176
+
177
+ def _return_false_on_error!
178
+ @return_false_on_error = true
179
+ end
180
+
181
+ def _set_login_param_from_account
182
+ if session_value && !params[login_param] && (account = account_ds(session_value).first)
183
+ params[login_param] = account[login_column]
184
+ end
185
+ end
186
+
187
+ def _get_remember_cookie
188
+ params[remember_param]
189
+ end
190
+
191
+ def _handle_internal_request_eval(_)
192
+ v = instance_eval(&internal_request_block)
193
+ _set_internal_request_return_value(v) unless defined?(@internal_request_return_value)
194
+ end
195
+
196
+ def _handle_account_id_for_login(_)
197
+ raise InternalRequestError, "no login provided" unless login = param_or_nil(login_param)
198
+ raise InternalRequestError, "no account for login" unless account = account_from_login(login)
199
+ _return_from_internal_request(account[account_id_column])
200
+ end
201
+
202
+ def _handle_account_exists?(_)
203
+ raise InternalRequestError, "no login provided" unless login = param_or_nil(login_param)
204
+ _return_from_internal_request(!!account_from_login(login))
205
+ end
206
+
207
+ def _handle_lock_account(_)
208
+ raised_uniqueness_violation{account_lockouts_ds(session_value).insert(_setup_account_lockouts_hash(session_value, generate_unlock_account_key))}
209
+ end
210
+
211
+ def _handle_remember_setup(request)
212
+ params[remember_param] = remember_remember_param_value
213
+ _handle_remember(request)
214
+ end
215
+
216
+ def _handle_remember_disable(request)
217
+ params[remember_param] = remember_disable_param_value
218
+ _handle_remember(request)
219
+ end
220
+
221
+ def _handle_account_id_for_remember_key(request)
222
+ load_memory
223
+ raise InternalRequestError, "invalid remember key"
224
+ end
225
+
226
+ def _handle_otp_setup_params(request)
227
+ request.env['REQUEST_METHOD'] = 'GET'
228
+ _handle_otp_setup(request)
229
+ end
230
+
231
+ def _predicate_internal_request(meth, request)
232
+ _return_false_on_error!
233
+ _set_internal_request_return_value(true)
234
+ send(meth, request)
235
+ end
236
+
237
+ def _handle_valid_login_and_password?(request)
238
+ _predicate_internal_request(:_handle_login, request)
239
+ end
240
+
241
+ def _handle_valid_email_auth?(request)
242
+ _predicate_internal_request(:_handle_email_auth, request)
243
+ end
244
+
245
+ def _handle_valid_otp_auth?(request)
246
+ _predicate_internal_request(:_handle_otp_auth, request)
247
+ end
248
+
249
+ def _handle_valid_recovery_auth?(request)
250
+ _predicate_internal_request(:_handle_recovery_auth, request)
251
+ end
252
+
253
+ def _handle_valid_sms_auth?(request)
254
+ _predicate_internal_request(:_handle_sms_auth, request)
255
+ end
256
+ end
257
+
258
+ module InternalRequestClassMethods
259
+ def internal_request(route, opts={}, &block)
260
+ opts = opts.dup
261
+
262
+ env = {
263
+ 'REQUEST_METHOD'=>'POST',
264
+ 'PATH_INFO'=>'/',
265
+ "SCRIPT_NAME" => "",
266
+ "HTTP_HOST" => INVALID_DOMAIN,
267
+ "SERVER_NAME" => INVALID_DOMAIN,
268
+ "SERVER_PORT" => 443,
269
+ "CONTENT_TYPE" => "application/x-www-form-urlencoded",
270
+ "rack.input"=>StringIO.new(''),
271
+ "rack.url_scheme"=>"https"
272
+ }
273
+ env.merge!(opts.delete(:env)) if opts[:env]
274
+
275
+ session = {}
276
+ session.merge!(opts.delete(:session)) if opts[:session]
277
+
278
+ params = {}
279
+ params.merge!(opts.delete(:params)) if opts[:params]
280
+
281
+ scope = roda_class.new(env)
282
+ rodauth = new(scope)
283
+ rodauth.session = session
284
+ rodauth.params = params
285
+ rodauth.internal_request_block = block
286
+
287
+ unless account_id = opts.delete(:account_id)
288
+ if (account_login = opts.delete(:account_login))
289
+ if (account = rodauth.send(:_account_from_login, account_login))
290
+ account_id = account[rodauth.account_id_column]
291
+ else
292
+ raise InternalRequestError, "no account for login: #{account_login.inspect}"
293
+ end
294
+ end
295
+ end
296
+
297
+ if account_id
298
+ session[rodauth.session_key] = account_id
299
+ unless authenticated_by = opts.delete(:authenticated_by)
300
+ authenticated_by = case route
301
+ when :otp_auth, :sms_request, :sms_auth, :recovery_auth, :valid_otp_auth?, :valid_sms_auth?, :valid_recovery_auth?
302
+ ['internal1']
303
+ else
304
+ ['internal1', 'internal2']
305
+ end
306
+ end
307
+ session[rodauth.authenticated_by_session_key] = authenticated_by
308
+ end
309
+
310
+ opts.keys.each do |k|
311
+ meth = :"#{k}_param"
312
+ params[rodauth.public_send(meth).to_s] = opts.delete(k) if rodauth.respond_to?(meth)
313
+ end
314
+
315
+ unless opts.empty?
316
+ warn "unhandled options passed to #{route}: #{opts.inspect}"
317
+ end
318
+
319
+ rodauth.handle_internal_request(:"_handle_#{route}")
320
+ end
321
+ end
322
+
323
+ Feature.define(:internal_request, :InternalRequest) do
324
+ configuration_module_eval do
325
+ def internal_request_configuration(&block)
326
+ @auth.instance_exec do
327
+ (@internal_request_configuration_blocks ||= []) << block
328
+ end
329
+ end
330
+ end
331
+
332
+ def post_configure
333
+ super
334
+
335
+ return if is_a?(InternalRequestMethods)
336
+
337
+ klass = self.class
338
+ internal_class = Class.new(klass) do
339
+ @roda_class = klass.roda_class
340
+ @features = klass.features.clone
341
+ @routes = klass.routes.clone
342
+ @route_hash = klass.route_hash.clone
343
+ @configuration = klass.configuration.clone
344
+ @configuration.instance_variable_set(:@auth, self)
345
+ end
346
+
347
+ if blocks = klass.instance_variable_get(:@internal_request_configuration_blocks)
348
+ configuration = internal_class.configuration
349
+ blocks.each do |block|
350
+ configuration.instance_exec(&block)
351
+ end
352
+ end
353
+ internal_class.send(:extend, InternalRequestClassMethods)
354
+ internal_class.send(:include, InternalRequestMethods)
355
+ internal_class.allocate.post_configure
356
+
357
+ ([:base] + internal_class.features).each do |feature_name|
358
+ feature = FEATURES[feature_name]
359
+ if meths = feature.internal_request_methods
360
+ meths.each do |name|
361
+ klass.define_singleton_method(name){|opts={}, &block| internal_class.internal_request(name, opts, &block)}
362
+ end
363
+ end
364
+ end
365
+ end
366
+ end
367
+ end