rodauth 2.1.0 → 2.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +56 -0
  3. data/README.rdoc +14 -0
  4. data/doc/base.rdoc +3 -1
  5. data/doc/guides/admin_activation.rdoc +46 -0
  6. data/doc/guides/already_authenticated.rdoc +10 -0
  7. data/doc/guides/alternative_login.rdoc +46 -0
  8. data/doc/guides/create_account_programmatically.rdoc +38 -0
  9. data/doc/guides/delay_password.rdoc +25 -0
  10. data/doc/guides/email_only.rdoc +16 -0
  11. data/doc/guides/i18n.rdoc +26 -0
  12. data/doc/{internals.rdoc → guides/internals.rdoc} +0 -0
  13. data/doc/guides/links.rdoc +12 -0
  14. data/doc/guides/login_return.rdoc +37 -0
  15. data/doc/guides/password_column.rdoc +25 -0
  16. data/doc/guides/password_confirmation.rdoc +37 -0
  17. data/doc/guides/password_requirements.rdoc +30 -0
  18. data/doc/guides/paths.rdoc +36 -0
  19. data/doc/guides/query_params.rdoc +9 -0
  20. data/doc/guides/redirects.rdoc +17 -0
  21. data/doc/guides/registration_field.rdoc +68 -0
  22. data/doc/guides/require_mfa.rdoc +30 -0
  23. data/doc/guides/reset_password_autologin.rdoc +21 -0
  24. data/doc/guides/status_column.rdoc +28 -0
  25. data/doc/guides/totp_or_recovery.rdoc +16 -0
  26. data/doc/jwt_refresh.rdoc +17 -0
  27. data/doc/login.rdoc +8 -0
  28. data/doc/login_password_requirements_base.rdoc +3 -0
  29. data/doc/otp.rdoc +1 -0
  30. data/doc/password_pepper.rdoc +44 -0
  31. data/doc/release_notes/2.2.0.txt +39 -0
  32. data/doc/release_notes/2.3.0.txt +37 -0
  33. data/doc/release_notes/2.4.0.txt +22 -0
  34. data/doc/release_notes/2.5.0.txt +20 -0
  35. data/doc/release_notes/2.6.0.txt +37 -0
  36. data/doc/verify_login_change.rdoc +1 -0
  37. data/javascript/webauthn_auth.js +9 -9
  38. data/javascript/webauthn_setup.js +9 -6
  39. data/lib/rodauth.rb +13 -9
  40. data/lib/rodauth/features/active_sessions.rb +5 -7
  41. data/lib/rodauth/features/audit_logging.rb +2 -0
  42. data/lib/rodauth/features/base.rb +18 -3
  43. data/lib/rodauth/features/change_password.rb +1 -1
  44. data/lib/rodauth/features/close_account.rb +8 -6
  45. data/lib/rodauth/features/confirm_password.rb +2 -2
  46. data/lib/rodauth/features/disallow_password_reuse.rb +4 -2
  47. data/lib/rodauth/features/email_auth.rb +2 -2
  48. data/lib/rodauth/features/jwt.rb +10 -7
  49. data/lib/rodauth/features/jwt_cors.rb +15 -15
  50. data/lib/rodauth/features/jwt_refresh.rb +76 -10
  51. data/lib/rodauth/features/login.rb +23 -12
  52. data/lib/rodauth/features/login_password_requirements_base.rb +9 -4
  53. data/lib/rodauth/features/otp.rb +5 -1
  54. data/lib/rodauth/features/password_complexity.rb +4 -2
  55. data/lib/rodauth/features/password_pepper.rb +45 -0
  56. data/lib/rodauth/features/remember.rb +2 -0
  57. data/lib/rodauth/features/session_expiration.rb +1 -6
  58. data/lib/rodauth/features/single_session.rb +1 -1
  59. data/lib/rodauth/features/sms_codes.rb +0 -1
  60. data/lib/rodauth/features/two_factor_base.rb +4 -4
  61. data/lib/rodauth/features/verify_account.rb +10 -6
  62. data/lib/rodauth/features/verify_account_grace_period.rb +2 -4
  63. data/lib/rodauth/features/verify_login_change.rb +2 -1
  64. data/lib/rodauth/features/webauthn.rb +1 -3
  65. data/lib/rodauth/features/webauthn_login.rb +1 -1
  66. data/lib/rodauth/migrations.rb +16 -5
  67. data/lib/rodauth/version.rb +1 -1
  68. metadata +37 -5
