rodauth 2.4.0 → 2.9.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.
@@ -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
@@ -21,12 +24,15 @@ module Rodauth
21
24
  auth_value_method :jwt_refresh_token_table, :account_jwt_refresh_keys
22
25
  translatable_method :jwt_refresh_without_access_token_message, 'no JWT access token provided during refresh'
23
26
  auth_value_method :jwt_refresh_without_access_token_status, 401
27
+ translatable_method :expired_jwt_access_token_message, "expired JWT access token"
28
+ auth_value_method :expired_jwt_access_token_status, 400
24
29
 
25
30
  auth_private_methods(
26
31
  :account_from_refresh_token
27
32
  )
28
33
 
29
34
  route do |r|
35
+ @jwt_refresh_route = true
30
36
  before_jwt_refresh_route
31
37
 
32
38
  r.post do
@@ -38,17 +44,15 @@ module Rodauth
38
44
  before_refresh_token
39
45
  formatted_token = generate_refresh_token
40
46
  remove_jwt_refresh_token_key(refresh_token)
47
+ set_jwt_refresh_token_hmac_session_key(formatted_token)
41
48
  json_response[jwt_refresh_token_key] = formatted_token
42
49
  json_response[jwt_access_token_key] = session_jwt
43
50
  after_refresh_token
44
51
  end
45
52
  else
46
53
  json_response[json_response_error_key] = jwt_refresh_invalid_token_message
47
- response.status ||= json_response_error_status
48
54
  end
49
- response['Content-Type'] ||= json_response_content_type
50
- response.write(_json_response_body(json_response))
51
- request.halt
55
+ _return_json_response
52
56
  end
53
57
  end
54
58
 
@@ -58,7 +62,9 @@ module Rodauth
58
62
  # JWT login puts the access token in the header.
59
63
  # We put the refresh token in the body.
60
64
  # 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
65
+ token = json_response[jwt_refresh_token_key] = generate_refresh_token
66
+
67
+ set_jwt_refresh_token_hmac_session_key(token)
62
68
  end
63
69
 
64
70
  def set_jwt_token(token)
@@ -85,12 +91,33 @@ module Rodauth
85
91
 
86
92
  private
87
93
 
94
+ def rescue_jwt_payload(e)
95
+ if e.instance_of?(JWT::ExpiredSignature)
96
+ begin
97
+ # Some versions of jwt will raise JWT::ExpiredSignature even when the
98
+ # JWT is invalid for other reasons. Make sure the expiration is the
99
+ # only reason the JWT isn't valid before treating this as an expired token.
100
+ JWT.decode(jwt_token, jwt_secret, true, Hash[jwt_decode_opts].merge!(:verify_expiration=>false, :algorithm=>jwt_algorithm))[0]
101
+ rescue => e
102
+ else
103
+ json_response[json_response_error_key] = expired_jwt_access_token_message
104
+ response.status ||= expired_jwt_access_token_status
105
+ end
106
+ end
107
+
108
+ super
109
+ end
110
+
88
111
  def _account_from_refresh_token(token)
89
112
  id, token_id, key = _account_refresh_token_split(token)
90
113
 
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))
114
+ unless key &&
115
+ (id == session_value.to_s) &&
116
+ (actual = get_active_refresh_token(id, token_id)) &&
117
+ timing_safe_eql?(key, convert_token_key(actual)) &&
118
+ jwt_refresh_token_match?(key)
119
+ return
120
+ end
94
121
 
95
122
  ds = account_ds(id)
96
123
  ds = ds.where(account_status_column=>account_open_status_value) unless skip_status_checks?
@@ -107,6 +134,23 @@ module Rodauth
107
134
  [id, token_id, key]
108
135
  end
109
136
 
