rodauth 2.43.0 → 2.45.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 (32) hide show
  1. checksums.yaml +4 -4
  2. data/lib/rodauth/features/active_sessions.rb +32 -0
  3. data/lib/rodauth/features/argon2.rb +2 -0
  4. data/lib/rodauth/features/base.rb +52 -11
  5. data/lib/rodauth/features/disallow_password_reuse.rb +2 -0
  6. data/lib/rodauth/features/email_auth.rb +2 -0
  7. data/lib/rodauth/features/http_basic_auth.rb +8 -3
  8. data/lib/rodauth/features/internal_request.rb +18 -3
  9. data/lib/rodauth/features/json.rb +6 -2
  10. data/lib/rodauth/features/jwt.rb +6 -4
  11. data/lib/rodauth/features/jwt_refresh.rb +2 -0
  12. data/lib/rodauth/features/lockout.rb +10 -1
  13. data/lib/rodauth/features/login.rb +12 -2
  14. data/lib/rodauth/features/login_password_requirements_base.rb +2 -0
  15. data/lib/rodauth/features/otp.rb +8 -6
  16. data/lib/rodauth/features/otp_unlock.rb +2 -0
  17. data/lib/rodauth/features/password_grace_period.rb +3 -1
  18. data/lib/rodauth/features/password_pepper.rb +2 -0
  19. data/lib/rodauth/features/path_class_methods.rb +2 -2
  20. data/lib/rodauth/features/recovery_codes.rb +4 -2
  21. data/lib/rodauth/features/remember.rb +3 -1
  22. data/lib/rodauth/features/reset_password.rb +7 -0
  23. data/lib/rodauth/features/sms_codes.rb +15 -7
  24. data/lib/rodauth/features/two_factor_base.rb +2 -0
  25. data/lib/rodauth/features/update_password_hash.rb +2 -0
  26. data/lib/rodauth/features/verify_account.rb +2 -0
  27. data/lib/rodauth/features/verify_login_change.rb +2 -0
  28. data/lib/rodauth/features/webauthn_login.rb +2 -0
  29. data/lib/rodauth/features/webauthn_verify_account.rb +2 -0
  30. data/lib/rodauth/version.rb +1 -1
  31. data/lib/rodauth.rb +111 -12
  32. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ebccee76adf022cf82eca6e36c69f5846353b4499021aec5f410dc7e3b04c65f
4
- data.tar.gz: b9fb907c3430c9b043c8cc361877c191ed8f4ed99388d410d0eb84b66cb57ab5
3
+ metadata.gz: 5dea248f50448b260c6d0007d8bc4054f17794270e857a6310b969ba58105fbf
4
+ data.tar.gz: 875dbda4ddd85b04041a8e854e0adb09b466269ecef74dd1a73f384519d54553
5
5
  SHA512:
6
- metadata.gz: c38cbd45e2ef7a29c4de7c2e9450979a2739bae690af5dd34288684f8c6289423334033e26ecd96fca758cae0ef7f1a6c6af69de36d95651a6c577206721fde5
7
- data.tar.gz: 2c69a447d7d465e76b29f1746dc3f886881f39bb20af47d292093333104ab2e88edf74d4d6262f96f38492d69d284e23719e8cf29823b2a117fd52967c7b79b1
6
+ metadata.gz: dbf7c82f8b1c93496c59225891c1d747f317eaf46d8283f9c6f2d0562a48aaaec4d6491ed7567011356914fb4498e0f86576c230e183cf53c0ac7a7c5f27d99a
7
+ data.tar.gz: c8c2a228ce0395e4e14dbd1a69b01f7038b2ac94267b33bc5695d11b3eae6a278846be5100714dc18b0a65010db1693c0621f9ec1f2d0ba0793e92b61a698bf9
@@ -37,6 +37,8 @@ module Rodauth
37
37
  :remove_inactive_sessions,
38
38
  )
39
39
 
40
+ uses_instance_variables(:@active_sessions_key, :@clear_active_sessions_after_two_factor_setup)
41
+
40
42
  def currently_active_session?
41
43
  return false unless session_id = session[session_id_session_key]
42
44
 
@@ -148,6 +150,21 @@ module Rodauth
148
150
  remove_all_active_sessions
149
151
  end
150
152
 
153
+ def after_otp_setup
154
+ super if defined?(super)
155
+ remove_all_active_sessions_except_current if @clear_active_sessions_after_two_factor_setup
156
+ end
157
+
158
+ def after_sms_confirm
159
+ super if defined?(super)
160
+ remove_all_active_sessions_except_current if @clear_active_sessions_after_two_factor_setup
161
+ end
162
+
163
+ def after_webauthn_setup
164
+ super if defined?(super)
165
+ remove_all_active_sessions_except_current if @clear_active_sessions_after_two_factor_setup
166
+ end
167
+
151
168
  def before_logout
152
169
  if param_or_nil(global_logout_param)
153
170
  remove_remember_key(session_value) if respond_to?(:remove_remember_key)
@@ -158,6 +175,21 @@ module Rodauth
158
175
  super
159
176
  end
160
177
 
178
+ def before_otp_setup
179
+ @clear_active_sessions_after_two_factor_setup = !two_factor_authentication_setup?
180
+ super if defined?(super)
181
+ end
182
+
183
+ def before_sms_confirm
184
+ @clear_active_sessions_after_two_factor_setup = !two_factor_authentication_setup?
185
+ super if defined?(super)
186
+ end
187
+
188
+ def before_webauthn_setup
189
+ @clear_active_sessions_after_two_factor_setup = !two_factor_authentication_setup?
190
+ super if defined?(super)
191
+ end
192
+
161
193
  attr_reader :active_sessions_key
