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.
- checksums.yaml +4 -4
- data/CHANGELOG +42 -0
- data/MIT-LICENSE +1 -1
- data/README.rdoc +18 -6
- data/doc/argon2.rdoc +49 -0
- data/doc/base.rdoc +3 -2
- data/doc/guides/migrate_password_hash_algorithm.rdoc +15 -0
- data/doc/json.rdoc +47 -0
- data/doc/jwt.rdoc +1 -28
- data/doc/jwt_refresh.rdoc +8 -0
- data/doc/login_password_requirements_base.rdoc +1 -1
- data/doc/recovery_codes.rdoc +2 -1
- data/doc/release_notes/2.10.0.txt +47 -0
- data/doc/release_notes/2.6.0.txt +37 -0
- data/doc/release_notes/2.7.0.txt +33 -0
- data/doc/release_notes/2.8.0.txt +20 -0
- data/doc/release_notes/2.9.0.txt +21 -0
- data/doc/remember.rdoc +1 -1
- data/javascript/webauthn_auth.js +9 -9
- data/javascript/webauthn_setup.js +9 -6
- data/lib/rodauth.rb +14 -6
- data/lib/rodauth/features/argon2.rb +69 -0
- data/lib/rodauth/features/base.rb +12 -3
- data/lib/rodauth/features/confirm_password.rb +2 -2
- data/lib/rodauth/features/disallow_password_reuse.rb +20 -7
- data/lib/rodauth/features/json.rb +189 -0
- data/lib/rodauth/features/jwt.rb +22 -170
- data/lib/rodauth/features/jwt_refresh.rb +63 -13
- data/lib/rodauth/features/login_password_requirements_base.rb +4 -0
- data/lib/rodauth/features/otp.rb +0 -2
- data/lib/rodauth/features/recovery_codes.rb +22 -1
- data/lib/rodauth/features/remember.rb +6 -1
- data/lib/rodauth/features/update_password_hash.rb +1 -1
- data/lib/rodauth/features/verify_account.rb +6 -7
- data/lib/rodauth/features/webauthn_verify_account.rb +1 -1
- data/lib/rodauth/migrations.rb +31 -5
- data/lib/rodauth/version.rb +1 -1
- 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.
|
data/javascript/webauthn_auth.js
CHANGED
@@ -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 =
|
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 =
|
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:
|
25
|
-
clientDataJSON:
|
26
|
-
signature:
|
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 =
|
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 =
|
9
|
-
opts.user.id =
|
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 =
|
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:
|
23
|
-
clientDataJSON:
|
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
|
-
|
124
|
-
|
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
|
-
|
292
|
-
|
293
|
-
|
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,
|
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
|
-
|
29
|
+
r.get do
|
30
30
|
confirm_password_view
|
31
31
|
end
|
32
32
|
|
33
|
-
|
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
|
-
|
32
|
-
ds.
|
33
|
-
|
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
|