137
+ def _jwt_decode_opts
138
+ if allow_refresh_with_expired_jwt_access_token? && @jwt_refresh_route
139
+ Hash[super].merge!(:verify_expiration=>false)
140
+ else
141
+ super
142
+ end
143
+ end
144
+
145
+ def jwt_refresh_token_match?(key)
146
+ # We don't need to match tokens if we are requiring a valid current access token
147
+ return true unless allow_refresh_with_expired_jwt_access_token?
148
+
149
+ # If allowing with expired jwt access token, check the expired session contains
150
+ # hmac matching submitted and active refresh token.
151
+ timing_safe_eql?(compute_hmac(session[jwt_refresh_token_data_session_key].to_s + key), session[jwt_refresh_token_hmac_session_key].to_s)
152
+ end
153
+
110
154
  def get_active_refresh_token(account_id, token_id)
111
155
  jwt_refresh_token_account_ds(account_id).
112
156
  where(Sequel::CURRENT_TIMESTAMP > jwt_refresh_token_deadline_column).
@@ -130,8 +174,7 @@ module Rodauth
130
174
  end
131
175
 
132
176
  def remove_jwt_refresh_token_key(token)
133
- account_id, token = split_token(token)
134
- token_id, _ = split_token(token)
177
+ account_id, token_id, _ = _account_refresh_token_split(token)
135
178
  jwt_refresh_token_account_token_ds(account_id, token_id).delete
136
179
  end
137
180
 
@@ -146,6 +189,15 @@ module Rodauth
146
189
  hash
147
190
  end
148
191
 
192
+ def set_jwt_refresh_token_hmac_session_key(token)
193
+ if allow_refresh_with_expired_jwt_access_token?
194
+ key = _account_refresh_token_split(token).last
195
+ data = random_key
196
+ set_session_value(jwt_refresh_token_data_session_key, data)
197
+ set_session_value(jwt_refresh_token_hmac_session_key, compute_hmac(data + key))
198
+ end
199
+ end
200
+
149
201
  def before_logout
150
202
  if token = param_or_nil(jwt_refresh_token_key_param)
151
203
  if token == 'all'
@@ -154,9 +206,7 @@ module Rodauth
154
206
  id, token_id, key = _account_refresh_token_split(token)
155
207
 
156
208
  if id && token_id && key && (actual = get_active_refresh_token(session_value, token_id)) && timing_safe_eql?(key, convert_token_key(actual))
157
- jwt_refresh_token_account_ds(id).
158
- where(jwt_refresh_token_id_column=>token_id).
159
- delete
209
+ jwt_refresh_token_account_token_ds(id, token_id).delete
160
210
  end
161
211
  end
162
212
  end
@@ -23,6 +23,8 @@ module Rodauth
23
23
  auth_cached_method :login_form_footer_links
24
24
  auth_cached_method :login_form_footer
25
25
 
26
+ auth_value_methods :login_return_to_requested_location_path
27
+
26
28
  route do |r|
27
29
  check_already_logged_in
28
30
  before_login_route
@@ -85,12 +87,16 @@ module Rodauth
85
87
  end
86
88
 
87
89
  def login_required
88
- if login_return_to_requested_location?
89
- set_session_value(login_redirect_session_key, request.fullpath)
90
+ if login_return_to_requested_location? && (path = login_return_to_requested_location_path)
91
+ set_session_value(login_redirect_session_key, path)
90
92
  end
91
93
  super
92
94
  end
93
95
 
96
+ def login_return_to_requested_location_path
97
+ request.fullpath if request.get?
98
+ end
99
+
94
100
  def after_login_entered_during_multi_phase_login
95
101
  set_notice_now_flash need_password_notice_flash
96
102
  if multi_phase_login_forms.length == 1 && (meth = multi_phase_login_forms[0][2])
@@ -76,9 +76,7 @@ module Rodauth
76
76
  )
77
77
 
78
78
  auth_methods(
79
- :otp,
80
79
  :otp_exists?,
81
- :otp_key,
82
80
  :otp_last_use,
83
81
  :otp_locked_out?,
84
82
  :otp_new_secret,
@@ -34,6 +34,7 @@ module Rodauth
34
34
  auth_value_method :add_recovery_codes_param, 'add'
35
35
  translatable_method :add_recovery_codes_heading, '<h2>Add Additional Recovery Codes</h2>'
36
36
  auth_value_method :auto_add_recovery_codes?, false
37
+ auth_value_method :auto_remove_recovery_codes?, false
37
38
  translatable_method :invalid_recovery_code_message, "Invalid recovery code"
38
39
  auth_value_method :recovery_codes_limit, 16
39
40
  auth_value_method :recovery_codes_column, :code
@@ -56,7 +57,6 @@ module Rodauth
56
57
  :can_add_recovery_codes?,
57
58
  :new_recovery_code,
58
59
  :recovery_code_match?,
59
- :recovery_codes
60
60
  )
