rodauth 1.18.0 → 1.19.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.
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
@@ -2,10 +2,12 @@ require File.expand_path("spec_helper", File.dirname(__FILE__))
2
2
 
3
3
  describe 'Rodauth lockout feature' do
4
4
  it "should support account lockouts without autologin on unlock" do
5
+ lockouts = []
5
6
  rodauth do
6
7
  enable :lockout
7
8
  max_invalid_logins 2
8
9
  unlock_account_autologin? false
10
+ after_account_lockout{lockouts << true}
9
11
  end
10
12
  roda do |r|
11
13
  r.rodauth
@@ -28,6 +30,7 @@ describe 'Rodauth lockout feature' do
28
30
  click_button 'Login'
29
31
  page.find('#error_flash').text.must_equal 'There was an error logging in'
30
32
  end
33
+ lockouts.must_equal [true]
31
34
 
32
35
  fill_in 'Password', :with=>'012345678910'
33
36
  click_button 'Login'
@@ -35,8 +38,15 @@ describe 'Rodauth lockout feature' do
35
38
  page.body.must_include("This account is currently locked out")
36
39
  click_button 'Request Account Unlock'
37
40
  page.find('#notice_flash').text.must_equal 'An email has been sent to you with a link to unlock your account'
38
-
39
41
  link = email_link(/(\/unlock-account\?key=.+)$/)
42
+
43
+ visit '/login'
44
+ fill_in 'Login', :with=>'foo@example.com'
45
+ fill_in 'Password', :with=>'012345678910'
46
+ click_button 'Login'
47
+ click_button 'Request Account Unlock'
48
+ email_link(/(\/unlock-account\?key=.+)$/).must_equal link
49
+
40
50
  visit link[0...-1]
41
51
  page.find('#error_flash').text.must_equal 'No matching unlock account key'
42
52
 
@@ -54,6 +64,7 @@ describe 'Rodauth lockout feature' do
54
64
  rodauth do
55
65
  enable :lockout
56
66
  unlock_account_requires_password? true
67
+ account_lockouts_email_last_sent_column :email_last_sent
57
68
  end
58
69
  roda do |r|
59
70
  r.rodauth
@@ -74,8 +85,16 @@ describe 'Rodauth lockout feature' do
74
85
  page.body.must_include("This account is currently locked out")
75
86
  click_button 'Request Account Unlock'
76
87
  page.find('#notice_flash').text.must_equal 'An email has been sent to you with a link to unlock your account'
77
-
78
88
  link = email_link(/(\/unlock-account\?key=.+)$/)
89
+
90
+ visit '/login'
91
+ fill_in 'Login', :with=>'foo@example.com'
92
+ fill_in 'Password', :with=>'012345678910'
93
+ click_button 'Login'
94
+ click_button 'Request Account Unlock'
95
+ page.find('#error_flash').text.must_equal "An email has recently been sent to you with a link to unlock the account"
96
+ Mail::TestMailer.deliveries.must_equal []
97
+
79
98
  visit link
80
99
  click_button 'Unlock Account'
81
100
 
@@ -147,9 +166,11 @@ describe 'Rodauth lockout feature' do
147
166
  end
148
167
 
149
168
  it "should handle uniqueness errors raised when inserting unlock account token" do
169
+ lockouts = []
150
170
  rodauth do
151
171
  enable :lockout
152
172
  max_invalid_logins 2
173
+ after_account_lockout{lockouts << true}
153
174
  end
154
175
  roda do |r|
155
176
  def rodauth.raised_uniqueness_violation(*) super; true; end
@@ -165,6 +186,7 @@ describe 'Rodauth lockout feature' do
165
186
 
166
187
  fill_in 'Password', :with=>'012345678910'
167
188
  click_button 'Login'
189
+ lockouts.must_equal [true]
168
190
  page.find('#error_flash').text.must_equal "This account is currently locked out and cannot be logged in to."
169
191
  page.body.must_include("This account is currently locked out")
170
192
  click_button 'Request Account Unlock'
@@ -34,6 +34,52 @@ describe 'Rodauth login feature' do
34
34
  page.current_path.must_equal '/login'
35
35
  end
36
36
 
