rodauth 2.5.0 → 2.6.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 +12 -0
- data/doc/base.rdoc +2 -1
- data/doc/jwt_refresh.rdoc +6 -0
- data/doc/release_notes/2.6.0.txt +37 -0
- data/javascript/webauthn_auth.js +9 -9
- data/javascript/webauthn_setup.js +9 -6
- data/lib/rodauth.rb +9 -5
- data/lib/rodauth/features/base.rb +6 -1
- data/lib/rodauth/features/confirm_password.rb +2 -2
- data/lib/rodauth/features/jwt.rb +5 -1
- data/lib/rodauth/features/jwt_refresh.rb +41 -4
- data/lib/rodauth/features/verify_account.rb +6 -6
- data/lib/rodauth/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 82585797ff882cb56340e2145b05b74da1e10c428413b02e1656604fa8209c93
|
4
|
+
data.tar.gz: 7148d04c255f4310a21c6373d9ab20888e8fe46d3a07c090576385890ea82858
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: be8a3bffa982ca9730dafdefb8a8a874c2a2cb8fa62c84d4b0467928fc2164251ba28bb88948274316d7870473f0a602b8ac69eef8b48b70b27584ed7a443ded
|
7
|
+
data.tar.gz: afaf45ddda1073eaba697035700ec9108bebd1fc18998af9245fd86f532f497e6723fa532624aae426dac7e1600556af7b7369b2e7203e387be26dcbdfc22e3d
|
data/CHANGELOG
CHANGED
@@ -1,3 +1,15 @@
|
|
1
|
+
=== 2.6.0 (2020-11-20)
|
2
|
+
|
3
|
+
* Avoid loading features multiple times (janko) (#131)
|
4
|
+
|
5
|
+
* Add around_rodauth method for running code around the handling of all Rodauth routes (bjeanes) (#129)
|
6
|
+
|
7
|
+
* Fix javascript for registration of multiple webauthn keys (bjeanes) (#127)
|
8
|
+
|
9
|
+
* Add allow_refresh_with_expired_jwt_access_token? configuration method to jwt_refresh feature, for allowing refresh with expired access token (jeremyevans)
|
10
|
+
|
11
|
+
* Promote setup_account_verification to public API, useful for automatically sending account verification emails (jeremyevans)
|
12
|
+
|
1
13
|
=== 2.5.0 (2020-10-22)
|
2
14
|
|
3
15
|
* Add change_login_needs_verification_notice_flash for easier translation of change_login_notice_flash when using verify_login_change (bjeanes, janko, jeremyevans) (#126)
|
data/doc/base.rdoc
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
= Documentation for Base Feature
|
2
2
|
|
3
3
|
The base feature is automatically loaded when you use Rodauth. It contains
|
4
|
-
shared functionality that is used by multiple features.
|
4
|
+
shared functionality that is used by multiple features.
|
5
5
|
|
6
6
|
== Auth Value Methods
|
7
7
|
|
@@ -88,6 +88,7 @@ account_session_value :: The primary value of the current account to store in th
|
|
88
88
|
after_login :: Run arbitrary code after a successful login.
|
89
89
|
after_login_failure :: Run arbitrary code after a login failure due to an invalid password.
|
90
90
|
already_logged_in :: What action to take if you are already logged in and attempt to access a page that only makes sense if you are not logged in.
|
91
|
+
around_rodauth(&block) :: Run arbitrary code around handling any rodauth route. Call <tt>super(&block)</tt> for Rodauth to handle the action.
|
91
92
|
authenticated? :: Whether the user has been authenticated. If multifactor authentication has been enabled for the account, this is true only if the session is multifactor authenticated.
|
92
93
|
before_login :: Run arbitrary code after password has been checked, but before updating the session.
|
93
94
|
before_login_attempt :: Run arbitrary code after an account has been located, but before the password has been checked.
|
data/doc/jwt_refresh.rdoc
CHANGED
@@ -21,19 +21,25 @@ a value of <tt>all</tt> as the token value.
|
|
21
21
|
|
22
22
|
When using the refresh token, you must provide a valid access token, as that contains
|
23
23
|
information about the current session, which is used to create the new access token.
|
24
|
+
If you change the +allow_refresh_with_expired_jwt_access_token?+ setting to +true+,
|
25
|
+
an expired but otherwise valid access token will be accepted, and Rodauth will check
|
26
|
+
that the access token was issued in the same session as the refresh token.
|
24
27
|
|
25
28
|
This feature depends on the jwt feature.
|
26
29
|
|
27
30
|
== Auth Value Methods
|
28
31
|
|
32
|
+
allow_refresh_with_expired_jwt_access_token? :: Whether refreshing should be allowed with an expired access token. Default is +false+. You must set an +hmac_secret+ if setting this value to +true+.
|
29
33
|
jwt_access_token_key :: Name of the key in the response json holding the access token. Default is +access_token+.
|
30
34
|
jwt_access_token_not_before_period :: How many seconds before the current time will the jwt be considered valid (to account for inaccurate clocks). Default is 5.
|
31
35
|
jwt_access_token_period :: Validity of an access token in seconds, default is 1800 (30 minutes).
|
32
36
|
jwt_refresh_route :: The route to the login action. Defaults to <tt>jwt-refresh</tt>.
|
33
37
|
jwt_refresh_invalid_token_message :: Error message when the provided refresh token is non existent, invalid or expired.
|
34
38
|
jwt_refresh_token_account_id_column :: The column name in the +jwt_refresh_token_table+ storing the account id, should be a foreign key referencing the accounts table.
|
39
|
+
jwt_refresh_token_data_session_key :: The key in the session hash storing random data, for access checking during refresh if +allow_refresh_with_expired_jwt_access_token?+ is set.
|
35
40
|
jwt_refresh_token_deadline_column :: The column name in the +jwt_refresh_token_table+ storing the deadline after which the refresh token will no longer be valid.
|
36
41
|
jwt_refresh_token_deadline_interval :: Validity of a refresh token. Default is 14 days.
|
42
|
+
jwt_refresh_token_hmac_session_key :: The key in the session hash storing the hmac, for access checking during refresh if +allow_refresh_with_expired_jwt_access_token?+ is set.
|
37
43
|
jwt_refresh_token_id_column :: The column name in the refresh token keys table storing the id of each token (the primary key of the table).
|
38
44
|
jwt_refresh_token_key :: Name of the key in the response json holding the refresh token. Default is +refresh_token+.
|
39
45
|
jwt_refresh_token_key_column :: The column name in the +jwt_refresh_token_table+ holding the refresh token key value.
|
@@ -0,0 +1,37 @@
|
|
1
|
+
= New Features
|
2
|
+
|
3
|
+
* An around_rodauth configuration method has been added, which is
|
4
|
+
called around all Rodauth actions. This configuration method
|
5
|
+
is passed a block, and is useful for cases where you want to wrap
|
6
|
+
Rodauth's handling of the request.
|
7
|
+
|
8
|
+
For example, if you had a method named time_block in your Roda scope
|
9
|
+
that timed block execution and added a response header, you could
|
10
|
+
time Rodauth actions using something like:
|
11
|
+
|
12
|
+
around_rodauth do |&block|
|
13
|
+
scope.time_block('Rodauth') do
|
14
|
+
super(&block)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
* The allow_refresh_with_expired_jwt_access_token? configuration has
|
19
|
+
been added to the jwt_refresh feature, allowing refreshing with an
|
20
|
+
expired but otherwise valid access token. When using this method,
|
21
|
+
it is required to have an hmac_secret specified, so that Rodauth
|
22
|
+
can make sure the access token matches the refresh token.
|
23
|
+
|
24
|
+
= Other Improvements
|
25
|
+
|
26
|
+
* The javascript for setting up a WebAuthn token has been fixed to
|
27
|
+
allow it to work correctly if there is already an existing
|
28
|
+
WebAuthn token for the account.
|
29
|
+
|
30
|
+
* The rodauth.setup_account_verification method has been promoted to
|
31
|
+
public API. You can use this method for automatically sending
|
32
|
+
account verification emails when automatically creating accounts.
|
33
|
+
|
34
|
+
* Rodauth no longer loads the same feature multiple times into a
|
35
|
+
single configuration. This didn't cause any problems before, but
|
36
|
+
could result in duplicate entries when looking at the loaded
|
37
|
+
features.
|
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
@@ -120,8 +120,10 @@ module Rodauth
|
|
120
120
|
define_method(handle_meth) do
|
121
121
|
request.is send(route_meth) do
|
122
122
|
check_csrf if check_csrf?
|
123
|
-
|
124
|
-
|
123
|
+
_around_rodauth do
|
124
|
+
before_rodauth
|
125
|
+
send(internal_handle_meth, request)
|
126
|
+
end
|
125
127
|
end
|
126
128
|
end
|
127
129
|
|
@@ -288,9 +290,11 @@ module Rodauth
|
|
288
290
|
end
|
289
291
|
|
290
292
|
def enable(*features)
|
291
|
-
|
292
|
-
|
293
|
-
|
293
|
+
features.each do |feature|
|
294
|
+
next if @auth.features.include?(feature)
|
295
|
+
load_feature(feature)
|
296
|
+
@auth.features << feature
|
297
|
+
end
|
294
298
|
end
|
295
299
|
|
296
300
|
private
|
@@ -111,7 +111,8 @@ module Rodauth
|
|
111
111
|
:account_from_session,
|
112
112
|
:field_attributes,
|
113
113
|
:field_error_attributes,
|
114
|
-
:formatted_field_error
|
114
|
+
:formatted_field_error,
|
115
|
+
:around_rodauth
|
115
116
|
)
|
116
117
|
|
117
118
|
configuration_module_eval do
|
@@ -459,6 +460,10 @@ 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
468
|
db.get(Sequel.function(function_name(name), hash_id, BCrypt::Engine.hash_secret(password, salt)))
|
464
469
|
end
|
@@ -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
|
data/lib/rodauth/features/jwt.rb
CHANGED
@@ -229,9 +229,13 @@ module Rodauth
|
|
229
229
|
end
|
230
230
|
end
|
231
231
|
|
232
|
+
def _jwt_decode_opts
|
233
|
+
jwt_decode_opts
|
234
|
+
end
|
235
|
+
|
232
236
|
def jwt_payload
|
233
237
|
return @jwt_payload if defined?(@jwt_payload)
|
234
|
-
@jwt_payload = JWT.decode(jwt_token, jwt_secret, true,
|
238
|
+
@jwt_payload = JWT.decode(jwt_token, jwt_secret, true, _jwt_decode_opts.merge(:algorithm=>jwt_algorithm))[0]
|
235
239
|
rescue JWT::DecodeError
|
236
240
|
@jwt_payload = false
|
237
241
|
end
|
@@ -7,6 +7,9 @@ module Rodauth
|
|
7
7
|
after 'refresh_token'
|
8
8
|
before 'refresh_token'
|
9
9
|
|
10
|
+
auth_value_method :allow_refresh_with_expired_jwt_access_token?, false
|
11
|
+
session_key :jwt_refresh_token_data_session_key, :jwt_refresh_token_data
|
12
|
+
session_key :jwt_refresh_token_hmac_session_key, :jwt_refresh_token_hash
|
10
13
|
auth_value_method :jwt_access_token_key, 'access_token'
|
11
14
|
auth_value_method :jwt_access_token_not_before_period, 5
|
12
15
|
auth_value_method :jwt_access_token_period, 1800
|
@@ -27,6 +30,7 @@ module Rodauth
|
|
27
30
|
)
|
28
31
|
|
29
32
|
route do |r|
|
33
|
+
@jwt_refresh_route = true
|
30
34
|
before_jwt_refresh_route
|
31
35
|
|
32
36
|
r.post do
|
@@ -38,6 +42,7 @@ module Rodauth
|
|
38
42
|
before_refresh_token
|
39
43
|
formatted_token = generate_refresh_token
|
40
44
|
remove_jwt_refresh_token_key(refresh_token)
|
45
|
+
set_jwt_refresh_token_hmac_session_key(formatted_token)
|
41
46
|
json_response[jwt_refresh_token_key] = formatted_token
|
42
47
|
json_response[jwt_access_token_key] = session_jwt
|
43
48
|
after_refresh_token
|
@@ -58,7 +63,9 @@ module Rodauth
|
|
58
63
|
# JWT login puts the access token in the header.
|
59
64
|
# We put the refresh token in the body.
|
60
65
|
# Note, do not put the access_token in the body here, as the access token content is not yet finalised.
|
61
|
-
json_response['refresh_token'] = generate_refresh_token
|
66
|
+
token = json_response['refresh_token'] = generate_refresh_token
|
67
|
+
|
68
|
+
set_jwt_refresh_token_hmac_session_key(token)
|
62
69
|
end
|
63
70
|
|
64
71
|
def set_jwt_token(token)
|
@@ -88,9 +95,13 @@ module Rodauth
|
|
88
95
|
def _account_from_refresh_token(token)
|
89
96
|
id, token_id, key = _account_refresh_token_split(token)
|
90
97
|
|
91
|
-
|
92
|
-
|
93
|
-
|
98
|
+
unless key &&
|
99
|
+
(id == session_value.to_s) &&
|
100
|
+
(actual = get_active_refresh_token(id, token_id)) &&
|
101
|
+
timing_safe_eql?(key, convert_token_key(actual)) &&
|
102
|
+
jwt_refresh_token_match?(key)
|
103
|
+
return
|
104
|
+
end
|
94
105
|
|
95
106
|
ds = account_ds(id)
|
96
107
|
ds = ds.where(account_status_column=>account_open_status_value) unless skip_status_checks?
|
@@ -107,6 +118,23 @@ module Rodauth
|
|
107
118
|
[id, token_id, key]
|
108
119
|
end
|
109
120
|
|
121
|
+
def _jwt_decode_opts
|
122
|
+
if allow_refresh_with_expired_jwt_access_token? && @jwt_refresh_route
|
123
|
+
Hash[super].merge!(:verify_expiration=>false)
|
124
|
+
else
|
125
|
+
super
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def jwt_refresh_token_match?(key)
|
130
|
+
# We don't need to match tokens if we are requiring a valid current access token
|
131
|
+
return true unless allow_refresh_with_expired_jwt_access_token?
|
132
|
+
|
133
|
+
# If allowing with expired jwt access token, check the expired session contains
|
134
|
+
# hmac matching submitted and active refresh token.
|
135
|
+
timing_safe_eql?(compute_hmac(session[jwt_refresh_token_data_session_key].to_s + key), session[jwt_refresh_token_hmac_session_key].to_s)
|
136
|
+
end
|
137
|
+
|
110
138
|
def get_active_refresh_token(account_id, token_id)
|
111
139
|
jwt_refresh_token_account_ds(account_id).
|
112
140
|
where(Sequel::CURRENT_TIMESTAMP > jwt_refresh_token_deadline_column).
|
@@ -146,6 +174,15 @@ module Rodauth
|
|
146
174
|
hash
|
147
175
|
end
|
148
176
|
|
177
|
+
def set_jwt_refresh_token_hmac_session_key(token)
|
178
|
+
if allow_refresh_with_expired_jwt_access_token?
|
179
|
+
key = _account_refresh_token_split(token).last
|
180
|
+
data = random_key
|
181
|
+
set_session_value(jwt_refresh_token_data_session_key, data)
|
182
|
+
set_session_value(jwt_refresh_token_hmac_session_key, compute_hmac(data + key))
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
149
186
|
def before_logout
|
150
187
|
if token = param_or_nil(jwt_refresh_token_key_param)
|
151
188
|
if token == 'all'
|
@@ -245,6 +245,12 @@ module Rodauth
|
|
245
245
|
end
|
246
246
|
end
|
247
247
|
|
248
|
+
def setup_account_verification
|
249
|
+
generate_verify_account_key_value
|
250
|
+
create_verify_account_key
|
251
|
+
send_verify_account_email
|
252
|
+
end
|
253
|
+
|
248
254
|
private
|
249
255
|
|
250
256
|
def _login_form_footer_links
|
@@ -276,12 +282,6 @@ module Rodauth
|
|
276
282
|
super
|
277
283
|
end
|
278
284
|
|
279
|
-
def setup_account_verification
|
280
|
-
generate_verify_account_key_value
|
281
|
-
create_verify_account_key
|
282
|
-
send_verify_account_email
|
283
|
-
end
|
284
|
-
|
285
285
|
def verify_account_check_already_logged_in
|
286
286
|
check_already_logged_in
|
287
287
|
end
|
data/lib/rodauth/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rodauth
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jeremy Evans
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-11-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: sequel
|
@@ -308,6 +308,7 @@ extra_rdoc_files:
|
|
308
308
|
- doc/release_notes/2.3.0.txt
|
309
309
|
- doc/release_notes/2.4.0.txt
|
310
310
|
- doc/release_notes/2.5.0.txt
|
311
|
+
- doc/release_notes/2.6.0.txt
|
311
312
|
files:
|
312
313
|
- CHANGELOG
|
313
314
|
- MIT-LICENSE
|
@@ -392,6 +393,7 @@ files:
|
|
392
393
|
- doc/release_notes/2.3.0.txt
|
393
394
|
- doc/release_notes/2.4.0.txt
|
394
395
|
- doc/release_notes/2.5.0.txt
|
396
|
+
- doc/release_notes/2.6.0.txt
|
395
397
|
- doc/remember.rdoc
|
396
398
|
- doc/reset_password.rdoc
|
397
399
|
- doc/session_expiration.rdoc
|