61
61
 
62
62
  route(:recovery_auth) do |r|
@@ -213,6 +213,21 @@ module Rodauth
213
213
  super
214
214
  end
215
215
 
216
+ def after_otp_disable
217
+ super if defined?(super)
218
+ auto_remove_recovery_codes
219
+ end
220
+
221
+ def after_sms_disable
222
+ super if defined?(super)
223
+ auto_remove_recovery_codes
224
+ end
225
+
226
+ def after_webauthn_remove
227
+ super if defined?(super)
228
+ auto_remove_recovery_codes
229
+ end
230
+
216
231
  def new_recovery_code
217
232
  random_key
218
233
  end
@@ -227,6 +242,12 @@ module Rodauth
227
242
  end
228
243
  end
229
244
 
245
+ def auto_remove_recovery_codes
246
+ if auto_remove_recovery_codes? && (%w'totp webauthn sms_code' & possible_authentication_methods).empty?
247
+ recovery_codes_remove
248
+ end
249
+ end
250
+
230
251
  def _recovery_codes
231
252
  recovery_codes_ds.select_map(recovery_codes_column)
232
253
  end
@@ -132,11 +132,16 @@ module Rodauth
132
132
  opts = Hash[remember_cookie_options]
133
133
  opts[:value] = "#{account_id}_#{convert_token_key(remember_key_value)}"
134
134
  opts[:expires] = convert_timestamp(active_remember_key_ds.get(remember_deadline_column))
135
+ opts[:path] = "/" unless opts.key?(:path)
136
+ opts[:httponly] = true unless opts.key?(:httponly)
137
+ opts[:secure] = true unless opts.key?(:secure) || !request.ssl?
135
138
  ::Rack::Utils.set_cookie_header!(response.headers, remember_cookie_key, opts)
136
139
  end
137
140
 
138
141
  def forget_login
139
- ::Rack::Utils.delete_cookie_header!(response.headers, remember_cookie_key, remember_cookie_options)
142
+ opts = Hash[remember_cookie_options]
143
+ opts[:path] = "/" unless opts.key?(:path)
144
+ ::Rack::Utils.delete_cookie_header!(response.headers, remember_cookie_key, opts)
140
145
  end
141
146
 
142
147
  def get_remember_key
@@ -47,7 +47,6 @@ module Rodauth
47
47
  :get_verify_account_key,
48
48
  :get_verify_account_email_last_sent,
49
49
  :remove_verify_account_key,
50
- :resend_verify_account_view,
51
50
  :send_verify_account_email,
52
51
  :set_verify_account_email_last_sent,
53
52
  :verify_account,
@@ -245,6 +244,12 @@ module Rodauth
245
244
  end
246
245
  end
247
246
 
247
+ def setup_account_verification
248
+ generate_verify_account_key_value
249
+ create_verify_account_key
250
+ send_verify_account_email
251
+ end
252
+
248
253
  private
249
254
 
250
255
  def _login_form_footer_links
@@ -276,12 +281,6 @@ module Rodauth
276
281
  super
277
282
  end
278
283
 
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
284
  def verify_account_check_already_logged_in
286
285
  check_already_logged_in
287
286
  end
@@ -8,6 +8,7 @@ module Rodauth
8
8
  error_flash "Unable to change login as there is already an account with the new login", 'verify_login_change_duplicate_account'
9
9
  error_flash "There was an error verifying your login change: invalid verify login change key", 'no_matching_verify_login_change_key'
10
10
  notice_flash "Your login change has been verified"