162
194
 
163
195
  def generate_active_sessions_key
@@ -16,6 +16,8 @@ module Rodauth
16
16
  auth_value_method :argon2_secret, nil
17
17
  auth_value_method :use_argon2?, true
18
18
 
19
+ uses_instance_variables(:@update_password_hash)
20
+
19
21
  def password_hash(password)
20
22
  return super unless use_argon2?
21
23
 
@@ -1,11 +1,24 @@
1
1
  # frozen-string-literal: true
2
2
 
3
- require 'rack/request'
4
- require 'rack/utils'
3
+ begin
4
+ require "rack/version"
5
+ # :nocov:
6
+ rescue LoadError
7
+ require "rack"
8
+ else
9
+ if Rack.release >= '3'
10
+ require "rack/request"
11
+ require "rack/utils"
12
+ else
13
+ require "rack"
14
+ end
15
+ end
16
+ # :nocov:
5
17
 
6
18
  module Rodauth
7
19
  Feature.define(:base, :Base) do
8
20
  after 'login'
21
+ after 'no_matching_login'
9
22
  after 'login_failure'
10
23
  before 'login'
11
24
  before 'login_attempt'
@@ -22,7 +35,7 @@ module Rodauth
22
35
  auth_value_method :accounts_table, :accounts
23
36
  auth_value_method :cache_templates, true
24
37
  auth_value_method :check_csrf_block, nil
25
- auth_value_method :check_csrf_opts, {}.freeze
38
+ auth_value_method :check_csrf_opts, OPTS
26
39
  auth_value_method :default_redirect, '/'
27
40
  auth_value_method :convert_token_id_to_integer?, nil
28
41
  flash_key :flash_error_key, :error
@@ -60,9 +73,10 @@ module Rodauth
60
73
  auth_value_method :mark_input_fields_with_inputmode?, true
61
74
  auth_value_method :skip_status_checks?, true
62
75
  translatable_method :strftime_format, '%F %T'
63
- auth_value_method :template_opts, {}.freeze
76
+ auth_value_method :template_opts, OPTS
64
77
  auth_value_method :title_instance_variable, nil
65
78
  auth_value_method :token_separator, "_"
79
+ auth_value_method :transaction_opts, OPTS
66
80
  auth_value_method :unmatched_field_error_status, 422
67
81
  auth_value_method :unopen_account_error_status, 403
68
82
  translatable_method :unverified_account_message, "unverified account, please verify account before logging in"
@@ -94,6 +108,7 @@ module Rodauth
94
108
  :check_csrf,
95
109
  :clear_session,
96
110
  :clear_tokens,
111
+ :convert_token_id,
97
112
  :csrf_tag,
98
113
  :function_name,
99
114
  :hook_action,
@@ -122,7 +137,6 @@ module Rodauth
122
137
  :account_from_id,
123
138
  :account_from_login,
124
139
  :account_from_session,
125
- :convert_token_id,
126
140
  :field_attributes,
127
141
  :field_error_attributes,
128
142
  :formatted_field_error,
@@ -137,14 +151,29 @@ module Rodauth
137
151
  def auth_class_eval(&block)
138
152
  auth.class_eval(&block)
139
153
  end
154
+
155
+ def uses_instance_variables(*ivs)
156
+ auth.define_singleton_method(:instance_variables_used) do
157
+ super() + ivs
158
+ end
159
+ end
140
160
  end
141
161
 
162
+ uses_instance_variables(
163
+ :@account,
164
+ :@current_route,
165
+ :@field_errors,
166
+ :@password_field_autocomplete_value,
167
+ :@has_password
168
+ )
169
+
142
170
  attr_reader :scope
143
171
  attr_reader :account
144
172
  attr_reader :current_route
145
173
 
146
174
  def initialize(scope)
147
175
  @scope = scope
176
+ _initialize_instance_variables
148
177
  end
149
178
 
150
179
  def features
@@ -190,7 +219,7 @@ module Rodauth
190
219
  end
191
220
  end
192
221
 
193
- def input_field_string(param, id, opts={})
222
+ def input_field_string(param, id, opts=OPTS)
194
223
  type = opts.fetch(:type, "text")
195
224
 
196
225
  unless type == "password"
@@ -421,7 +450,7 @@ module Rodauth
421
450
  _template_opts(opts, 'button')
422
451
  end
423
452
 
424
- def button(value, opts={})
453
+ def button(value, opts=OPTS)
425
454
  scope.render(button_opts(value, opts))
426
455
  end
427
456
 
@@ -549,13 +578,16 @@ module Rodauth
549
578
  end
550
579
 
551
580
  def has_password?
552
- return @has_password if defined?(@has_password)
581
+ return @has_password unless @has_password.nil?
553
582
  return false unless account || session_value
554
583
  @has_password = !!get_password_hash
555
584
  end
556
585
 
557
586
  private
558
587
 
588
+ def _initialize_instance_variables
589
+ end
590
+
559
591
  def _around_rodauth
560
592
  yield
561
593
  end
@@ -665,17 +697,18 @@ module Rodauth
665
697
  request.halt
666
698
  end
667
699
 
668
- def route_path(route, opts={})
700
+ def route_path(route, opts=OPTS)
669
701
  path = "#{prefix}/#{route}"
670
702
  path += "?#{Rack::Utils.build_nested_query(opts)}" unless opts.empty?