37
+ it "should handle multi phase login (email first, then password)" do
38
+ rodauth do
39
+ enable :login, :logout
40
+ use_multi_phase_login? true
41
+ end
42
+ roda do |r|
43
+ r.rodauth
44
+ next unless rodauth.logged_in?
45
+ r.root{view :content=>"Logged In"}
46
+ end
47
+
48
+ visit '/login'
49
+ page.title.must_equal 'Login'
50
+
51
+ page.all('input[type=password]').must_be :empty?
52
+ fill_in 'Login', :with=>'foo2@example.com'
53
+ click_button 'Login'
54
+ page.find('#error_flash').text.must_equal 'There was an error logging in'
55
+ page.html.must_include("no matching login")
56
+
57
+ page.all('input[type=password]').must_be :empty?
58
+ fill_in 'Login', :with=>'foo@example.com'
59
+ click_button 'Login'
60
+ page.find('#notice_flash').text.must_equal 'Login recognized, please enter your password'
61
+
62
+ page.all('input[type=text]').must_be :empty?
63
+ fill_in 'Password', :with=>'012345678'
64
+ click_button 'Login'
65
+ page.find('#error_flash').text.must_equal 'There was an error logging in'
66
+ page.html.must_include("invalid password")
67
+
68
+ page.all('input[type=text]').must_be :empty?
69
+ fill_in 'Password', :with=>'0123456789'
70
+ click_button 'Login'
71
+ page.current_path.must_equal '/'
72
+ page.find('#notice_flash').text.must_equal 'You have been logged in'
73
+ page.html.must_include("Logged In")
74
+
75
+ visit '/logout'
76
+ page.title.must_equal 'Logout'
77
+
78
+ click_button 'Logout'
79
+ page.find('#notice_flash').text.must_equal 'You have been logged out'
80
+ page.current_path.must_equal '/login'
81
+ end
82
+
37
83
  it "should not allow login to unverified account" do
38
84
  rodauth do
39
85
  enable :login
@@ -115,7 +161,7 @@ describe 'Rodauth login feature' do
115
161
  it "should handle a prefix and some other login options" do
116
162
  rodauth do
117
163
  enable :login, :logout
118
- prefix 'auth'
164
+ prefix '/auth'
119
165
  session_key 'login_email'
120
166
  account_from_session{DB[:accounts].first(:email=>session_value)}
121
167
  account_session_value{account[:email]}
@@ -36,6 +36,7 @@ Sequel.migration do
36
36
  foreign_key :id, :accounts, :primary_key=>true, :type=>:Bignum
37
37
  String :key, :null=>false
38
38
  DateTime :deadline, deadline_opts[1]
39
+ DateTime :email_last_sent, :null=>false, :default=>Sequel::CURRENT_TIMESTAMP
39
40
  end
40
41
 
41
42
  # Used by the account verification feature
@@ -43,6 +44,7 @@ Sequel.migration do
43
44
  foreign_key :id, :accounts, :primary_key=>true, :type=>:Bignum
44
45
  String :key, :null=>false
45
46
  DateTime :requested_at, :null=>false, :default=>Sequel::CURRENT_TIMESTAMP
47
+ DateTime :email_last_sent, :null=>false, :default=>Sequel::CURRENT_TIMESTAMP
46
48
  end
47
49
 
48
50
  # Used by the verify login change feature
@@ -69,6 +71,15 @@ Sequel.migration do
69
71
  foreign_key :id, :accounts, :primary_key=>true, :type=>:Bignum
70
72
  String :key, :null=>false
71
73
  DateTime :deadline, deadline_opts[1]
74
+ DateTime :email_last_sent
75
+ end
76
+
77
+ # Used by the email auth feature
78
+ create_table(:account_email_auth_keys) do
79
+ foreign_key :id, :accounts, :primary_key=>true, :type=>:Bignum
80
+ String :key, :null=>false
81
+ DateTime :deadline, deadline_opts[1]
82
+ DateTime :email_last_sent, :null=>false, :default=>Sequel::CURRENT_TIMESTAMP
72
83
  end
73
84
 
74
85
  # Used by the password expiration feature
@@ -132,6 +143,7 @@ Sequel.migration do
132
143
  run "GRANT ALL ON account_login_change_keys TO #{user}"
133
144
  run "GRANT ALL ON account_remember_keys TO #{user}"
134
145
  run "GRANT ALL ON account_login_failures TO #{user}"
146
+ run "GRANT ALL ON account_email_auth_keys TO #{user}"
135
147
  run "GRANT ALL ON account_lockouts TO #{user}"
136
148
  run "GRANT ALL ON account_password_change_times TO #{user}"
137
149
  run "GRANT ALL ON account_activity_times TO #{user}"