@@ -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
@@ -19,23 +22,31 @@ module Rodauth
19
22
  auth_value_method :jwt_refresh_token_key_column, :key
20
23
  auth_value_method :jwt_refresh_token_key_param, 'refresh_token'
21
24
  auth_value_method :jwt_refresh_token_table, :account_jwt_refresh_keys
25
+ translatable_method :jwt_refresh_without_access_token_message, 'no JWT access token provided during refresh'
26
+ auth_value_method :jwt_refresh_without_access_token_status, 401
22
27
 
23
28
  auth_private_methods(
24
29
  :account_from_refresh_token
25
30
  )
26
31
 
27
32
  route do |r|
33
+ @jwt_refresh_route = true
34
+ before_jwt_refresh_route
35
+
28
36
  r.post do
29
- if (refresh_token = param_or_nil(jwt_refresh_token_key_param)) && account_from_refresh_token(refresh_token)
30
- formatted_token = nil
37
+ if !session_value
38
+ response.status ||= jwt_refresh_without_access_token_status
39
+ json_response[json_response_error_key] = jwt_refresh_without_access_token_message
40
+ elsif (refresh_token = param_or_nil(jwt_refresh_token_key_param)) && account_from_refresh_token(refresh_token)
31
41
  transaction do
32
42
  before_refresh_token
33
43
  formatted_token = generate_refresh_token
34
44
  remove_jwt_refresh_token_key(refresh_token)
45
+ set_jwt_refresh_token_hmac_session_key(formatted_token)
46
+ json_response[jwt_refresh_token_key] = formatted_token
47
+ json_response[jwt_access_token_key] = session_jwt
35
48
  after_refresh_token
36
49
  end
37
- json_response[jwt_refresh_token_key] = formatted_token
38
- json_response[jwt_access_token_key] = session_jwt
39
50
  else
40
51
  json_response[json_response_error_key] = jwt_refresh_invalid_token_message
41
52
  response.status ||= json_response_error_status
@@ -52,7 +63,9 @@ module Rodauth
52
63
  # JWT login puts the access token in the header.
53
64
  # We put the refresh token in the body.
54
65
  # Note, do not put the access_token in the body here, as the access token content is not yet finalised.
55
- 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)
56
69
  end
57
70
 
58
71
  def set_jwt_token(token)
@@ -80,19 +93,46 @@ module Rodauth
80
93
  private
81
94
 
82
95
  def _account_from_refresh_token(token)
96
+ id, token_id, key = _account_refresh_token_split(token)
97
+
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
105
+
106
+ ds = account_ds(id)
107
+ ds = ds.where(account_status_column=>account_open_status_value) unless skip_status_checks?
108
+ ds.first
109
+ end
110
+
111
+ def _account_refresh_token_split(token)
83
112
  id, token = split_token(token)
84
113
  return unless id && token
85
114
 
86
115
  token_id, key = split_token(token)
87
116
  return unless token_id && key
88
117
 
89
- return unless actual = get_active_refresh_token(id, token_id)
118
+ [id, token_id, key]
119
+ end
90
120
 
91
- return unless timing_safe_eql?(key, convert_token_key(actual))
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
92
128
 
93
- ds = account_ds(id)
94
- ds = ds.where(account_status_column=>account_open_status_value) unless skip_status_checks?
95
- ds.first
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)
96
136
  end
97
137
 
98
138
  def get_active_refresh_token(account_id, token_id)
@@ -134,6 +174,32 @@ module Rodauth
134
174
  hash
135
175
  end
