rodauth 2.5.0 → 2.6.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|