rodauth 2.5.0 → 2.10.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 (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