136
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
+
186
+ def before_logout
187
+ if token = param_or_nil(jwt_refresh_token_key_param)
188
+ if token == 'all'
189
+ jwt_refresh_token_account_ds(session_value).delete
190
+ else
191
+ id, token_id, key = _account_refresh_token_split(token)
192
+
193
+ if id && token_id && key && (actual = get_active_refresh_token(session_value, token_id)) && timing_safe_eql?(key, convert_token_key(actual))
194
+ jwt_refresh_token_account_ds(id).
195
+ where(jwt_refresh_token_id_column=>token_id).
196
+ delete
197
+ end
198
+ end
199
+ end
200
+ super if defined?(super)
201
+ end
202
+
137
203
  def after_close_account
138
204
  jwt_refresh_token_account_ds(account_id).delete
139
205
  super if defined?(super)
@@ -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
@@ -62,7 +64,7 @@ module Rodauth
62
64
  throw_error_status(login_error_status, password_param, invalid_password_message)
63
65
  end
64
66
 
65
- _login('password')
67
+ login('password')
66
68
  end
67
69
 
68
70
  set_error_flash login_error_flash unless skip_error_flash
@@ -72,13 +74,29 @@ module Rodauth
72
74
 
73
75
  attr_reader :login_form_header
74
76
 
77
+ def login(auth_type)
78
+ saved_login_redirect = remove_session_value(login_redirect_session_key)
79
+ transaction do
80
+ before_login
81
+ login_session(auth_type)
82
+ yield if block_given?
83
+ after_login
84
+ end
85
+ set_notice_flash login_notice_flash
86
+ redirect(saved_login_redirect || login_redirect)
87
+ end
88
+
75
89
  def login_required
76
- if login_return_to_requested_location?
77
- 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)
78
92
  end
79
93
  super
80
94
  end
81
95
 
96
+ def login_return_to_requested_location_path
97
+ request.fullpath if request.get?
98
+ end
99
+
82
100
  def after_login_entered_during_multi_phase_login
83
101
  set_notice_now_flash need_password_notice_flash
84
102
  if multi_phase_login_forms.length == 1 && (meth = multi_phase_login_forms[0][2])
@@ -126,15 +144,8 @@ module Rodauth
126
144
  end
127
145
 
128
146
  def _login(auth_type)
129
- saved_login_redirect = remove_session_value(login_redirect_session_key)
130
- transaction do
131
- before_login
132
- login_session(auth_type)
133
- yield if block_given?
134
- after_login
135
- end
136
- set_notice_flash login_notice_flash
137
- redirect(saved_login_redirect || login_redirect)
147
+ warn("Deprecated #_login method called, use #login instead.")
148
+ login(auth_type)
138
149
  end
139
150
  end
140
151
  end
@@ -4,8 +4,10 @@ module Rodauth
4
4
  Feature.define(:login_password_requirements_base, :LoginPasswordRequirementsBase) do
5
5
  translatable_method :already_an_account_with_this_login_message, 'already an account with this login'
6
6
  auth_value_method :login_confirm_param, 'login-confirm'
7
+ auth_value_method :login_email_regexp, /\A[^,;@ \r\n]+@[^,@; \r\n]+\.[^,@; \r\n]+\z/
7
8
  auth_value_method :login_minimum_length, 3
8
9
  auth_value_method :login_maximum_length, 255
10
+ translatable_method :login_not_valid_email_message, 'not a valid email address'
9
11
  translatable_method :logins_do_not_match_message, 'logins do not match'
10
12
  auth_value_method :password_confirm_param, 'password-confirm'
11
13
  auth_value_method :password_minimum_length, 6
@@ -28,6 +30,7 @@ module Rodauth
28
30
 