671
703
  path
672
704
  end
673
705
 
674
- def route_url(route, opts={})
706
+ def route_url(route, opts=OPTS)
675
707
  "#{base_url}#{route_path(route, opts)}"
676
708
  end
677
709
 
678
- def transaction(opts={}, &block)
710
+ def transaction(opts=OPTS, &block)
711
+ opts = opts.empty? ? transaction_opts : transaction_opts.merge(opts)
679
712
  db.transaction(opts, &block)
680
713
  end
681
714
 
@@ -935,6 +968,14 @@ module Rodauth
935
968
  false
936
969
  end
937
970
 
971
+ # :nocov:
972
+ if RUBY_VERSION >= '4.0'
973
+ # :nocov:
974
+ def instance_variables_to_inspect
975
+ instance_variables.reject{|v| instance_variable_get(v).nil?}
976
+ end
977
+ end
978
+
938
979
  def use_scope_clear_session?
939
980
  scope.respond_to?(:clear_session)
940
981
  end
@@ -16,6 +16,8 @@ module Rodauth
16
16
  :password_doesnt_match_previous_password?
17
17
  )
18
18
 
19
+ uses_instance_variables(:@dont_check_previous_password)
20
+
19
21
  def set_password(password)
20
22
  hash = super
21
23
  add_previous_password_hash(hash)
@@ -47,6 +47,8 @@ module Rodauth
47
47
 
48
48
  auth_private_methods :account_from_email_auth_key
49
49
 
50
+ uses_instance_variables(:@email_auth_key_value)
51
+
50
52
  internal_request_method
51
53
  internal_request_method :email_auth_request
52
54
  internal_request_method :valid_email_auth?
@@ -5,10 +5,12 @@ module Rodauth
5
5
  auth_value_method :http_basic_auth_realm, "protected"
6
6
  auth_value_method :require_http_basic_auth?, false
7
7
 
8
+ uses_instance_variables(:@checked_http_basic_auth)
9
+
8
10
  def logged_in?
9
11
  ret = super
10
12
 
11
- if !ret && !defined?(@checked_http_basic_auth)
13
+ if !ret && @checked_http_basic_auth.nil?
12
14
  http_basic_auth
13
15
  ret = super
14
16
  end
@@ -32,9 +34,11 @@ module Rodauth
32
34
  end
33
35
 
34
36
  def http_basic_auth
35
- return @checked_http_basic_auth if defined?(@checked_http_basic_auth)
37
+ unless @checked_http_basic_auth.nil?
38
+ return (@checked_http_basic_auth ? true : nil)
39
+ end
36
40
 
37
- @checked_http_basic_auth = nil
41
+ @checked_http_basic_auth = false
38
42
  return unless token = ((v = request.env['HTTP_AUTHORIZATION']) && v[/\A *Basic (.*)\Z/, 1])
39
43
 
40
44
  username, password = token.unpack("m*").first.split(/:/, 2)
@@ -42,6 +46,7 @@ module Rodauth
42
46
 
43
47
  catch_error do
44
48
  unless account_from_login(username)
49
+ after_no_matching_login
45
50
  throw_basic_auth_error(login_param, no_matching_login_message)
46
51
  end
47
52
 
@@ -187,6 +187,7 @@ module Rodauth
187
187
  end
188
188
 
189
189
  def _set_internal_request_return_value(value)
190
+ @internal_request_return_value_set = true
190
191
  @internal_request_return_value = value
191
192
  end
192
193
 
@@ -219,7 +220,7 @@ module Rodauth
219
220
 
220
221
  def _handle_internal_request_eval(_)
221
222
  v = instance_eval(&internal_request_block)
222
- _set_internal_request_return_value(v) unless defined?(@internal_request_return_value)
223
+ _set_internal_request_return_value(v) unless @internal_request_return_value_set
223
224
  end
224
225
 
225
226
  def _handle_account_id_for_login(_)
@@ -304,7 +305,20 @@ module Rodauth
304
305
  end
305
306
 
306
307
  module InternalRequestClassMethods
307
- def internal_request(route, opts={}, &block)
308
+ def instance_variables_used
309
+ super + [
310
+ :@session,
311
+ :@params,
312
+ :@flash,
313
+ :@internal_request_block,
314
+ :@internal_request_return_value,
315
+ :@internal_request_return_value_set,
316
+ :@error_reason,
317
+ :@return_false_on_error
318
+ ]
319
+ end
320
+
321
+ def internal_request(route, opts=OPTS, &block)
308
322
  opts = opts.dup
309
323
 