11
+ notice_flash "An email has been sent to you with a link to verify your login change", 'change_login_needs_verification'
11
12
  loaded_templates %w'verify-login-change verify-login-change-email'
12
13
  view 'verify-login-change', 'Verify Login Change'
13
14
  additional_form_tags
@@ -131,7 +132,7 @@ module Rodauth
131
132
  end
132
133
 
133
134
  def change_login_notice_flash
134
- "An email has been sent to you with a link to verify your login change"
135
+ change_login_needs_verification_notice_flash
135
136
  end
136
137
 
137
138
  def verify_login_change_old_login
@@ -29,7 +29,7 @@ module Rodauth
29
29
 
30
30
  def before_verify_account
31
31
  super
32
- if features.include?(:jwt) && use_jwt? && !param_or_nil(webauthn_setup_param)
32
+ if features.include?(:json) && use_json? && !param_or_nil(webauthn_setup_param)
33
33
  cred = new_webauthn_credential
34
34
  json_response[webauthn_setup_param] = cred.as_json
35
35
  json_response[webauthn_setup_challenge_param] = cred.challenge
@@ -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 = 4
9
+ MINOR = 9
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.4.0
4
+ version: 2.9.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-09-21 00:00:00.000000000 Z
11
+ date: 2021-01-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sequel
@@ -237,28 +237,34 @@ extra_rdoc_files:
237
237
  - README.rdoc
238
238
  - CHANGELOG
239
239
  - MIT-LICENSE
240
- - doc/change_password_notify.rdoc
241
240
  - doc/account_expiration.rdoc
241
+ - doc/active_sessions.rdoc
242
+ - doc/audit_logging.rdoc
242
243
  - doc/base.rdoc
243
244
  - doc/change_login.rdoc
244
245
  - doc/change_password.rdoc
245
- - doc/confirm_password.rdoc
246
+ - doc/change_password_notify.rdoc
246
247
  - doc/close_account.rdoc
247
- - doc/http_basic_auth.rdoc
248
+ - doc/confirm_password.rdoc
248
249
  - doc/create_account.rdoc
249
- - doc/email_base.rdoc
250
250
  - doc/disallow_common_passwords.rdoc
251
251
  - doc/disallow_password_reuse.rdoc
252
- - doc/password_complexity.rdoc
252
+ - doc/email_auth.rdoc
253
+ - doc/email_base.rdoc
254
+ - doc/http_basic_auth.rdoc
255
+ - doc/json.rdoc
253
256
  - doc/jwt.rdoc
257
+ - doc/jwt_cors.rdoc
258
+ - doc/jwt_refresh.rdoc
254
259
  - doc/lockout.rdoc
255
260
  - doc/login.rdoc
261
+ - doc/login_password_requirements_base.rdoc
256
262
  - doc/logout.rdoc
257
263
  - doc/otp.rdoc
258
- - doc/login_password_requirements_base.rdoc
259
- - doc/jwt_cors.rdoc
264
+ - doc/password_complexity.rdoc
260
265
  - doc/password_expiration.rdoc
261
266
  - doc/password_grace_period.rdoc
267
+ - doc/password_pepper.rdoc
262
268
  - doc/recovery_codes.rdoc
263
269
  - doc/remember.rdoc
264
270
  - doc/reset_password.rdoc
@@ -268,17 +274,11 @@ extra_rdoc_files:
268
274
  - doc/two_factor_base.rdoc
269
275
  - doc/update_password_hash.rdoc
270
276
  - doc/verify_account.rdoc
271
- - doc/email_auth.rdoc
272
- - doc/jwt_refresh.rdoc
273
277
  - doc/verify_account_grace_period.rdoc
274
278
  - doc/verify_login_change.rdoc
275
279
  - doc/webauthn.rdoc
276
280
  - doc/webauthn_login.rdoc
277
281
  - doc/webauthn_verify_account.rdoc
278
- - doc/active_sessions.rdoc
279
- - doc/audit_logging.rdoc
280
- - doc/password_pepper.rdoc
281
- - doc/release_notes/1.17.0.txt
282
282
  - doc/release_notes/1.0.0.txt