29
31
  auth_methods(
30
32
  :login_meets_requirements?,
33
+ :login_valid_email?,
31
34
  :password_hash,
32
35
  :password_meets_requirements?,
33
36
  :set_password
@@ -104,13 +107,15 @@ module Rodauth
104
107
 
105
108
  def login_meets_email_requirements?(login)
106
109
  return true unless require_email_address_logins?
107
- if login =~ /\A[^,;@ \r\n]+@[^,@; \r\n]+\.[^,@; \r\n]+\z/
108
- return true
109
- end
110
- @login_requirement_message = 'not a valid email address'
110
+ return true if login_valid_email?(login)
111
+ @login_requirement_message = login_not_valid_email_message
111
112
  return false
112
113
  end
113
114
 
115
+ def login_valid_email?(login)
116
+ login =~ login_email_regexp
117
+ end
118
+
114
119
  def password_meets_length_requirements?(password)
115
120
  return true if password_minimum_length <= password.length
116
121
  @password_requirement_message = password_too_short_message
@@ -79,6 +79,7 @@ module Rodauth
79
79
  :otp,
80
80
  :otp_exists?,
81
81
  :otp_key,
82
+ :otp_last_use,
82
83
  :otp_locked_out?,
83
84
  :otp_new_secret,
84
85
  :otp_provisioning_name,
@@ -255,7 +256,6 @@ module Rodauth
255
256
  def otp_remove
256
257
  otp_key_ds.delete
257
258
  @otp_key = nil
258
- super if defined?(super)
259
259
  end
260
260
 
261
261
  def otp_add_key
@@ -269,6 +269,10 @@ module Rodauth
269
269
  update(otp_keys_last_use_column=>Sequel::CURRENT_TIMESTAMP) == 1
270
270
  end
271
271
 
272
+ def otp_last_use
273
+ convert_timestamp(otp_key_ds.get(otp_keys_last_use_column))
274
+ end
275
+
272
276
  def otp_record_authentication_failure
273
277
  otp_key_ds.update(otp_keys_failures_column=>Sequel.identifier(otp_keys_failures_column) + 1)
274
278
  end
@@ -26,14 +26,16 @@ module Rodauth
26
26
 
27
27
  def post_configure
28
28
  super
29
- return if singleton_methods.map(&:to_sym).include?(:password_dictionary)
29
+ return if method(:password_dictionary).owner != Rodauth::PasswordComplexity
30
30
 
31
31
  case password_dictionary_file
32
32
  when false
33
- return
33
+ # nothing
34
34
  when nil
35
35
  default_dictionary_file = '/usr/share/dict/words'
36
+ # :nocov:
36
37
  if File.file?(default_dictionary_file)
38
+ # :nocov:
37
39
  words = File.read(default_dictionary_file)
38
40
  end
39
41
  else
@@ -0,0 +1,45 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Rodauth
4
+ Feature.define(:password_pepper, :PasswordPepper) do
5
+ depends :login_password_requirements_base
6
+
7
+ auth_value_method :password_pepper, nil
8
+ auth_value_method :previous_password_peppers, [""]
9
+ auth_value_method :password_pepper_update?, true
10
+
11
+ def password_match?(password)
12
+ if (result = super) && @previous_pepper_matched && password_pepper_update?
13
+ set_password(password)
14
+ end
15
+
16
+ result
17
+ end
18
+
19
+ private
20
+
21
+ def password_hash(password)
22
+ super(password + password_pepper.to_s)
23
+ end
24
+
25
+ def password_hash_match?(hash, password)
26
+ return super if password_pepper.nil?
27
+
28
+ return true if super(hash, password + password_pepper)
29
+
30
+ @previous_pepper_matched = previous_password_peppers.any? do |pepper|
31
+ super(hash, password + pepper)
32
+ end
33
+ end
34
+
35
+ def database_function_password_match?(name, hash_id, password, salt)
36
+ return super if password_pepper.nil?
37
+
38
+ return true if super(name, hash_id, password + password_pepper, salt)
39
+
40
+ @previous_pepper_matched = previous_password_peppers.any? do |pepper|
41
+ super(name, hash_id, password + pepper, salt)
42
+ end
43
+ end
44
+ end
45
+ end
@@ -58,7 +58,9 @@ module Rodauth
58
58
  if [remember_remember_param_value, remember_forget_param_value, remember_disable_param_value].include?(remember)
59
59
  transaction do
60
60
  before_remember
61
+ # :nocov:
61
62
  case remember
63
+ # :nocov:
62
64
  when remember_remember_param_value
63
65
  remember_login
64
66
  when remember_forget_param_value
@@ -3,6 +3,7 @@
3
3
  module Rodauth
4
4
  Feature.define(:session_expiration, :SessionExpiration) do
5
5
  error_flash "This session has expired, please login again"
6
+ redirect{require_login_redirect}
6
7
 
7
8
  auth_value_method :max_session_lifetime, 86400
8
9
  session_key :session_created_session_key, :session_created_at
@@ -11,8 +12,6 @@ module Rodauth
11
12
  auth_value_method :session_inactivity_timeout, 1800
12
13
  session_key :session_last_activity_session_key, :last_session_activity_at
13
14
 
14
- auth_value_methods :session_expiration_redirect
15
-
16
15
  def check_session_expiration
17
16
  return unless logged_in?
18
17
 
@@ -43,10 +42,6 @@ module Rodauth
43
42
  redirect session_expiration_redirect
44
43
  end
45
44
 
46
- def session_expiration_redirect
47
- require_login_redirect
48
- end
49
-
50
45
  def update_session
51
46
  super
52
47
  t = Time.now.to_i
@@ -85,7 +85,7 @@ module Rodauth
85
85
  end
86
86
 
87
87
  def before_logout
88
- reset_single_session_key if request.post?
88
+ reset_single_session_key
89
89
  super if defined?(super)
90
90
  end
91
91
 
@@ -337,7 +337,6 @@ module Rodauth
337
337
  def sms_disable
338
338
  sms_ds.delete
339
339
  @sms = nil
340
- super if defined?(super)
341
340
  end
342
341
 
343
342
  def sms_confirm_failure
@@ -125,7 +125,7 @@ module Rodauth
125
125
  return true if two_factor_authenticated?
126
126
 
127
127
  # True if authenticated via single factor and 2nd factor not setup
128
- !two_factor_authentication_setup?
128
+ !uses_two_factor_authentication?
129
129
  end
130
130
 
131
131
  def require_authentication
@@ -134,7 +134,7 @@ module Rodauth
134
134
  # Avoid database query if already authenticated via 2nd factor
135
135
  return if two_factor_authenticated?
136
136
 
137
- require_two_factor_authenticated if two_factor_authentication_setup?
137
+ require_two_factor_authenticated if uses_two_factor_authentication?
138
138
  end
139
139
 
140
140
  def require_two_factor_setup
@@ -208,11 +208,11 @@ module Rodauth
208
208
  end
209
209
 
210
210
  def _two_factor_setup_links
211
- (super if defined?(super)) || []
211
+ []
212
212
  end
213
213
 
214
214
  def _two_factor_remove_links
215
- (super if defined?(super)) || []
215
+ []
216
216
  end
217
217
 
218
218
  def _two_factor_remove_all_from_session
@@ -70,6 +70,7 @@ module Rodauth
70
70
  end
71
71
 
72
72
  r.post do
73
+ verified = false
73
74
  if account_from_login(param(login_param)) && allow_resending_verify_account_email?
74
75
  if verify_account_email_recently_sent?
75
76
  set_redirect_error_flash verify_account_email_recently_sent_error_flash
@@ -79,8 +80,11 @@ module Rodauth
79
80
  before_verify_account_email_resend
80
81
  if verify_account_email_resend
81
82
  after_verify_account_email_resend
83
+ verified = true
82
84
  end
85
+ end
83
86
 
87
+ if verified
84
88
  set_notice_flash verify_account_email_sent_notice_flash
85
89
  else
86
90
  set_redirect_error_status(no_matching_login_error_status)
@@ -241,6 +245,12 @@ module Rodauth
241
245
  end
242
246
  end
243
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
+
244
254
  private
245
255
 
246
256
  def _login_form_footer_links
@@ -272,12 +282,6 @@ module Rodauth
272
282
  super
273
283
  end
274
284
 
275
- def setup_account_verification
276
- generate_verify_account_key_value
277
- create_verify_account_key
278
- send_verify_account_email
279
- end
280
-
281
285
  def verify_account_check_already_logged_in
282
286
  check_already_logged_in
283
287
  end