310
324
  env = {
@@ -404,13 +418,14 @@ module Rodauth
404
418
 
405
419
  internal_class.send(:extend, InternalRequestClassMethods)
406
420
  internal_class.send(:include, InternalRequestMethods)
421
+ internal_class.send(:make_shape_friendly)
407
422
  internal_class.allocate.post_configure
408
423
 
409
424
  ([:base] + internal_class.features).each do |feature_name|
410
425
  feature = FEATURES[feature_name]
411
426
  if meths = feature.internal_request_methods
412
427
  meths.each do |name|
413
- klass.define_singleton_method(name){|opts={}, &block| internal_class.internal_request(name, opts, &block)}
428
+ klass.define_singleton_method(name){|opts=OPTS, &block| internal_class.internal_request(name, opts, &block)}
414
429
  end
415
430
  end
416
431
  end
@@ -2,6 +2,8 @@
2
2
 
3
3
  module Rodauth
4
4
  Feature.define(:json, :Json) do
5
+ self.allowed_undefined_configuration_methods = [:only_json?].freeze
6
+
5
7
  translatable_method :json_not_accepted_error_message, 'Unsupported Accept header. Must accept "application/json" or compatible content type'
6
8
  translatable_method :json_non_post_error_message, 'non-POST method used in JSON API'
7
9
  auth_value_method :json_accept_regexp, /(?:(?:\*|\bapplication)\/\*|\bapplication\/(?:vnd\.api\+)?json\b)/i
@@ -27,6 +29,8 @@ module Rodauth
27
29
 
28
30
  auth_private_methods :json_response_body
29
31
 
32
+ uses_instance_variables(:@json_request)
33
+
30
34
  def set_field_error(field, message)
31
35
  return super unless use_json?
32
36
  json_response[json_response_field_error_key] = [field, message]
@@ -53,8 +57,8 @@ module Rodauth
53
57
  end
54
58
 
55
59
  def json_request?
56
- return @json_request if defined?(@json_request)
57
- @json_request = request.content_type =~ json_request_content_type_regexp
60
+ return @json_request unless @json_request.nil?
61
+ @json_request = !(request.content_type !~ json_request_content_type_regexp)
58
62
  end
59
63
 
60
64
  def use_json?
@@ -11,7 +11,7 @@ module Rodauth
11
11
  auth_value_method :jwt_algorithm, "HS256"
12
12
  auth_value_method :jwt_authorization_ignore, /\A(?:Basic|Digest) /
13
13
  auth_value_method :jwt_authorization_remove, /\ABearer:?\s+/
14
- auth_value_method :jwt_decode_opts, {}.freeze
14
+ auth_value_method :jwt_decode_opts, OPTS
15
15
  auth_value_method :jwt_old_secret, nil
16
16
  auth_value_method :jwt_session_key, nil
17
17
  auth_value_method :jwt_symbolize_deeply?, false
@@ -30,8 +30,10 @@ module Rodauth
30
30
 
31
31
  def_deprecated_alias :json_check_accept?, :jwt_check_accept?
32
32
 
33
+ uses_instance_variables(:@session, :@jwt_token, :@jwt_payload)
34
+
33
35
  def session
34
- return @session if defined?(@session)
36
+ return @session if @session
35
37
  return super unless use_jwt?
36
38
 
37
39
  s = {}
@@ -76,7 +78,7 @@ module Rodauth
76
78
  end
77
79
 
78
80
  def jwt_token
79
- return @jwt_token if defined?(@jwt_token)
81
+ return @jwt_token if @jwt_token
80
82
 
81
83
  if (v = request.env['HTTP_AUTHORIZATION']) && v !~ jwt_authorization_ignore
82
84
  @jwt_token = v.sub(jwt_authorization_remove, '')
@@ -120,7 +122,7 @@ module Rodauth
120
122
  end
121
123
 
122
124
  def jwt_payload
123
- return @jwt_payload if defined?(@jwt_payload)
125
+ return @jwt_payload unless @jwt_payload.nil?
124
126
  @jwt_payload = JWT.decode(jwt_token, _jwt_decode_secrets, true, _jwt_decode_opts.merge(:algorithm=>jwt_algorithm))[0]
125
127
  rescue JWT::DecodeError => e
126
128
  rescue_jwt_payload(e)
@@ -31,6 +31,8 @@ module Rodauth
31
31
  :account_from_refresh_token
32
32
  )
33
33
 
34
+ uses_instance_variables(:@jwt_refresh_route)
35
+
34
36
  route do |r|
35
37
  @jwt_refresh_route = true
36
38
  before_jwt_refresh_route
@@ -61,6 +61,8 @@ module Rodauth
61
61
  )
62
62
  auth_private_methods :account_from_unlock_key
63
63
 
64
+ uses_instance_variables(:@unlock_account_key_value)
65
+
64
66
  internal_request_method(:lock_account)
65
67
  internal_request_method(:unlock_account_request)
66
68
  internal_request_method(:unlock_account)
@@ -249,7 +251,14 @@ module Rodauth
249
251
 
250
252
  private
251
253
 
252
- attr_reader :unlock_account_key_value
254
+ def unlock_account_key
255
+ @unlock_account_key_value
256
+ end
257
+
258
+ def unlock_account_key_value
259
+ # RODAUTH3: call unlock_account_key directly
260
+ unlock_account_key
261
+ end
253
262
 
254
263
  def before_login_attempt
255
264
  if locked_out?
@@ -20,8 +20,8 @@ module Rodauth
20
20
 
21
21
  session_key :login_redirect_session_key, :login_redirect
22
22
 
23
- auth_cached_method :multi_phase_login_forms
24
- auth_cached_method :login_form_footer
23
+ cached_auth_method :multi_phase_login_forms
24
+ cached_auth_method :login_form_footer
25
25
 
26
26
  auth_value_methods :login_return_to_requested_location_path
27
27
 
@@ -30,6 +30,15 @@ module Rodauth
30
30
  :login_response
31
31
  )
32
32
 
33
+ uses_instance_variables(
34
+ :@login_form_footer,
35
+ :@login_form_footer_links,
36
+ :@login_form_header,
37
+ :@multi_phase_login_forms,
38
+ :@saved_login_redirect,
39
+ :@valid_login_entered
40
+ )
41
+
33
42
  internal_request_method
34
43
  internal_request_method :valid_login_and_password?
35
44
 
@@ -47,6 +56,7 @@ module Rodauth
47
56
 
