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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e85b192c01a27f50a917584cb3d0f09e947152c116b022f955d2c7c03ff43f7a
4
- data.tar.gz: a4d87aa35b1ae50b5901495b8062ad3a2ad25695b46d4fa91b30d8439a4b7ab1
3
+ metadata.gz: 82585797ff882cb56340e2145b05b74da1e10c428413b02e1656604fa8209c93
4
+ data.tar.gz: 7148d04c255f4310a21c6373d9ab20888e8fe46d3a07c090576385890ea82858
5
5
  SHA512:
6
- metadata.gz: '000885791eb76a2f283e2f243d281c6c103cdd35ad3b8e15b2510d20e7a05db19bee9a30a5ca9d2fc536a46007fe169674aa799698ad83eac292fd5475d9beb8'
7
- data.tar.gz: c73dfb2edd91f6a2d134494504ca0042f7357380ea9dbcb495b7265d25c0fdc62a8cadb07cf5182e330e1bf935c608e9f01f943dfdb21084bf08147848b67026
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)
@@ -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.
@@ -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.
@@ -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);
@@ -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
- before_rodauth
124
- send(internal_handle_meth, request)
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
- new_features = features - @auth.features
292
- new_features.each{|f| load_feature(f)}
293
- @auth.features.concat(new_features)
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
- 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
@@ -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, jwt_decode_opts.merge(:algorithm=>jwt_algorithm))[0]
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
- return unless key
92
- return unless actual = get_active_refresh_token(id, token_id)
93
- return unless timing_safe_eql?(key, convert_token_key(actual))
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
@@ -6,7 +6,7 @@ module Rodauth
6
6
  MAJOR = 2
7
7
 
8
8
  # The minor version of Rodauth, updated for new feature releases of Rodauth.
9
- MINOR = 5
9
+ MINOR = 6
10
10
 
11
11
  # The patch version of Rodauth, updated only for bug fixes from the last
12
12
  # feature release.
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.5.0
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-10-22 00:00:00.000000000 Z
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