rodauth 1.18.0 → 1.19.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +24 -0
  3. data/README.rdoc +20 -11
  4. data/doc/base.rdoc +2 -2
  5. data/doc/email_auth.rdoc +53 -0
  6. data/doc/email_base.rdoc +4 -0
  7. data/doc/internals.rdoc +3 -3
  8. data/doc/lockout.rdoc +28 -48
  9. data/doc/login.rdoc +4 -4
  10. data/doc/otp.rdoc +1 -3
  11. data/doc/release_notes/1.19.0.txt +116 -0
  12. data/doc/reset_password.rdoc +29 -49
  13. data/doc/verify_account.rdoc +30 -50
  14. data/doc/verify_login_change.rdoc +4 -0
  15. data/lib/rodauth/features/base.rb +0 -1
  16. data/lib/rodauth/features/change_login.rb +4 -0
  17. data/lib/rodauth/features/disallow_common_passwords.rb +1 -1
  18. data/lib/rodauth/features/email_auth.rb +253 -0
  19. data/lib/rodauth/features/email_base.rb +2 -0
  20. data/lib/rodauth/features/lockout.rb +35 -6
  21. data/lib/rodauth/features/login.rb +46 -9
  22. data/lib/rodauth/features/otp.rb +8 -4
  23. data/lib/rodauth/features/recovery_codes.rb +0 -2
  24. data/lib/rodauth/features/remember.rb +1 -1
  25. data/lib/rodauth/features/reset_password.rb +32 -4
  26. data/lib/rodauth/features/sms_codes.rb +2 -8
  27. data/lib/rodauth/features/two_factor_base.rb +22 -15
  28. data/lib/rodauth/features/verify_account.rb +27 -1
  29. data/lib/rodauth/features/verify_login_change.rb +30 -7
  30. data/lib/rodauth/migrations.rb +2 -8
  31. data/lib/rodauth/version.rb +1 -1
  32. data/spec/email_auth_spec.rb +285 -0
  33. data/spec/lockout_spec.rb +24 -2
  34. data/spec/login_spec.rb +47 -1
  35. data/spec/migrate/001_tables.rb +13 -0
  36. data/spec/migrate_travis/001_tables.rb +10 -0
  37. data/spec/reset_password_spec.rb +20 -2
  38. data/spec/two_factor_spec.rb +46 -0
  39. data/spec/verify_account_grace_period_spec.rb +1 -1
  40. data/spec/verify_account_spec.rb +33 -3
  41. data/spec/verify_login_change_spec.rb +54 -1
  42. data/templates/email-auth-email.str +5 -0
  43. data/templates/email-auth-request-form.str +7 -0
  44. data/templates/email-auth.str +5 -0
  45. data/templates/login-display.str +4 -0
  46. data/templates/login.str +2 -2
  47. data/templates/otp-setup.str +13 -11
  48. metadata +12 -2
@@ -6,6 +6,8 @@ module Rodauth
6
6
  auth_value_method :require_mail?, true
7
7
  auth_value_method :token_separator, "_"
8
8
 
9
+ redirect :default_post_email
10
+
9
11
  auth_value_methods(
10
12
  :email_from
11
13
  )
@@ -11,16 +11,19 @@ module Rodauth
11
11
  before 'unlock_account_request'
12
12
  after 'unlock_account'
13
13
  after 'unlock_account_request'
14
+ after 'account_lockout'
14
15
  additional_form_tags 'unlock_account'
15
16
  additional_form_tags 'unlock_account_request'
16
17
  button 'Unlock Account', 'unlock_account'
17
18
  button 'Request Account Unlock', 'unlock_account_request'
18
19
  error_flash "There was an error unlocking your account", 'unlock_account'
19
20
  error_flash "This account is currently locked out and cannot be logged in to.", "login_lockout"
21
+ error_flash "An email has recently been sent to you with a link to unlock the account", 'unlock_account_email_recently_sent'
20
22
  notice_flash "Your account has been unlocked", 'unlock_account'
21
23
  notice_flash "An email has been sent to you with a link to unlock your account", 'unlock_account_request'
22
24
  redirect :unlock_account
23
- redirect :unlock_account_request
25
+ redirect(:unlock_account_request){default_post_email_redirect}
26
+ redirect(:unlock_account_email_recently_sent){default_post_email_redirect}
24
27
 
25
28
  auth_value_method :unlock_account_autologin?, true