@@ -149,6 +161,7 @@ Sequel.migration do
149
161
  :account_session_keys,
150
162
  :account_activity_times,
151
163
  :account_password_change_times,
164
+ :account_email_auth_keys,
152
165
  :account_lockouts,
153
166
  :account_login_failures,
154
167
  :account_remember_keys,
@@ -44,12 +44,14 @@ Sequel.migration do
44
44
  foreign_key :id, :accounts, :primary_key=>true, :type=>:Bignum
45
45
  String :key, :null=>false
46
46
  DateTime :deadline, deadline_opts[1]
47
+ DateTime :email_last_sent, :null=>false, :default=>Sequel::CURRENT_TIMESTAMP
47
48
  end
48
49
 
49
50
  create_table(:account_verification_keys) do
50
51
  foreign_key :id, :accounts, :primary_key=>true, :type=>:Bignum
51
52
  String :key, :null=>false
52
53
  DateTime :requested_at, :null=>false, :default=>Sequel::CURRENT_TIMESTAMP
54
+ DateTime :email_last_sent, :null=>false, :default=>Sequel::CURRENT_TIMESTAMP
53
55
  end
54
56
 
55
57
  create_table(:account_login_change_keys) do
@@ -65,6 +67,13 @@ Sequel.migration do
65
67
  DateTime :deadline, deadline_opts[14]
66
68
  end
67
69
 
70
+ create_table(:account_email_auth_keys) do
71
+ foreign_key :id, :accounts, :primary_key=>true, :type=>:Bignum
72
+ String :key, :null=>false
73
+ DateTime :deadline, deadline_opts[1]
74
+ DateTime :email_last_sent, :null=>false, :default=>Sequel::CURRENT_TIMESTAMP
75
+ end
76
+
68
77
  create_table(:account_login_failures) do
69
78
  foreign_key :id, :accounts, :primary_key=>true, :type=>:Bignum
70
79
  Integer :number, :null=>false, :default=>1
@@ -73,6 +82,7 @@ Sequel.migration do
73
82
  foreign_key :id, :accounts, :primary_key=>true, :type=>:Bignum
74
83
  String :key, :null=>false
75
84
  DateTime :deadline, deadline_opts[1]
85
+ DateTime :email_last_sent
76
86
  end
77
87
 
78
88
  create_table(:account_password_change_times) do
@@ -2,8 +2,10 @@ require File.expand_path("spec_helper", File.dirname(__FILE__))
2
2
 
3
3
  describe 'Rodauth reset_password feature' do
4
4
  it "should support resetting passwords for accounts" do
5
+ last_sent_column = nil
5
6
  rodauth do
6
7
  enable :login, :reset_password
8
+ reset_password_email_last_sent_column{last_sent_column}
7
9
  end
8
10
  roda do |r|
9
11
  r.rodauth
@@ -29,8 +31,24 @@ describe 'Rodauth reset_password feature' do
29
31
  click_button 'Request Password Reset'
30
32
  email_link(/(\/reset-password\?key=.+)$/).must_equal link
31
33
 
32
- visit '/login'
33
- login(:pass=>'01234567', :visit=>false)
34
+ login(:pass=>'01234567')
35
+ click_button 'Request Password Reset'
36
+ email_link(/(\/reset-password\?key=.+)$/).must_equal link
37
+
38
+ last_sent_column = :email_last_sent
39
+ login(:pass=>'01234567')
40
+ click_button 'Request Password Reset'
41
+ page.find('#error_flash').text.must_equal "An email has recently been sent to you with a link to reset your password"
42
+ Mail::TestMailer.deliveries.must_equal []
43
+
44
+ DB[:account_password_reset_keys].update(:email_last_sent => Time.now - 250).must_equal 1
45
+ login(:pass=>'01234567')
46
+ click_button 'Request Password Reset'
47
+ page.find('#error_flash').text.must_equal "An email has recently been sent to you with a link to reset your password"
48
+ Mail::TestMailer.deliveries.must_equal []
49
+
50
+ DB[:account_password_reset_keys].update(:email_last_sent => Time.now - 350).must_equal 1
51
+ login(:pass=>'01234567')
34
52
  click_button 'Request Password Reset'
35
53
  email_link(/(\/reset-password\?key=.+)$/).must_equal link
36
54
 
@@ -1316,4 +1316,50 @@ describe 'Rodauth OTP feature' do
1316
1316
  DB[t].count.must_equal 0
1317
1317
  end
