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