48
57
  catch_error do
49
58
  unless account_from_login(login_param_value)
59
+ after_no_matching_login
50
60
  throw_error_reason(:no_matching_login, no_matching_login_error_status, login_param, no_matching_login_message)
51
61
  end
52
62
 
@@ -44,6 +44,8 @@ module Rodauth
44
44
  :set_password
45
45
  )
46
46
 
47
+ uses_instance_variables(:@password_requirement_message, :@login_requirement_message)
48
+
47
49
  def login_confirm_label
48
50
  "Confirm #{login_label}"
49
51
  end
@@ -68,8 +68,8 @@ module Rodauth
68
68
  auth_value_method :otp_setup_raw_param, 'otp_raw_secret'
69
69
  translatable_method :otp_auth_form_footer, ''
70
70
 
71
- auth_cached_method :otp_key
72
- auth_cached_method :otp
71
+ cached_auth_method :otp_key
72
+ cached_auth_method :otp
73
73
  private :otp
74
74
 
75
75
  auth_value_methods(
@@ -100,6 +100,8 @@ module Rodauth
100
100
  :otp_valid_code_for_old_secret
101
101
  )
102
102
 
103
+ uses_instance_variables(:@otp, :@otp_key, :@otp_user_key, :@otp_tmp_key)
104
+
103
105
  internal_request_method :otp_setup_params
104
106
  internal_request_method :otp_setup
105
107
  internal_request_method :otp_auth
@@ -245,7 +247,7 @@ module Rodauth
245
247
  end
246
248
 
247
249
  def otp_exists?
248
- !otp_key.nil?
250
+ !!otp_key
249
251
  end
250
252
 
251
253
  def otp_valid_code?(ot_pass)
@@ -277,7 +279,7 @@ module Rodauth
277
279
 
278
280
  def otp_remove
279
281
  otp_key_ds.delete
280
- @otp_key = nil
282
+ @otp_key = false
281
283
  end
282
284
 
283
285
  def otp_add_key
@@ -368,7 +370,7 @@ module Rodauth
368
370
  end
369
371
 
370
372
  def clear_cached_otp
371
- remove_instance_variable(:@otp) if defined?(@otp)
373
+ @otp = nil
372
374
  end
373
375
 
374
376
  def otp_tmp_key(secret)
@@ -436,7 +438,7 @@ module Rodauth
436
438
 
437
439
  def _otp_key
438
440
  @otp_user_key = nil
439
- otp_key_ds.get(otp_keys_column)
441
+ otp_key_ds.get(otp_keys_column) || false
440
442
  end
441
443
 
442
444
  def _otp_for_key(key)
@@ -56,6 +56,8 @@ module Rodauth
56
56
  :otp_unlock_refresh_tag,
57
57
  )
58
58
 
59
+ uses_instance_variables(:@otp_unlock_data)
60
+
59
61
  route(:otp_unlock) do |r|
60
62
  require_login
61
63
  require_account_session
@@ -7,6 +7,8 @@ module Rodauth
7
7
 
8
8
  auth_methods :password_recently_entered?
9
9
 
10
+ uses_instance_variables(:@last_password_entry)
11
+
10
12
  def modifications_require_password?
11
13
  return false unless super
12
14
  !password_recently_entered?
@@ -26,7 +28,7 @@ module Rodauth
26
28
 
27
29
  def update_session
28
30
  super
29
- set_session_value(last_password_entry_session_key, @last_password_entry) if defined?(@last_password_entry)
31
+ set_session_value(last_password_entry_session_key, @last_password_entry) if @last_password_entry
30
32
  end
31
33
 
32
34
  private
@@ -8,6 +8,8 @@ module Rodauth
8
8
  auth_value_method :previous_password_peppers, [""]
9
9
  auth_value_method :password_pepper_update?, true
10
10
 
11
+ uses_instance_variables(:@previous_pepper_matched)
12
+
11
13
  def password_match?(password)
12
14
  if (result = super) && @previous_pepper_matched && password_pepper_update?
13
15
  set_password(password)
@@ -13,8 +13,8 @@ module Rodauth
13
13
  path_meth = :"#{route}_path"
14
14
  url_meth = :"#{route}_url"
15
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)}
16
+ klass.define_singleton_method(path_meth){|opts=OPTS| instance.send(path_meth, opts)}
17
+ klass.define_singleton_method(url_meth){|opts=OPTS| instance.send(url_meth, opts)}
18
18
  end
19
19
  end
20
20
  end
@@ -46,7 +46,7 @@ module Rodauth
46
46
  translatable_method :recovery_auth_link_text, "Authenticate Using Recovery Code"
47
47
  translatable_method :recovery_codes_link_text, "View Authentication Recovery Codes"
48
48
 
49
- auth_cached_method :recovery_codes
49
+ cached_auth_method :recovery_codes
50
50
 
51
51
  auth_value_methods(
52
52
  :recovery_codes_primary?
@@ -60,6 +60,8 @@ module Rodauth
60
60
  :recovery_codes_available?,
61
61
  )
62
62
 
63
+ uses_instance_variables(:@recovery_codes, :@recovery_codes_button)
64
+
63
65
  internal_request_method :recovery_codes
64
66
  internal_request_method :recovery_auth
65
67
  internal_request_method :valid_recovery_auth?
@@ -181,7 +183,7 @@ module Rodauth
181
183
  add_recovery_code
182
184
  end
183
185
  end
184
- remove_instance_variable(:@recovery_codes)
186
+ @recovery_codes = nil
185
187
  end