1318
1318
  end
1319
+
1320
+ it "should allow two factor authentication setup, login, recovery, removal" do
1321
+ warning = nil
1322
+ before_called = false
1323
+ rodauth do
1324
+ enable :login, :otp, :logout
1325
+ (class << self; self end).send(:define_method, :warn){|w| warning = w}
1326
+ before_otp_authentication_route{before_called = true}
1327
+ warning.must_equal "before_otp_authentication_route is deprecated, switch to before_otp_auth_route"
1328
+ otp_drift 10
1329
+ end
1330
+ roda do |r|
1331
+ r.rodauth
1332
+
1333
+ r.redirect '/login' unless rodauth.logged_in?
1334
+
1335
+ if rodauth.two_factor_authentication_setup?
1336
+ r.redirect '/otp-auth' unless rodauth.authenticated?
1337
+ view :content=>"With OTP"
1338
+ else
1339
+ view :content=>"Without OTP"
1340
+ end
1341
+ end
1342
+
1343
+ login
1344
+ page.html.must_include('Without OTP')
1345
+
1346
+ visit '/otp-auth'
1347
+ before_called.must_equal false
1348
+ page.current_path.must_equal '/otp-setup'
1349
+
1350
+ secret = page.html.match(/Secret: ([a-z2-7]{16})/)[1]
1351
+ totp = ROTP::TOTP.new(secret)
1352
+ fill_in 'Password', :with=>'0123456789'
1353
+ fill_in 'Authentication Code', :with=>totp.now
1354
+ click_button 'Setup Two Factor Authentication'
1355
+ page.find('#notice_flash').text.must_equal 'Two factor authentication is now setup'
1356
+ page.current_path.must_equal '/'
1357
+ page.html.must_include 'With OTP'
1358
+
1359
+ logout
1360
+ before_called.must_equal false
1361
+ login
1362
+ page.current_path.must_equal '/otp-auth'
1363
+ before_called.must_equal true
1364
+ end
1319
1365
  end
@@ -73,7 +73,7 @@ describe 'Rodauth verify_account_grace_period feature' do
73
73
  click_button 'Create Account'
74
74
  click_button 'Send Verification Email Again'
75
75
  page.find('#notice_flash').text.must_equal "An email has been sent to you with a link to verify your account"
76
- page.current_path.must_equal '/login'
76
+ page.current_path.must_equal '/'
77
77
  email_link(/(\/verify-account\?key=.+)$/, 'foo@example2.com').must_equal link
78
78
 
79
79
  visit link
@@ -2,9 +2,11 @@ require File.expand_path("spec_helper", File.dirname(__FILE__))
2
2
 
3
3
  describe 'Rodauth verify_account feature' do
4
4
  it "should support verifying accounts" do
5
+ last_sent_column = nil
5
6
  rodauth do
6
7
  enable :login, :create_account, :verify_account
7
8
  verify_account_autologin? false
9
+ verify_account_email_last_sent_column{last_sent_column}
8
10
  end
9
11
  roda do |r|
10
12
  r.rodauth
@@ -25,21 +27,49 @@ describe 'Rodauth verify_account feature' do
25
27
  page.find('#error_flash').text.must_equal 'The account you tried to login with is currently awaiting verification'
26
28
  page.html.must_include("If you no longer have the email to verify the account, you can request that it be resent to you")
27
29
  click_button 'Send Verification Email Again'
28
- page.current_path.must_equal '/login'
30
+ page.current_path.must_equal '/'
29
31
  email_link(/(\/verify-account\?key=.+)$/, 'foo@example2.com').must_equal link
30
32
 
33
+ visit '/login'
31
34
  click_link 'Resend Verify Account Information'
32
35
  fill_in 'Login', :with=>'foo@example2.com'
33
36
  click_button 'Send Verification Email Again'
34
- page.current_path.must_equal '/login'
37
+ page.current_path.must_equal '/'
35
38
  email_link(/(\/verify-account\?key=.+)$/, 'foo@example2.com').must_equal link
36
39
 
