rodauth 2.5.0 → 2.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +42 -0
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +18 -6
  5. data/doc/argon2.rdoc +49 -0
  6. data/doc/base.rdoc +3 -2
  7. data/doc/guides/migrate_password_hash_algorithm.rdoc +15 -0
  8. data/doc/json.rdoc +47 -0
  9. data/doc/jwt.rdoc +1 -28
  10. data/doc/jwt_refresh.rdoc +8 -0
  11. data/doc/login_password_requirements_base.rdoc +1 -1
  12. data/doc/recovery_codes.rdoc +2 -1
  13. data/doc/release_notes/2.10.0.txt +47 -0
  14. data/doc/release_notes/2.6.0.txt +37 -0
  15. data/doc/release_notes/2.7.0.txt +33 -0
  16. data/doc/release_notes/2.8.0.txt +20 -0
  17. data/doc/release_notes/2.9.0.txt +21 -0
  18. data/doc/remember.rdoc +1 -1
  19. data/javascript/webauthn_auth.js +9 -9
  20. data/javascript/webauthn_setup.js +9 -6
  21. data/lib/rodauth.rb +14 -6
  22. data/lib/rodauth/features/argon2.rb +69 -0
  23. data/lib/rodauth/features/base.rb +12 -3
  24. data/lib/rodauth/features/confirm_password.rb +2 -2
  25. data/lib/rodauth/features/disallow_password_reuse.rb +20 -7
  26. data/lib/rodauth/features/json.rb +189 -0
  27. data/lib/rodauth/features/jwt.rb +22 -170
  28. data/lib/rodauth/features/jwt_refresh.rb +63 -13
  29. data/lib/rodauth/features/login_password_requirements_base.rb +4 -0
  30. data/lib/rodauth/features/otp.rb +0 -2
  31. data/lib/rodauth/features/recovery_codes.rb +22 -1
  32. data/lib/rodauth/features/remember.rb +6 -1
  33. data/lib/rodauth/features/update_password_hash.rb +1 -1
  34. data/lib/rodauth/features/verify_account.rb +6 -7
  35. data/lib/rodauth/features/webauthn_verify_account.rb +1 -1
  36. data/lib/rodauth/migrations.rb +31 -5
  37. data/lib/rodauth/version.rb +1 -1
  38. metadata +55 -24