186
188
 
187
189
  def add_recovery_code
@@ -16,7 +16,7 @@ module Rodauth
16
16
  response
17
17
 
18
18
  auth_value_method :raw_remember_token_deadline, nil
19
- auth_value_method :remember_cookie_options, {}.freeze
19
+ auth_value_method :remember_cookie_options, OPTS
20
20
  auth_value_method :extend_remember_deadline?, false
21
21
  auth_value_method :extend_remember_deadline_period, 3600
22
22
  auth_value_method :remember_period, {:days=>14}.freeze
@@ -49,6 +49,8 @@ module Rodauth
49
49
  :remove_remember_key
50
50
  )
51
51
 
52
+ uses_instance_variables(:@remember_key_value)
53
+
52
54
  internal_request_method :remember_setup
53
55
  internal_request_method :remember_disable
54
56
  internal_request_method :account_id_for_remember_key
@@ -57,6 +57,8 @@ module Rodauth
57
57
  :account_from_reset_password_key
58
58
  )
59
59
 
60
+ uses_instance_variables(:@reset_password_key_value)
61
+
60
62
  internal_request_method(:reset_password_request)
61
63
  internal_request_method
62
64
 
@@ -190,6 +192,11 @@ module Rodauth
190
192
  end
191
193
 
192
194
  def get_password_reset_key(id)
195
+ # RODAUTH3: Remove method and call get_reset_password_key directly
196
+ get_reset_password_key(id)
197
+ end
198
+
199
+ def get_reset_password_key(id)
193
200
  ds = password_reset_ds(id)
194
201
  ds.where(Sequel::CURRENT_TIMESTAMP > reset_password_deadline_column).delete
195
202
  ds.get(reset_password_key_column)
@@ -89,15 +89,13 @@ module Rodauth
89
89
  auth_value_method :sms_phone_min_length, 7
90
90
  auth_value_method :sms_phone_param, 'sms-phone'
91
91
 
92
- auth_cached_method :sms
93
-
94
92
  auth_value_methods(
95
93
  :sms_codes_primary?,
96
94
  :sms_needs_confirmation_notice_flash,
97
- :sms_request_response
98
95
  )
99
96
 
100
97
  auth_methods(
98
+ :sms,
101
99
  :sms_auth_message,
102
100
  :sms_available?,
103
101
  :sms_code_issued_at,
@@ -121,6 +119,9 @@ module Rodauth
121
119
  :sms_setup?,
122
120
  :sms_valid_phone?
123
121
  )
122
+ auth_private_methods :sms_request_response
123
+
124
+ uses_instance_variables(:@sms)
124
125
 
125
126
  internal_request_method :sms_setup
126
127
  internal_request_method :sms_confirm
@@ -356,7 +357,7 @@ module Rodauth
356
357
 
357
358
  def sms_disable
358
359
  sms_ds.delete
359
- @sms = nil
360
+ @sms = false
360
361
  end
361
362
 
362
363
  def sms_confirm_failure
@@ -367,7 +368,7 @@ module Rodauth
367
368
  # Cannot handle uniqueness violation here, as the phone number given may not match the
368
369
  # one in the table.
369
370
  sms_ds.insert(sms_id_column=>session_value, sms_phone_column=>phone_number, sms_failures_column => nil)
370
- remove_instance_variable(:@sms) if instance_variable_defined?(:@sms)
371
+ @sms = nil
371
372
  end
372
373
 
373
374
  def sms_remove_failures
@@ -521,8 +522,15 @@ module Rodauth
521
522
  update_hash_ds(sms, sms_ds, values)
522
523
  end
523
524
 
524
- def _sms
525
- sms_ds.first
525
+ def sms
526
+ case @sms
527
+ when nil
528
+ (@sms = sms_ds.first || false) || nil
529
+ when false
530
+ nil
531
+ else
532
+ @sms
533
+ end
526
534
  end
527
535
 
528
536
  def sms_ds
@@ -62,6 +62,8 @@ module Rodauth
62
62
  :two_factor_remove_links
63
63
  )
64
64
 
65
+ uses_instance_variables(:@two_factor_auth_links, :@two_factor_setup_links, :@two_factor_remove_links)
66
+
65
67
  internal_request_method :two_factor_disable
66
68
 
67
69
  route(:two_factor_manage, 'multifactor-manage') do |r|
@@ -4,6 +4,8 @@ module Rodauth
4
4
  Feature.define(:update_password_hash, :UpdatePasswordHash) do
5
5
  depends :login_password_requirements_base
6
6
 
7
+ uses_instance_variables(:@current_password_hash_cost, :@update_password_hash)
8
+
7
9
  def password_match?(password)
8
10
  if (result = super) && update_password_hash?
9
11
  @update_password_hash = false
@@ -59,6 +59,8 @@ module Rodauth
59
59
  :account_from_verify_account_key
60
60
  )
61
61
 
62
+ uses_instance_variables(:@verify_account_key_value)
63
+
62
64
  internal_request_method(:verify_account_resend)
63
65
  internal_request_method
64
66
 
@@ -51,6 +51,8 @@ module Rodauth
51
51
  :account_from_verify_login_change_key
52
52
  )
53
53
 
54
+ uses_instance_variables(:@verify_login_change_key_value, :@verify_login_change_new_login)
55
+
54
56
  internal_request_method
55
57
 
56
58
  route do |r|
@@ -12,6 +12,8 @@ module Rodauth
12
12
 