283
283
  - doc/release_notes/1.1.0.txt
284
284
  - doc/release_notes/1.10.0.txt
@@ -288,7 +288,14 @@ extra_rdoc_files:
288
288
  - doc/release_notes/1.14.0.txt
289
289
  - doc/release_notes/1.15.0.txt
290
290
  - doc/release_notes/1.16.0.txt
291
+ - doc/release_notes/1.17.0.txt
292
+ - doc/release_notes/1.18.0.txt
293
+ - doc/release_notes/1.19.0.txt
291
294
  - doc/release_notes/1.2.0.txt
295
+ - doc/release_notes/1.20.0.txt
296
+ - doc/release_notes/1.21.0.txt
297
+ - doc/release_notes/1.22.0.txt
298
+ - doc/release_notes/1.23.0.txt
292
299
  - doc/release_notes/1.3.0.txt
293
300
  - doc/release_notes/1.4.0.txt
294
301
  - doc/release_notes/1.5.0.txt
@@ -296,17 +303,16 @@ extra_rdoc_files:
296
303
  - doc/release_notes/1.7.0.txt
297
304
  - doc/release_notes/1.8.0.txt
298
305
  - doc/release_notes/1.9.0.txt
299
- - doc/release_notes/1.18.0.txt
300
- - doc/release_notes/1.19.0.txt
301
- - doc/release_notes/1.20.0.txt
302
- - doc/release_notes/1.21.0.txt
303
- - doc/release_notes/1.22.0.txt
304
- - doc/release_notes/1.23.0.txt
305
306
  - doc/release_notes/2.0.0.txt
306
307
  - doc/release_notes/2.1.0.txt
307
308
  - doc/release_notes/2.2.0.txt
308
309
  - doc/release_notes/2.3.0.txt
309
310
  - doc/release_notes/2.4.0.txt
311
+ - doc/release_notes/2.5.0.txt
312
+ - doc/release_notes/2.6.0.txt
313
+ - doc/release_notes/2.7.0.txt
314
+ - doc/release_notes/2.8.0.txt
315
+ - doc/release_notes/2.9.0.txt
310
316
  files:
311
317
  - CHANGELOG
312
318
  - MIT-LICENSE
@@ -348,6 +354,7 @@ files:
348
354
  - doc/guides/status_column.rdoc
349
355
  - doc/guides/totp_or_recovery.rdoc
350
356
  - doc/http_basic_auth.rdoc
357
+ - doc/json.rdoc
351
358
  - doc/jwt.rdoc
352
359
  - doc/jwt_cors.rdoc
353
360
  - doc/jwt_refresh.rdoc
@@ -390,6 +397,11 @@ files:
390
397
  - doc/release_notes/2.2.0.txt
391
398
  - doc/release_notes/2.3.0.txt
392
399
  - doc/release_notes/2.4.0.txt
400
+ - doc/release_notes/2.5.0.txt
401
+ - doc/release_notes/2.6.0.txt
402
+ - doc/release_notes/2.7.0.txt
403
+ - doc/release_notes/2.8.0.txt
404
+ - doc/release_notes/2.9.0.txt
393
405
  - doc/remember.rdoc
394
406
  - doc/reset_password.rdoc
395
407
  - doc/session_expiration.rdoc
@@ -422,6 +434,7 @@ files:
422
434
  - lib/rodauth/features/email_auth.rb
423
435
  - lib/rodauth/features/email_base.rb
424
436
  - lib/rodauth/features/http_basic_auth.rb
437
+ - lib/rodauth/features/json.rb
425
438
  - lib/rodauth/features/jwt.rb
426
439
  - lib/rodauth/features/jwt_cors.rb
427
440
  - lib/rodauth/features/jwt_refresh.rb
@@ -533,7 +546,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
533
546
  - !ruby/object:Gem::Version
534
547
  version: '0'
535
548
  requirements: []
536
- rubygems_version: 3.1.2
549
+ rubygems_version: 3.2.3
537
550
  signing_key:
538
551
  specification_version: 4
539
552
  summary: Authentication and Account Management Framework for Rack Applications