@@ -0,0 +1,33 @@
1
+ = New Features
2
+
3
+ * An auto_remove_recovery_codes? configuration method has been added
4
+ to the recovery_codes feature. This will automatically remove
5
+ recovery codes when the last multifactor authentication type other
6
+ than the recovery codes has been removed.
7
+
8
+ * The jwt_access_expired_status and expired_jwt_access_token_message
9
+ configuration methods have been added to the jwt_refresh feature,
10
+ for supporting custom statuses and messages for expired tokens.
11
+
12
+ = Other Improvements
13
+
14
+ * Rodauth will no longer attempt to require a feature that has
15
+ already been required. Related to this is you can now use a
16
+ a custom Rodauth feature without a rodauth/features/*.rb file
17
+ in the Ruby library path, as long as you load the feature
18
+ manually.
19
+
20
+ * Rodauth now avoids method redefinition warnings in verbose
21
+ warning mode. As Ruby 3 is dropping uninitialized instance
22
+ variable warnings, Rodauth will be verbose warning free in
23
+ Ruby 3.
24
+
25
+ = Backwards Compatibility
26
+
27
+ * The default remember cookie path is now set to '/'. This fixes
28
+ usage in the case where rodauth is loaded under a subpath of the
29
+ application (which is not the default behavior). Unfortunately,
30
+ this change can negatively affect cases where multiple rodauth
31
+ configurations are used in separate paths on the same domain.
32
+ In these cases, you should now use remember_cookie_options and
33
+ include a :path option.
@@ -0,0 +1,20 @@
1
+ = Improvements
2
+
3
+ * HttpOnly is now set by default on the remember cookie, so it is no
4
+ longer accessible from Javascript. This is a more secure approach
5
+ that makes applications using Rodauth's remember feature less
6
+ vulnerable in case they are subject to a separate XSS attack.
7
+
8
+ * When using the jwt feature, rodauth.clear_session now clears the
9
+ JWT session even when the Roda sessions plugin was in use. In most
10
+ cases, the jwt feature is not used with the Roda sessions plugin,
11
+ but in cases where the same application serves as both an JSON API
12
+ and as a HTML site, it is possible the two may be used together.
13
+
14
+ = Backwards Compatibility
15
+
16
+ * As the default remember cookie :httponly setting is now set to true,
17
+ applications using Rodauth that expected to be able to access the
18
+ remember cookie from Javascript will no longer work by default.
19
+ In these cases, you should now use remember_cookie_options and
20
+ include a :httponly=>false option.
@@ -0,0 +1,21 @@
1
+ = New Features
2
+
3
+ * A json feature has been extracted from the existing jwt feature.
4
+ This feature allows for the same JSON API previously supported
5
+ by the JWT feature, but stores the session information in the
6
+ Rack session instead of in a separate JWT. This makes it
7
+ significantly easier to have certain pages use the JSON API,
8
+ and other pages the HTML forms.
9
+
10
+ = Other Improvements
11
+
12
+ * If the remember cookie is created in an SSL request, the Secure
13
+ flag is added by default, so the cookie will not be transmitted
14
+ in non-SSL requests.
15
+
16
+ = Backwards Compatibility
17
+
18
+ * Rodauth configurations that use the remember feature and support
19
+ requests over both http and https and want to have the remember
20
+ cookie transmitted over both should now include :secure=>false in
21
+ remember_cookie_options.
data/doc/remember.rdoc CHANGED
@@ -35,7 +35,7 @@ raw_remember_token_deadline :: A deadline before which to allow a raw remember t
35
35
  remember_additional_form_tags :: HTML fragment containing additional form tags to use on the change remember setting form.
36
36
  remember_button :: The text to use for the change remember settings button.
37
37
  remember_cookie_key :: The cookie name to use for the remember token.
38
- remember_cookie_options :: Any options to set for the remember cookie.
38
+ remember_cookie_options :: Any options to set for the remember cookie. By default, the `:path` cookie option is set to `/` and `:httponly` is set to `true`. Also, `:secure` is set to `true` by default if the current request is an HTTPS request.
39
39
  remember_deadline_column :: The column name in the +remember_table+ storing the deadline after which the token will be ignored.
40
40
  remember_deadline_interval :: The amount of time for which to remember accounts, 14 days by default. Only used if +set_deadline_values?+ is true.
41
41
  remember_disable_label :: The label for disabling remembering.
@@ -1,34 +1,34 @@
1
1
  (function() {
2
+ var pack = function(v) { return btoa(String.fromCharCode.apply(null, new Uint8Array(v))).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); };
3
+ var unpack = function(v) { return Uint8Array.from(atob(v.replace(/-/g, '+').replace(/_/g, '/')), c => c.charCodeAt(0)); };
2
4
  var element = document.getElementById('webauthn-auth-form');
3
5
  var f = function(e) {
4
6
  //console.log(e);
5
7
  e.preventDefault();
6
8
  if (navigator.credentials) {
7
9
  var opts = JSON.parse(element.getAttribute("data-credential-options"));
8
- opts.challenge = Uint8Array.from(atob(opts.challenge.replace(/-/g, '+').replace(/_/g, '/')), c => c.charCodeAt(0));
9
- opts.allowCredentials.forEach(function(cred) {
10
- cred.id = Uint8Array.from(atob(cred.id.replace(/-/g, '+').replace(/_/g, '/')), c => c.charCodeAt(0));
11
- });
10
+ opts.challenge = unpack(opts.challenge);
11
+ opts.allowCredentials.forEach(function(cred) { cred.id = unpack(cred.id); });
12
12
  //console.log(opts);
13
13
  navigator.credentials.get({publicKey: opts}).
14
14
  then(function(cred){
15
15
  //console.log(cred);
16
16
  //window.cred = cred
17
17
 
18
- var rawId = btoa(String.fromCharCode.apply(null, new Uint8Array(cred.rawId))).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
18
+ var rawId = pack(cred.rawId);
19
19
  var authValue = {
20
20
  type: cred.type,
21
21
  id: rawId,
22
22
  rawId: rawId,
23
23
  response: {
24
- authenticatorData: btoa(String.fromCharCode.apply(null, new Uint8Array(cred.response.authenticatorData))).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''),
25
- clientDataJSON: btoa(String.fromCharCode.apply(null, new Uint8Array(cred.response.clientDataJSON))).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''),
26
- signature: btoa(String.fromCharCode.apply(null, new Uint8Array(cred.response.signature))).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '')
24
+ authenticatorData: pack(cred.response.authenticatorData),
25
+ clientDataJSON: pack(cred.response.clientDataJSON),
26
+ signature: pack(cred.response.signature)
27
27
  }
28
28
  };
29
29
 
30
30
  if (cred.response.userHandle) {
31
- authValue.response.userHandle = btoa(String.fromCharCode.apply(null, new Uint8Array(cred.response.userHandle))).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
31
+ authValue.response.userHandle = pack(cred.response.userHandle);
32
32
  }
33
33
 
34
34
  document.getElementById('webauthn-auth').value = JSON.stringify(authValue);
@@ -1,26 +1,29 @@
1
1
  (function() {
2
+ var pack = function(v) { return btoa(String.fromCharCode.apply(null, new Uint8Array(v))).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); };
3
+ var unpack = function(v) { return Uint8Array.from(atob(v.replace(/-/g, '+').replace(/_/g, '/')), c => c.charCodeAt(0)); };
2
4
  var element = document.getElementById('webauthn-setup-form');
3
5
  var f = function(e) {
4
6
  //console.log(e);
5
7
  e.preventDefault();
6
8
  if (navigator.credentials) {
7
9
  var opts = JSON.parse(element.getAttribute("data-credential-options"));
8
- opts.challenge = Uint8Array.from(atob(opts.challenge.replace(/-/g, '+').replace(/_/g, '/')), c => c.charCodeAt(0));
9
- opts.user.id = Uint8Array.from(atob(opts.user.id.replace(/-/g, '+').replace(/_/g, '/')), c => c.charCodeAt(0));
10
+ opts.challenge = unpack(opts.challenge);
11
+ opts.user.id = unpack(opts.user.id);
12
+ opts.excludeCredentials.forEach(function(cred) { cred.id = unpack(cred.id); });
10
13
  //console.log(opts);
11
14
  navigator.credentials.create({publicKey: opts}).
12
15
  then(function(cred){
13
16
  //console.log(cred);
14
17
  //window.cred = cred
15
-
16
- var rawId = btoa(String.fromCharCode.apply(null, new Uint8Array(cred.rawId))).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
18
+
19
+ var rawId = pack(cred.rawId);
17
20
  document.getElementById('webauthn-setup').value = JSON.stringify({
18
21
  type: cred.type,
19
22
  id: rawId,
20
23
  rawId: rawId,
21
24
  response: {
22
- attestationObject: btoa(String.fromCharCode.apply(null, new Uint8Array(cred.response.attestationObject))).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''),
23
- clientDataJSON: btoa(String.fromCharCode.apply(null, new Uint8Array(cred.response.clientDataJSON))).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '')
25
+ attestationObject: pack(cred.response.attestationObject),
26
+ clientDataJSON: pack(cred.response.clientDataJSON)
24
27
  }
25
28
  });
26
29
  element.removeEventListener("submit", f);
data/lib/rodauth.rb CHANGED
@@ -66,6 +66,7 @@ module Rodauth
66
66
  define_method(meth) do |&block|
67
67
  @auth.send(:define_method, meth, &block)
68
68
  @auth.send(:private, meth) if priv
69
+ @auth.send(:alias_method, meth, meth)
69
70
  end
70
71
  end
71
72
 
@@ -74,6 +75,7 @@ module Rodauth
74
75
  define_method(meth) do |&block|
75
76
  @auth.send(:define_method, umeth, &block)
76
77
  @auth.send(:private, umeth)
78
+ @auth.send(:alias_method, umeth, umeth)
77
79
  end
78
80
  end
79
81
 
@@ -82,6 +84,7 @@ module Rodauth
82
84
  block ||= proc{v}
83
85
  @auth.send(:define_method, meth, &block)
84
86
  @auth.send(:private, meth) if priv
87
+ @auth.send(:alias_method, meth, meth)
85
88
  end
86
89
  end
87
90
  end
@@ -120,8 +123,10 @@ module Rodauth
120
123
  define_method(handle_meth) do
121
124
  request.is send(route_meth) do
122
125
  check_csrf if check_csrf?
123
- before_rodauth
124
- send(internal_handle_meth, request)
126
+ _around_rodauth do
127
+ before_rodauth
128
+ send(internal_handle_meth, request)
129
+ end
125
130
  end
126
131
  end
127
132
 
@@ -238,6 +243,7 @@ module Rodauth
238
243
  instance_variable_set(iv, send(umeth))
239
244
  end
240
245
  end
246
+ alias_method(meth, meth)
241
247
  auth_private_methods(meth)
242
248
  end
243
249
 
@@ -288,15 +294,17 @@ module Rodauth
288
294
  end
289
295
 
290
296
  def enable(*features)
291
- new_features = features - @auth.features
292
- new_features.each{|f| load_feature(f)}
293
- @auth.features.concat(new_features)
297
+ features.each do |feature|
298
+ next if @auth.features.include?(feature)
299
+ load_feature(feature)
300
+ @auth.features << feature
301
+ end
294
302
  end
295
303
 
296
304
  private
297
305
 
298
306
  def load_feature(feature_name)
299
- require "rodauth/features/#{feature_name}"
307
+ require "rodauth/features/#{feature_name}" unless FEATURES[feature_name]
300
308
  feature = FEATURES[feature_name]
301
309
  enable(*feature.dependencies)
302
310
  extend feature.configuration
@@ -0,0 +1,69 @@
1
+ # frozen-string-literal: true
2
+
3
+ require 'argon2'
4
+
5
+ # :nocov:
6
+ if !defined?(Argon2::VERSION) || Argon2::VERSION < '2'
7
+ raise LoadError, "argon2 version 1.x not supported as it does not support argon2id hashes"
8
+ end
9
+ # :nocov:
10
+
11
+ module Rodauth
12
+ Feature.define(:argon2, :Argon2) do
13
+ depends :login_password_requirements_base
14
+
15
+ auth_value_method :use_argon2?, true
16
+
17
+ private
18
+
19
+ def password_hash_cost
20
+ return super unless use_argon2?
21
+ argon2_hash_cost
22
+ end
23
+
24
+ def password_hash(password)
25
+ return super unless use_argon2?
26
+ ::Argon2::Password.new(password_hash_cost).create(password)
27
+ end
28
+
29
+ def password_hash_match?(hash, password)
30
+ return super unless argon2_hash_algorithm?(hash)
31
+ argon2_password_hash_match?(hash, password)
32
+ end
33
+
34
+ def password_hash_using_salt(password, salt)
35
+ return super unless argon2_hash_algorithm?(salt)
36
+
37
+ argon2_params = Hash[extract_password_hash_cost(salt)]
38
+ argon2_params[:salt_do_not_supply] = Base64.decode64(salt.split('$').last)
39
+ ::Argon2::Password.new(argon2_params).create(password)
40
+ end
41
+
42
+ def extract_password_hash_cost(hash)
43
+ return super unless argon2_hash_algorithm?(hash )
44
+
45
+ /\A\$argon2id\$v=\d+\$m=(\d+),t=(\d+)/ =~ hash
46
+ { t_cost: $2.to_i, m_cost: Math.log2($1.to_i).to_i }
47
+ end
48
+
49
+ if ENV['RACK_ENV'] == 'test'
50
+ def argon2_hash_cost
51
+ {t_cost: 1, m_cost: 3}
52
+ end
53
+ # :nocov:
54
+ else
55
+ def argon2_hash_cost
56
+ {t_cost: 2, m_cost: 16}
57
+ end
58
+ end
59
+ # :nocov:
60
+
61
+ def argon2_hash_algorithm?(hash)
62
+ hash.start_with?('$argon2id$')
63
+ end
64
+
65
+ def argon2_password_hash_match?(hash, password)
66
+ ::Argon2::Password.verify_password(password, hash)
67
+ end
68
+ end
69
+ end
@@ -102,7 +102,6 @@ module Rodauth
102
102
  :set_redirect_error_flash,
103
103
  :set_title,
104
104
  :translate,
105
- :unverified_account_message,
106
105
  :update_session
107
106
  )
108
107
 
@@ -111,7 +110,8 @@ module Rodauth
111
110
  :account_from_session,
112
111
  :field_attributes,
113
112
  :field_error_attributes,
114
- :formatted_field_error
113
+ :formatted_field_error,
114
+ :around_rodauth
115
115
  )
116
116
 
117
117
  configuration_module_eval do
@@ -260,6 +260,7 @@ module Rodauth
260
260
  @password_field_autocomplete_value || 'current-password'
261
261
  end
262
262
 
263
+ alias account_password_hash_column account_password_hash_column
263
264
  # If the account_password_hash_column is set, the password hash is verified in
264
265
  # ruby, it will not use a database function to do so, it will check the password
265
266
  # hash using bcrypt.
@@ -459,8 +460,12 @@ module Rodauth
459
460
 
460
461
  private
461
462
 
463
+ def _around_rodauth
464
+ yield
465
+ end
466
+
462
467
  def database_function_password_match?(name, hash_id, password, salt)
463
- db.get(Sequel.function(function_name(name), hash_id, BCrypt::Engine.hash_secret(password, salt)))
468
+ db.get(Sequel.function(function_name(name), hash_id, password_hash_using_salt(password, salt)))
464
469
  end
465
470
 
466
471
  def password_hash_match?(hash, password)
@@ -588,6 +593,10 @@ module Rodauth
588
593
  @has_password = !!get_password_hash
589
594
  end
590
595
 
596
+ def password_hash_using_salt(password, salt)
597
+ BCrypt::Engine.hash_secret(password, salt)
598
+ end
599
+
591
600
  # Get the password hash for the user. When using database authentication functions,
592
601
  # note that only the salt is returned.
593
602
  def get_password_hash
@@ -26,11 +26,11 @@ module Rodauth
26
26
  require_account_session
27
27
  before_confirm_password_route
28
28
 
29
- request.get do
29
+ r.get do
30
30
  confirm_password_view
31
31
  end
32
32
 
33
- request.post do
33
+ r.post do
34
34
  if password_match?(param(password_param))
35
35
  transaction do
36
36
  before_confirm_password
@@ -24,13 +24,16 @@ module Rodauth
24
24
 
25
25
  def add_previous_password_hash(hash)
26
26
  ds = previous_password_ds
27
- keep_before = ds.reverse(previous_password_id_column).
28
- limit(nil, previous_passwords_to_check).
29
- get(previous_password_id_column)
30
27
 
31
- if keep_before
32
- ds.where(Sequel.expr(previous_password_id_column) <= keep_before).
33
- delete
28
+ unless @dont_check_previous_password
29
+ keep_before = ds.reverse(previous_password_id_column).
30
+ limit(nil, previous_passwords_to_check).
31
+ get(previous_password_id_column)
32
+
33
+ if keep_before
34
+ ds.where(Sequel.expr(previous_password_id_column) <= keep_before).
35
+ delete
36
+ end
34
37
  end
35
38
 
36
39
  # This should never raise uniqueness violations, as it uses a serial primary key
@@ -39,7 +42,7 @@ module Rodauth
39
42
 
40
43
  def password_meets_requirements?(password)
41
44
  super &&
42
- password_doesnt_match_previous_password?(password)
45
+ (@dont_check_previous_password || password_doesnt_match_previous_password?(password))
43
46
  end
44
47
 
45
48
  private
@@ -71,6 +74,16 @@ module Rodauth
71
74
  previous_password_ds.delete
72
75
  end
73
76
 
77
+ def before_create_account_route
78
+ super if defined?(super)
79
+ @dont_check_previous_password = true
80
+ end
81
+
82
+ def before_verify_account_route
83
+ super if defined?(super)
84
+ @dont_check_previous_password = true
85
+ end
86
+
74
87
  def after_create_account
75
88
  if account_password_hash_column && !(respond_to?(:verify_account_set_password?) && verify_account_set_password?)
76
89
  add_previous_password_hash(password_hash(param(password_param)))
@@ -0,0 +1,189 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Rodauth
4
+ Feature.define(:json, :Json) do
5
+ translatable_method :json_not_accepted_error_message, 'Unsupported Accept header. Must accept "application/json" or compatible content type'
6
+ translatable_method :json_non_post_error_message, 'non-POST method used in JSON API'
7
+ auth_value_method :json_accept_regexp, /(?:(?:\*|\bapplication)\/\*|\bapplication\/(?:vnd\.api\+)?json\b)/i
8
+ auth_value_method :json_check_accept?, true
9
+ auth_value_method :json_request_content_type_regexp, /\bapplication\/(?:vnd\.api\+)?json\b/i
10
+ auth_value_method :json_response_content_type, 'application/json'
11
+ auth_value_method :json_response_custom_error_status?, true
12
+ auth_value_method :json_response_error_status, 400
13
+ auth_value_method :json_response_error_key, "error"
14
+ auth_value_method :json_response_field_error_key, "field-error"
15
+ auth_value_method :json_response_success_key, "success"
16
+ translatable_method :non_json_request_error_message, 'Only JSON format requests are allowed'
17
+
18
+ auth_value_methods(
19
+ :only_json?,
20
+ :use_json?,
21
+ )
22
+
23
+ auth_methods(
24
+ :json_request?,
25
+ )
26
+
27
+ auth_private_methods :json_response_body
28
+
29
+ def set_field_error(field, message)
30
+ return super unless use_json?
31
+ json_response[json_response_field_error_key] = [field, message]
32
+ end
33
+
34
+ def set_error_flash(message)
35
+ return super unless use_json?
36
+ json_response[json_response_error_key] = message
37
+ end
38
+
39
+ def set_redirect_error_flash(message)
40
+ return super unless use_json?
41
+ json_response[json_response_error_key] = message
42
+ end
43
+
44
+ def set_notice_flash(message)
45
+ return super unless use_json?
46
+ json_response[json_response_success_key] = message if include_success_messages?
47
+ end
48
+
49
+ def set_notice_now_flash(message)
50
+ return super unless use_json?
51
+ json_response[json_response_success_key] = message if include_success_messages?
52
+ end
53
+
54
+ def json_request?
55
+ return @json_request if defined?(@json_request)
56
+ @json_request = request.content_type =~ json_request_content_type_regexp
57
+ end
58
+
59
+ def use_json?
60
+ json_request? || only_json?
61
+ end
62
+
63
+ def view(page, title)
64
+ return super unless use_json?
65
+ return_json_response
66
+ end
67
+
68
+ private
69
+
70
+ def before_view_recovery_codes
71
+ super if defined?(super)
72
+ if use_json?
73
+ json_response[:codes] = recovery_codes
74
+ json_response[json_response_success_key] ||= "" if include_success_messages?
75
+ end
76
+ end
77
+
78
+ def before_webauthn_setup_route
79
+ super if defined?(super)
80
+ if use_json? && !param_or_nil(webauthn_setup_param)
81
+ cred = new_webauthn_credential
82
+ json_response[webauthn_setup_param] = cred.as_json
83
+ json_response[webauthn_setup_challenge_param] = cred.challenge
84
+ json_response[webauthn_setup_challenge_hmac_param] = compute_hmac(cred.challenge)
85
+ end
86
+ end
87
+
88
+ def before_webauthn_auth_route
89
+ super if defined?(super)
90
+ if use_json? && !param_or_nil(webauthn_auth_param)
91
+ cred = webauth_credential_options_for_get
92
+ json_response[webauthn_auth_param] = cred.as_json
93
+ json_response[webauthn_auth_challenge_param] = cred.challenge
94
+ json_response[webauthn_auth_challenge_hmac_param] = compute_hmac(cred.challenge)
95
+ end
96
+ end
97
+
98
+ def before_webauthn_login_route
99
+ super if defined?(super)
100
+ if use_json? && !param_or_nil(webauthn_auth_param) && account_from_login(param(login_param))
101
+ cred = webauth_credential_options_for_get
102
+ json_response[webauthn_auth_param] = cred.as_json
103
+ json_response[webauthn_auth_challenge_param] = cred.challenge
104
+ json_response[webauthn_auth_challenge_hmac_param] = compute_hmac(cred.challenge)
105
+ end
106
+ end
107
+
108
+ def before_webauthn_remove_route
109
+ super if defined?(super)
110
+ if use_json? && !param_or_nil(webauthn_remove_param)
111
+ json_response[webauthn_remove_param] = account_webauthn_usage
112
+ end
113
+ end
114
+
115
+ def before_otp_setup_route
116
+ super if defined?(super)
117
+ if use_json? && otp_keys_use_hmac? && !param_or_nil(otp_setup_raw_param)
118
+ _otp_tmp_key(otp_new_secret)
119
+ json_response[otp_setup_param] = otp_user_key
120
+ json_response[otp_setup_raw_param] = otp_key
121
+ end
122
+ end
123
+
124
+ def before_rodauth
125
+ if json_request?
126
+ if json_check_accept? && (accept = request.env['HTTP_ACCEPT']) && accept !~ json_accept_regexp
127
+ response.status = 406
128
+ json_response[json_response_error_key] = json_not_accepted_error_message
129
+ _return_json_response
130
+ end
131
+
132
+ unless request.post?
133
+ response.status = 405
134
+ response.headers['Allow'] = 'POST'
135
+ json_response[json_response_error_key] = json_non_post_error_message
136
+ return_json_response
137
+ end
138
+ elsif only_json?
139
+ response.status = json_response_error_status
140
+ response.write non_json_request_error_message
141
+ request.halt
142
+ end
143
+
144
+ super
145
+ end
146
+
147
+ def redirect(_)
148
+ return super unless use_json?
149
+ return_json_response
150
+ end
151
+
152
+ def return_json_response
153
+ _return_json_response
154
+ end
155
+
156
+ def _return_json_response
157
+ response.status ||= json_response_error_status if json_response[json_response_error_key]
158
+ response['Content-Type'] ||= json_response_content_type
159
+ response.write(_json_response_body(json_response))
160
+ request.halt
161
+ end
162
+
163
+ def include_success_messages?
164
+ !json_response_success_key.nil?
165
+ end
166
+
167
+ def _json_response_body(hash)
168
+ request.send(:convert_to_json, hash)
169
+ end
170
+
171
+ def json_response
172
+ @json_response ||= {}
173
+ end
174
+
175
+ def set_redirect_error_status(status)
176
+ if use_json? && json_response_custom_error_status?
177
+ response.status = status
178
+ end
179
+ end
180
+
181
+ def set_response_error_status(status)
182
+ if use_json? && !json_response_custom_error_status?
183
+ status = json_response_error_status
184
+ end
185
+
186
+ super
187
+ end
188
+ end
189
+ end