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
@@ -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