13
13
  auth_value_method :webauthn_login_user_verification_additional_factor?, false
14
14
 
15
+ uses_instance_variables(:@webauthn_login)
16
+
15
17
  internal_request_method :webauthn_login_params
16
18
  internal_request_method :webauthn_login
17
19
 
@@ -4,6 +4,8 @@ module Rodauth
4
4
  Feature.define(:webauthn_verify_account, :WebauthnVerifyAccount) do
5
5
  depends :verify_account, :webauthn
6
6
 
7
+ uses_instance_variables(:@webauthn_credential)
8
+
7
9
  def verify_account_view
8
10
  webauthn_setup_view
9
11
  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 = 43
9
+ MINOR = 45
10
10
 
11
11
  # The patch version of Rodauth, updated only for bug fixes from the last
12
12
  # feature release.
data/lib/rodauth.rb CHANGED
@@ -3,9 +3,12 @@
3
3
  require 'securerandom'
4
4
 
5
5
  module Rodauth
6
+ OPTS = {}.freeze
7
+ SCOPE_INSTANCE_VARIABLES = [:@_rodauths, :@_rodauth].freeze
8
+
6
9
  class ConfigurationError < StandardError; end
7
10
 
8
- def self.lib(opts={}, &block)
11
+ def self.lib(opts=OPTS, &block)
9
12
  require 'roda'
10
13
  c = Class.new(Roda)
11
14
  c.plugin(:rodauth, opts) do
@@ -16,7 +19,7 @@ module Rodauth
16
19
  c.rodauth
17
20
  end
18
21
 
19
- def self.load_dependencies(app, opts={}, &_)
22
+ def self.load_dependencies(app, opts=OPTS, &_)
20
23
  json_opt = opts.fetch(:json, app.opts[:rodauth_json])
21
24
  if json_opt
22
25
  app.plugin :json
@@ -45,7 +48,7 @@ module Rodauth
45
48
  end
46
49
  end
47
50
 
48
- def self.configure(app, opts={}, &block)
51
+ def self.configure(app, opts=OPTS, &block)
49
52
  json_opt = app.opts[:rodauth_json] = opts.fetch(:json, app.opts[:rodauth_json])
50
53
  csrf = app.opts[:rodauth_csrf] = opts.fetch(:csrf, app.opts[:rodauth_csrf])
51
54
  app.opts[:rodauth_route_csrf] = case csrf
@@ -63,6 +66,7 @@ module Rodauth
63
66
  end
64
67
  auth_class.class_eval{@configuration_name = opts[:name] unless defined?(@configuration_name)}
65
68
  auth_class.configure(&block) if block
69
+ auth_class.send(:make_shape_friendly)
66
70
  auth_class.allocate.post_configure if auth_class.method_defined?(:post_configure)
67
71
  end
68
72
 
@@ -70,15 +74,32 @@ module Rodauth
70
74
 
71
75
  class FeatureConfiguration < Module
72
76
  def def_configuration_methods(feature)
73
- private_methods = feature.private_instance_methods.map(&:to_sym)
77
+ private_methods = feature.private_instance_methods
74
78
  priv = proc{|m| private_methods.include?(m)}
75
- feature.auth_methods.each{|m| def_auth_method(m, priv[m])}
76
- feature.auth_value_methods.each{|m| def_auth_value_method(m, priv[m])}
77
- feature.auth_private_methods.each{|m| def_auth_private_method(m)}
79
+ feature.auth_methods.each do |m|
80
+ _check_method_defined(feature, m, priv[m])
81
+ def_auth_method(m, priv[m])
82
+ end
83
+ feature.auth_value_methods.each do |m|
84
+ _check_method_defined(feature, m, priv[m])
85
+ def_auth_value_method(m, priv[m])
86
+ end
87
+ feature.auth_private_methods.each do |m|
88
+ _check_method_defined(feature, :"_#{m}", true)
89
+ def_auth_private_method(m)
90
+ end
78
91
  end
79
92
 
80
93
  private
81
94
 
95
+ def _check_method_defined(feature, meth, priv)
96
+ if !feature.send(priv ? :private_method_defined? : :method_defined?, meth) &&
97
+ (!(allowed = feature.allowed_undefined_configuration_methods) || !allowed.include?(meth))
98
+ # RODAUTH3: raise instead of warn
99
+ warn "Bug in Rodauth #{feature.feature_name} feature definition, configuration method added for #{meth}, but the feature doesn't define the method"
100
+ end
101
+ end
102
+
82
103
  def def_auth_method(meth, priv)
83
104
  define_method(meth) do |&block|
84
105
  @auth.send(:define_method, meth, &block)
@@ -124,14 +145,16 @@ module Rodauth
124
145
  attr_accessor :dependencies
125
146
  attr_accessor :routes
126
147
  attr_accessor :configuration
148
+ attr_accessor :allowed_undefined_configuration_methods
127
149
  attr_reader :internal_request_methods
150
+ attr_reader :instance_variables_used
128
151
 
129
152
  def route(name=feature_name, default=name.to_s.tr('_', '-'), &block)
130
153
  route_meth = :"#{name}_route"
131
154
  auth_value_method route_meth, default
132
155
 
133
- define_method(:"#{name}_path"){|opts={}| route_path(send(route_meth), opts) if send(route_meth)}
134
- define_method(:"#{name}_url"){|opts={}| route_url(send(route_meth), opts) if send(route_meth)}
156
+ define_method(:"#{name}_path"){|opts=OPTS| route_path(send(route_meth), opts) if send(route_meth)}
157
+ define_method(:"#{name}_url"){|opts=OPTS| route_url(send(route_meth), opts) if send(route_meth)}
135
158
 