26
29
  auth_value_method :max_invalid_logins, 100
@@ -30,26 +33,26 @@ module Rodauth
30
33
  auth_value_method :account_lockouts_table, :account_lockouts
31
34
  auth_value_method :account_lockouts_id_column, :id
32
35
  auth_value_method :account_lockouts_key_column, :key
36
+ auth_value_method :account_lockouts_email_last_sent_column, nil
33
37
  auth_value_method :account_lockouts_deadline_column, :deadline
34
38
  auth_value_method :account_lockouts_deadline_interval, {:days=>1}
35
39
  auth_value_method :no_matching_unlock_account_key_message, 'No matching unlock account key'
36
40
  auth_value_method :unlock_account_email_subject, 'Unlock Account'
37
41
  auth_value_method :unlock_account_key_param, 'key'
38
42
  auth_value_method :unlock_account_requires_password?, false
43
+ auth_value_method :unlock_account_skip_resend_email_within, 300
39
44
  session_key :unlock_account_session_key, :unlock_account_key
40
45
 
41
- auth_value_methods(
42
- :unlock_account_redirect,
43
- :unlock_account_request_redirect
44
- )
45
46
  auth_methods(
46
47
  :clear_invalid_login_attempts,
47
48
  :create_unlock_account_email,
48
49
  :generate_unlock_account_key,
49
50
  :get_unlock_account_key,
51
+ :get_unlock_account_email_last_sent,
50
52
  :invalid_login_attempted,
51
53
  :locked_out?,
52
54
  :send_unlock_account_email,
55
+ :set_unlock_account_email_last_sent,
53
56
  :unlock_account_email_body,
54
57
  :unlock_account_email_link,
55
58
  :unlock_account,
@@ -63,8 +66,14 @@ module Rodauth
63
66
 
64
67
  r.post do
65
68
  if account_from_login(param(login_param)) && get_unlock_account_key
69
+ if unlock_account_email_recently_sent?
70
+ set_redirect_error_flash unlock_account_email_recently_sent_error_flash
71
+ redirect unlock_account_email_recently_sent_redirect
72
+ end
73
+
66
74
  transaction do
67
75
  before_unlock_account_request
76
+ set_unlock_account_email_last_sent
68
77
  send_unlock_account_email
69
78
  after_unlock_account_request
70
79
  end
@@ -186,7 +195,11 @@ module Rodauth
186
195
  # key out of it. If that doesn't return a valid key, we should reraise the error.
187
196
  raise e unless @unlock_account_key_value = account_lockouts_ds.get(account_lockouts_key_column)
188
197
 
198
+ after_account_lockout
189
199
  show_lockout_page
200
+ else
201
+ after_account_lockout
202
+ e
190
203
  end
191
204
  end
192
205
  end
@@ -208,6 +221,18 @@ module Rodauth
208
221
  token_link(unlock_account_route, unlock_account_key_param, unlock_account_key_value)
209
222
  end
210
223
 
224
+ def get_unlock_account_email_last_sent
225
+ if column = account_lockouts_email_last_sent_column
226
+ if ts = account_lockouts_ds.get(column)
227
+ convert_timestamp(ts)
228
+ end
229
+ end
230
+ end
231
+
232
+ def set_unlock_account_email_last_sent
233
+ account_lockouts_ds.update(account_lockouts_email_last_sent_column=>Sequel::CURRENT_TIMESTAMP) if account_lockouts_email_last_sent_column
234
+ end
235
+
211
236
  private
212
237
 
213
238
  attr_reader :unlock_account_key_value
@@ -258,8 +283,12 @@ module Rodauth
258
283
  render('unlock-account-email')
259
284
  end
260
285
 
286
+ def unlock_account_email_recently_sent?
287
+ (email_last_sent = get_unlock_account_email_last_sent) && (Time.now - email_last_sent < unlock_account_skip_resend_email_within)
288
+ end
289
+
261
290
  def use_date_arithmetic?
262
- db.database_type == :mysql
291
+ super || db.database_type == :mysql
263
292
  end
264
293
 
265
294
  def account_login_failures_ds
@@ -3,8 +3,9 @@
3
3
  module Rodauth
4
4
  Feature.define(:login, :Login) do
5
5
  notice_flash "You have been logged in"
6
+ notice_flash "Login recognized, please enter your password", "need_password"
6
7
  error_flash "There was an error logging in"
7
- loaded_templates %w'login login-field password-field'
8
+ loaded_templates %w'login login-field password-field login-display'
8
9
  view 'login', 'Login'
9
10
  additional_form_tags
10
11
  button 'Login'
@@ -12,6 +13,7 @@ module Rodauth
12
13
 
13
14
  auth_value_method :login_error_status, 401
14
15
  auth_value_method :login_form_footer, ''
16
+ auth_value_method :use_multi_phase_login?, false
15
17
 
16
18
  route do |r|
17
19
  check_already_logged_in
@@ -23,6 +25,7 @@ module Rodauth
23
25
 
24
26
  r.post do
25
27
  clear_session
28
+ skip_error_flash = false
26
29
 
27
30
  catch_error do
28
31
  unless account_from_login(param(login_param))
@@ -35,25 +38,59 @@ module Rodauth
35
38
  throw_error_status(unopen_account_error_status, login_param, unverified_account_message)
36
39
  end
37
40
 
41
+ if use_multi_phase_login?
42
+ @valid_login_entered = true
43
+
44
+ unless param_or_nil(password_param)
45
+ after_login_entered_during_multi_phase_login
46
+ skip_error_flash = true
47
+ next
48
+ end
49
+ end
50
+
38
51
  unless password_match?(param(password_param))
39
52
  after_login_failure
40
53
  throw_error_status(login_error_status, password_param, invalid_password_message)
41
54
  end
42
55
 
43
- transaction do
44
- before_login
45
- update_session
46
- after_login
47
- end
48
- set_notice_flash login_notice_flash
49
- redirect login_redirect
56
+ _login
50
57
  end
51
58
 
52
- set_error_flash login_error_flash
59
+ set_error_flash login_error_flash unless skip_error_flash
53
60
  login_view
54
61
  end
55
62
  end
56
63
 
57
64
  attr_reader :login_form_header
65
+
66
+ def after_login_entered_during_multi_phase_login
67
+ set_notice_now_flash need_password_notice_flash
68
+ end
69
+
70
+ def skip_login_field_on_login?
71
+ return false unless use_multi_phase_login?
72
+ @valid_login_entered
73
+ end
74
+
75
+ def skip_password_field_on_login?
76
+ return false unless use_multi_phase_login?
77
+ @valid_login_entered != true
78
+ end
79
+
80
+ def login_hidden_field
81
+ "<input type='hidden' name=\"#{login_param}\" value=\"#{scope.h param(login_param)}\" />"
82
+ end
83
+
84
+ private
85
+
86
+ def _login
87
+ transaction do
88
+ before_login
89
+ update_session
90
+ after_login
91
+ end
92
+ set_notice_flash login_notice_flash
93
+ redirect login_redirect
94
+ end
58
95
  end
59
96
  end
@@ -18,9 +18,13 @@ module Rodauth
18
18
  before 'otp_authentication'
19
19
  before 'otp_setup'
20
20
  before 'otp_disable'
21
- before 'otp_authentication_route'
22
- before 'otp_setup_route'
23
- before 'otp_disable_route'
21
+
22
+ configuration_module_eval do
23
+ def before_otp_authentication_route(&block)
24
+ warn "before_otp_authentication_route is deprecated, switch to before_otp_auth_route"
25
+ before_otp_auth_route(&block)
26
+ end
27
+ end
24
28
 
25
29
  button 'Authenticate via 2nd Factor', 'otp_auth'
26
30
  button 'Disable Two Factor Authentication', 'otp_disable'
@@ -104,7 +108,7 @@ module Rodauth
104
108
  redirect otp_lockout_redirect
105
109
  end
106
110
 
107
- before_otp_authentication_route
111
+ before_otp_auth_route
108
112
 
109
113
  r.get do
110
114
  otp_auth_view
@@ -10,8 +10,6 @@ module Rodauth
10
10
  before 'add_recovery_codes'
11
11
  before 'view_recovery_codes'
12
12
  before 'recovery_auth'
13
- before 'recovery_auth_route'
14
- before 'recovery_codes_route'
15
13
 
16
14
  after 'add_recovery_codes'
17
15
 
@@ -188,7 +188,7 @@ module Rodauth
188
188
  end
189
189
 
190
190
  def use_date_arithmetic?
191
- extend_remember_deadline? || db.database_type == :mysql
191
+ super || extend_remember_deadline? || db.database_type == :mysql
192
192
  end
193
193
 
194
194
  def remember_key_ds(id=account_id)
@@ -8,6 +8,7 @@ module Rodauth
8
8
  notice_flash "An email has been sent to you with a link to reset the password for your account", 'reset_password_email_sent'
9
9
  error_flash "There was an error resetting your password"
10
10
  error_flash "There was an error requesting a password reset", 'reset_password_request'
11
+ error_flash "An email has recently been sent to you with a link to reset your password", 'reset_password_email_recently_sent'
11
12
  loaded_templates %w'reset-password-request reset-password password-field password-confirm-field reset-password-email'
12
13
  view 'reset-password', 'Reset Password'
13
14
  view 'reset-password-request', 'Request Password Reset', 'reset_password_request'
@@ -20,7 +21,8 @@ module Rodauth
20
21
  button 'Reset Password'
21
22
  button 'Request Password Reset', 'reset_password_request'
22
23
  redirect
23
- redirect :reset_password_email_sent
24
+ redirect(:reset_password_email_sent){default_post_email_redirect}
25
+ redirect(:reset_password_email_recently_sent){default_post_email_redirect}
24
26
 
25
27
  auth_value_method :reset_password_deadline_column, :deadline
26
28
  auth_value_method :reset_password_deadline_interval, {:days=>1}
@@ -31,21 +33,25 @@ module Rodauth
31
33
  auth_value_method :reset_password_table, :account_password_reset_keys
32
34
  auth_value_method :reset_password_id_column, :id
33
35
  auth_value_method :reset_password_key_column, :key
36
+ auth_value_method :reset_password_email_last_sent_column, nil
37
+ auth_value_method :reset_password_skip_resend_email_within, 300
34
38
  session_key :reset_password_session_key, :reset_password_key
35
39
 
36
- auth_value_methods :reset_password_email_sent_redirect, :reset_password_request_link
40
+ auth_value_methods :reset_password_request_link
37
41
 
38
42
  auth_methods(
39
43
  :create_reset_password_key,
40
44
  :create_reset_password_email,
41
45
  :get_reset_password_key,
46
+ :get_reset_password_email_last_sent,
42
47
  :login_failed_reset_password_request_form,
43
48
  :remove_reset_password_key,
44
49
  :reset_password_email_body,
45
50
  :reset_password_email_link,
46
51
  :reset_password_key_insert_hash,
47
52
  :reset_password_key_value,
48
- :send_reset_password_email
53
+ :send_reset_password_email,
54
+ :set_reset_password_email_last_sent
49
55
  )
50
56
  auth_private_methods(
51
57
  :account_from_reset_password_key
@@ -61,6 +67,11 @@ module Rodauth
61
67
 
62
68
  r.post do
63
69
  if account_from_login(param(login_param)) && open_account?
70
+ if reset_password_email_recently_sent?
71
+ set_redirect_error_flash reset_password_email_recently_sent_error_flash
72
+ redirect reset_password_email_recently_sent_redirect
73
+ end
74
+
64
75
  generate_reset_password_key_value
65
76
  transaction do
66
77
  before_reset_password_request
@@ -146,6 +157,7 @@ module Rodauth
146
157
  def create_reset_password_key
147
158
  transaction do
148
159
  if reset_password_key_value = get_password_reset_key(account_id)
160
+ set_reset_password_email_last_sent
149
161
  @reset_password_key_value = reset_password_key_value
150
162
  elsif e = raised_uniqueness_violation{password_reset_ds.insert(reset_password_key_insert_hash)}
151
163
  # If inserting into the reset password table causes a violation, we can pull the
@@ -185,8 +197,24 @@ module Rodauth
185
197
  "<p><a href=\"#{prefix}/#{reset_password_request_route}\">Forgot Password?</a></p>"
186
198
  end
187
199
 
200
+ def set_reset_password_email_last_sent
201
+ password_reset_ds.update(reset_password_email_last_sent_column=>Sequel::CURRENT_TIMESTAMP) if reset_password_email_last_sent_column
202
+ end
203
+
204
+ def get_reset_password_email_last_sent
205
+ if column = reset_password_email_last_sent_column
206
+ if ts = password_reset_ds.get(column)
207
+ convert_timestamp(ts)
208
+ end
209
+ end
210
+ end
211
+
188
212
  private
189
213
 
214
+ def reset_password_email_recently_sent?
215
+ (email_last_sent = get_reset_password_email_last_sent) && (Time.now - email_last_sent < reset_password_skip_resend_email_within)
216
+ end
217
+
190
218
  attr_reader :reset_password_key_value
191
219
 
192
220
  def after_login_failure
@@ -218,7 +246,7 @@ module Rodauth
218
246
  end
219
247
 
220
248
  def use_date_arithmetic?
221
- db.database_type == :mysql
249
+ super || db.database_type == :mysql
222
250
  end
223
251
 
224
252
  def reset_password_key_insert_hash
@@ -49,6 +49,7 @@ module Rodauth
49
49
  redirect(:sms_needs_confirmation){"#{prefix}/#{sms_confirm_route}"}
50
50
  redirect(:sms_needs_setup){"#{prefix}/#{sms_setup_route}"}
51
51
  redirect(:sms_request){"#{prefix}/#{sms_request_route}"}
52
+ redirect(:sms_lockout){_two_factor_auth_required_redirect}
52
53
 
53
54
  loaded_templates %w'sms-auth sms-confirm sms-disable sms-request sms-setup sms-code-field password-field'
54
55
  view 'sms-auth', 'Authenticate via SMS Code', 'sms_auth'
@@ -80,10 +81,7 @@ module Rodauth
80
81
 
81
82
  auth_cached_method :sms
82
83
 
83
- auth_value_methods(
84
- :sms_lockout_redirect,
85
- :sms_codes_primary?
86
- )
84
+ auth_value_methods :sms_codes_primary?
87
85
 
88
86
  auth_methods(
89
87
  :sms_auth_message,
@@ -416,10 +414,6 @@ module Rodauth
416
414
  phone.length >= sms_phone_min_length
417
415
  end
418
416
 
419
- def sms_lockout_redirect
420
- _two_factor_auth_required_redirect
421
- end
422
-
423
417
  def sms_auth_message(code)
424
418
  "SMS authentication code for #{request.host} is #{code}"
425
419
  end
@@ -20,11 +20,9 @@ module Rodauth
20
20
  session_key :two_factor_session_key, :two_factor_auth
21
21
  session_key :two_factor_setup_session_key, :two_factor_auth_setup
22
22
  auth_value_method :two_factor_need_setup_redirect, nil
23
+ auth_value_method :two_factor_auth_required_redirect, nil
23
24
 
24
- auth_value_methods(
25
- :two_factor_auth_required_redirect,
26
- :two_factor_modifications_require_password?
27
- )
25
+ auth_value_methods :two_factor_modifications_require_password?
28
26
 
29
27
  auth_methods(
30
28
  :two_factor_authenticated?,
@@ -39,21 +37,34 @@ module Rodauth
39
37
  end
40
38
 
41
39
  def authenticated?
42
- super
43
- two_factor_authenticated? if two_factor_authentication_setup?
40
+ # False if not authenticated via single factor
41
+ return false unless super
42
+
43
+ # True if already authenticated via 2nd factor
44
+ return true if two_factor_authenticated?
45
+
46
+ # True if authenticated via single factor and 2nd factor not setup
47
+ !two_factor_authentication_setup?
44
48
  end
45
49
 
46
50
  def require_authentication
47
51
  super
52
+
53
+ # Avoid database query if already authenticated via 2nd factor
54
+ return if two_factor_authenticated?
55
+
48
56
  require_two_factor_authenticated if two_factor_authentication_setup?
49
57
  end
50
58
 
51
59
  def require_two_factor_setup
52
- unless uses_two_factor_authentication?
53
- set_redirect_error_status(two_factor_not_setup_error_status)
54
- set_redirect_error_flash two_factor_not_setup_error_flash
55
- redirect two_factor_need_setup_redirect
56
- end
60
+ # Avoid database query if already authenticated via 2nd factor
61
+ return if two_factor_authenticated?
62
+
63
+ return if uses_two_factor_authentication?
64
+
65
+ set_redirect_error_status(two_factor_not_setup_error_status)
66
+ set_redirect_error_flash two_factor_not_setup_error_flash
67
+ redirect two_factor_need_setup_redirect
57
68
  end
58
69
 
59
70
  def require_two_factor_not_authenticated
@@ -76,10 +87,6 @@ module Rodauth
76
87
  nil
77
88
  end
78
89
 
79
- def two_factor_auth_required_redirect
80
- nil
81
- end
82
-
83
90
  def two_factor_auth_fallback_redirect
84
91
  nil
85
92
  end