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 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