136
159
  handle_meth = :"handle_#{name}"
137
160
  internal_handle_meth = :"_#{handle_meth}"
@@ -175,6 +198,10 @@ module Rodauth
175
198
  (@internal_request_methods ||= []) << name
176
199
  end
177
200
 
201
+ def uses_instance_variables(*ivs)
202
+ @instance_variables_used = ivs.freeze
203
+ end
204
+
178
205
  def configuration_module_eval(&block)
179
206
  configuration.module_eval(&block)
180
207
  end
@@ -254,7 +281,7 @@ module Rodauth
254
281
  end
255
282
  end
256
283
 
257
- def email(type, subject, opts = {})
284
+ def email(type, subject, opts = OPTS)
258
285
  subject_method = :"#{type}_email_subject"
259
286
  body_method = :"#{type}_email_body"
260
287
  create_method = :"create_#{type}_email"
@@ -305,7 +332,24 @@ module Rodauth
305
332
  auth_value_methods(meth)
306
333
  end
307
334
 
308
- def auth_cached_method(meth, iv=:"@#{meth}")
335
+ # Auth caching method that treats a nil instance variable as
336
+ # not being cached. If nil is a valid value for the instance
337
+ # variable, do not use this method, use a regular auth_method
338
+ # and handle caching manually.
339
+ def cached_auth_method(meth, iv=:"@#{meth}")
340
+ umeth = :"_#{meth}"
341
+ define_method(meth) do
342
+ v = instance_variable_get(iv)
343
+ v.nil? ? instance_variable_set(iv, send(umeth)) : v
344
+ end
345
+ alias_method(meth, meth)
346
+ auth_private_methods(meth)
347
+ end
348
+
349
+ # :nocov:
350
+ def auth_cached_method(meth, iv=:"@#{meth}") # :nodoc:
351
+ # Non-shape friendly historical method.
352
+ # RODAUTH3: Remove
309
353
  umeth = :"_#{meth}"
310
354
  define_method(meth) do
311
355
  if instance_variable_defined?(iv)
@@ -317,6 +361,7 @@ module Rodauth
317
361
  alias_method(meth, meth)
318
362
  auth_private_methods(meth)
319
363
  end
364
+ # :nocov:
320
365
 
321
366
  [:notice_flash, :error_flash, :button].each do |meth|
322
367
  define_method(meth) do |v, name=feature_name|
@@ -376,6 +421,60 @@ module Rodauth
376
421
  attr_accessor :route_hash
377
422
  attr_reader :configuration_name
378
423
  attr_reader :configuration
424
+
425
+ private
426
+
427
+ if RUBY_VERSION >= "3.2" && defined?(RubyVM::YJIT.enable)
428
+ # Use a shape-friendly object on Ruby 3.2 if YJIT is available
429
+ # with the assumption that it will be enabled later in any situation
430
+ # desiring high performance.
431
+ def make_shape_friendly
432
+ ivs = instance_variables_used
433
+
434
+ unless ivs.empty?
435
+ ivs.uniq!
436
+ ivs = ivs.reverse.join(" = ")
437
+ method_content = "#{ivs} = nil"
438
+ class_eval(<<-RUBY, __FILE__, __LINE__+1)
439
+ private def _initialize_instance_variables
440
+ #{method_content}
441
+ end
442
+ alias _initialize_instance_variables _initialize_instance_variables
443
+ RUBY
444
+ end
445
+
446
+ nil
447
+ end
448
+
449
+ def instance_variables_used
450
+ ivs = []
451
+ features = []
452
+
453
+ # Try to ensure that instance variable order follows feature enablement order
454
+ ancestors.each do |mod|
455
+ features << mod if mod.is_a?(Feature)
456
+ end
457
+
458
+ features.reverse_each do |mod|
459
+ if (feature_ivs = mod.instance_variables_used)
460
+ feature_ivs.each do |iv|
461
+ unless iv.match?(/\A@[a-z_][a-z0-9_]*\z/)
462
+ raise ConfigurationError, "invalid model instance variable used"
463
+ end
464
+
465
+ ivs << iv
466
+ end
467
+ end
468
+ end
469
+
470
+ ivs
471
+ end
472
+ # :nocov:
473
+ else
474
+ def make_shape_friendly # :nodoc:
475
+ end
476
+ end
477
+ # :nocov:
379
478
  end
380
479
 
381
480
  def self.inherited(subclass)
@@ -429,7 +528,7 @@ module Rodauth
429
528
  view_opts = rodauth.send(:loaded_templates).map do |page|
430
529
  rodauth.send(:_view_opts, page)
431
530
  end
432
- view_opts << rodauth.send(:button_opts, '', {})
531
+ view_opts << rodauth.send(:button_opts, '', OPTS)
433
532
 
434
533
  view_opts.each do |opts|
435
534
  instance.send(:retrieve_template, opts).send(:compiled_method, opts[:locals].keys.sort_by(&:to_s))
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rodauth
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.43.0
4
+ version: 2.45.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeremy Evans
@@ -403,7 +403,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
403
403
  - !ruby/object:Gem::Version
404
404
  version: '0'
405
405
  requirements: []
406
- rubygems_version: 4.0.3
406
+ rubygems_version: 4.0.10
407
407
  specification_version: 4
408
408
  summary: Authentication and Account Management Framework for Rack Applications
409
409
  test_files: []