40
+ visit '/login'
41
+ last_sent_column = :email_last_sent
42
+ click_link 'Resend Verify Account Information'
43
+ fill_in 'Login', :with=>'foo@example2.com'
44
+ click_button 'Send Verification Email Again'
45
+ page.current_path.must_equal '/'
46
+ page.find('#error_flash').text.must_equal "An email has recently been sent to you with a link to verify your account"
47
+ Mail::TestMailer.deliveries.must_equal []
48
+
49
+ visit '/login'
50
+ DB[:account_verification_keys].update(:email_last_sent => Time.now - 250).must_equal 1
51
+ click_link 'Resend Verify Account Information'
52
+ fill_in 'Login', :with=>'foo@example2.com'
53
+ click_button 'Send Verification Email Again'
54
+ page.current_path.must_equal '/'
55
+ page.find('#error_flash').text.must_equal "An email has recently been sent to you with a link to verify your account"
56
+ Mail::TestMailer.deliveries.must_equal []
57
+
58
+ visit '/login'
59
+ DB[:account_verification_keys].update(:email_last_sent => Time.now - 350).must_equal 1
60
+ click_link 'Resend Verify Account Information'
61
+ fill_in 'Login', :with=>'foo@example2.com'
62
+ click_button 'Send Verification Email Again'
63
+ page.current_path.must_equal '/'
64
+ email_link(/(\/verify-account\?key=.+)$/, 'foo@example2.com').must_equal link
65
+
66
+ DB[:account_verification_keys].update(:email_last_sent => Time.now - 350).must_equal 1
37
67
  visit '/create-account'
38
68
  fill_in 'Login', :with=>'foo@example2.com'
39
69
  click_button 'Create Account'
40
70
  click_button 'Send Verification Email Again'
41
71
  page.find('#notice_flash').text.must_equal "An email has been sent to you with a link to verify your account"
42
- page.current_path.must_equal '/login'
72
+ page.current_path.must_equal '/'
43
73
 
44
74
  link = email_link(/(\/verify-account\?key=.+)$/, 'foo@example2.com')
45
75
  visit link[0...-1]
@@ -78,7 +78,60 @@ describe 'Rodauth verify_login_change feature' do
78
78
  page.body.must_include('Logged In')
79
79
  end
80
80
 
81
- it "should handle uniqueness errors raised when inserting password reset token" do
81
+ it "should check for duplicate accounts before sending verify email and before updating login" do
82
+ rodauth do
83
+ enable :login, :logout, :verify_login_change, :create_account
84
+ change_login_requires_password? false
85
+ create_account_autologin? false
86
+ end
87
+ roda do |r|
88
+ r.rodauth
89
+ r.root{view :content=>rodauth.logged_in? ? "Logged In" : "Not Logged"}
90
+ end
91
+
92
+ visit '/create-account'
93
+ fill_in 'Login', :with=>'foo@example2.com'
94
+ fill_in 'Confirm Login', :with=>'foo@example2.com'
95
+ fill_in 'Password', :with=>'0123456789'
96
+ fill_in 'Confirm Password', :with=>'0123456789'
97
+ click_button 'Create Account'
98
+
99
+ login
100
+
101
+ visit '/change-login'
102
+ fill_in 'Login', :with=>'foo@example2.com'
103
+ fill_in 'Confirm Login', :with=>'foo@example.com'
104
+ click_button 'Change Login'
105
+ page.find('#error_flash').text.must_equal "There was an error changing your login"
106
+ page.body.must_include "logins do not match"
107
+
108
+ visit '/change-login'
109
+ fill_in 'Login', :with=>'foo@example2.com'
110
+ fill_in 'Confirm Login', :with=>'foo@example2.com'
111
+ click_button 'Change Login'
112
+ page.find('#error_flash').text.must_equal "There was an error changing your login"
113
+ page.body.must_include "invalid login, already an account with this login"
114
+
115
+ visit '/change-login'
116
+ fill_in 'Login', :with=>'foo@example3.com'
117
+ fill_in 'Confirm Login', :with=>'foo@example3.com'
118
+ click_button 'Change Login'
119
+ link = email_link(/(\/verify-login-change\?key=.+)$/, 'foo@example3.com')
120
+
121
+ logout
122
+
123
+ DB[:accounts].where(:email=>'foo@example2.com').update(:email=>'foo@example3.com')
124
+
125
+ visit link
126
+ click_button 'Verify Login Change'
127
+ page.find('#error_flash').text.must_equal "Unable to change login as there is already an account with the new login"
128
+ page.current_path.must_equal '/login'
129
+
130
+ visit link
131
+ page.find('#error_flash').text.must_equal "invalid verify login change key"
132
+ end
133
+
134
+ it "should handle uniqueness errors raised when inserting verify login change entry" do
82
135
  unique = false
83
136
  rodauth do
84
137
  enable :login, :logout, :verify_login_change