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,7 @@ module Rodauth
6
6
 
7
7
  error_flash "Unable to verify account"
8
8
  error_flash "Unable to resend verify account email", 'verify_account_resend'
9
+ error_flash "An email has recently been sent to you with a link to verify your account", 'verify_account_email_recently_sent'
9
10
  notice_flash "Your account has been verified"
10
11
  notice_flash "An email has been sent to you with a link to verify your account", 'verify_account_email_sent'
11
12
  loaded_templates %w'verify-account verify-account-resend verify-account-email'
@@ -20,7 +21,8 @@ module Rodauth
20
21
  button 'Verify Account'
21
22
  button 'Send Verification Email Again', 'verify_account_resend'
22
23
  redirect
23
- redirect(:verify_account_email_sent){require_login_redirect}
24
+ redirect(:verify_account_email_sent){default_post_email_redirect}
25
+ redirect(:verify_account_email_recently_sent){default_post_email_redirect}
24
26
 
25
27
  auth_value_method :no_matching_verify_account_key_message, "invalid verify account key"
26
28
  auth_value_method :attempt_to_create_unverified_account_notice_message, "The account you tried to create is currently awaiting verification"
@@ -30,6 +32,8 @@ module Rodauth
30
32
  auth_value_method :verify_account_autologin?, true
31
33
  auth_value_method :verify_account_table, :account_verification_keys
32
34
  auth_value_method :verify_account_id_column, :id
35
+ auth_value_method :verify_account_email_last_sent_column, nil
36
+ auth_value_method :verify_account_skip_resend_email_within, 300
33
37
  auth_value_method :verify_account_key_column, :key
34
38
  session_key :verify_account_session_key, :verify_account_key
35
39
  auth_value_method :verify_account_set_password?, false
@@ -63,6 +67,11 @@ module Rodauth
63
67
 
64
68
  r.post do
65
69
  if account_from_login(param(login_param)) && allow_resending_verify_account_email?
70
+ if verify_account_email_recently_sent?
71
+ set_redirect_error_flash verify_account_email_recently_sent_error_flash
72
+ redirect verify_account_email_recently_sent_redirect
73
+ end
74
+
66
75
  before_verify_account_email_resend
67
76
  if verify_account_email_resend
68
77
  after_verify_account_email_resend
@@ -158,6 +167,7 @@ module Rodauth
158
167
 
159
168
  def verify_account_email_resend
160
169
  if @verify_account_key_value = get_verify_account_key(account_id)
170
+ set_verify_account_email_last_sent
161
171
  send_verify_account_email
162
172
  true
163
173
  end
@@ -218,8 +228,24 @@ module Rodauth
218
228
  super
219
229
  end
220
230
 
231
+ def set_verify_account_email_last_sent
232
+ verify_account_ds.update(verify_account_email_last_sent_column=>Sequel::CURRENT_TIMESTAMP) if verify_account_email_last_sent_column
233
+ end
234
+
235
+ def get_verify_account_email_last_sent
236
+ if column = verify_account_email_last_sent_column
237
+ if ts = verify_account_ds.get(column)
238
+ convert_timestamp(ts)
239
+ end
240
+ end
241
+ end
242
+
221
243
  private
222
244
 
245
+ def verify_account_email_recently_sent?
246
+ (email_last_sent = get_verify_account_email_last_sent) && (Time.now - email_last_sent < verify_account_skip_resend_email_within)
247
+ end
248
+
223
249
  attr_reader :verify_account_key_value
224
250
 
225
251
  def before_login_attempt
@@ -5,14 +5,18 @@ module Rodauth
5
5
  depends :change_login, :email_base
6
6
 
7
7
  error_flash "Unable to verify login change"
8
+ error_flash "Unable to change login as there is already an account with the new login", :verify_login_change_duplicate_account
8
9
  notice_flash "Your login change has been verified"
9
10
  loaded_templates %w'verify-login-change verify-login-change-email'
10
11
  view 'verify-login-change', 'Verify Login Change'
11
12
  additional_form_tags
12
13
  after
14
+ after 'verify_login_change_email'
13
15
  before
16
+ before 'verify_login_change_email'
14
17
  button 'Verify Login Change'
15
18
  redirect
19
+ redirect(:verify_login_change_duplicate_account){require_login_redirect}
16
20
 
17
21
  auth_value_method :no_matching_verify_login_change_key_message, "invalid verify login change key"
18
22
  auth_value_method :verify_login_change_autologin?, false
@@ -76,7 +80,11 @@ module Rodauth
76
80
 
77
81
  transaction do
78
82
  before_verify_login_change
79
- verify_login_change
83
+ unless verify_login_change
84
+ set_redirect_error_status(invalid_key_error_status)
85
+ set_redirect_error_flash verify_login_change_duplicate_account_error_flash
86
+ redirect verify_login_change_duplicate_account_redirect
87
+ end
80
88
  remove_verify_login_change_key
81
89
  after_verify_login_change
82
90
  end
@@ -96,7 +104,11 @@ module Rodauth
96
104
  end
97
105
 
98
106
  def verify_login_change
99
- update_account(login_column=>verify_login_change_new_login)
107
+ unless res = _update_login(verify_login_change_new_login)
108
+ remove_verify_login_change_key
109
+ end
110
+
111
+ res
100
112
  end
101
113
 
102
114
  def account_from_verify_login_change_key(key)
@@ -134,10 +146,21 @@ module Rodauth
134
146
  end
135
147
 
136
148
  def update_login(login)
137
- generate_verify_login_change_key_value
138
- @verify_login_change_new_login = login
139
- create_verify_login_change_key(login)
140
- send_verify_login_change_email(login)
149
+ if _account_from_login(login)
150
+ @login_requirement_message = 'already an account with this login'
151
+ return false
152
+ end
153
+
154
+ transaction do
155
+ before_verify_login_change_email
156
+ generate_verify_login_change_key_value
157
+ @verify_login_change_new_login = login
158
+ create_verify_login_change_key(login)
159
+ send_verify_login_change_email(login)
160
+ after_verify_login_change_email
161
+ end
162
+
163
+ true
141
164
  end
142
165
 
143
166
  def generate_verify_login_change_key_value
@@ -150,7 +173,7 @@ module Rodauth
150
173
  ds.where((Sequel::CURRENT_TIMESTAMP > verify_login_change_deadline_column) | ~Sequel.expr(verify_login_change_login_column=>login)).delete
151
174
  if e = raised_uniqueness_violation{ds.insert(verify_login_change_key_insert_hash(login))}
152
175
  old_login, key = get_verify_login_change_login_and_key(account_id)
153
- # If inserting into the verify account table causes a violation, we can pull the
176
+ # If inserting into the verify login change table causes a violation, we can pull the
154
177
  # key from the verify login change table if the logins match, or reraise.
155
178
  @verify_login_change_key_value = if old_login.downcase == login.downcase
156
179
  key
@@ -43,15 +43,9 @@ CREATE FUNCTION #{get_salt_name}(acct_id int8) RETURNS varchar(255)
43
43
  SQL SECURITY DEFINER
44
44
  READS SQL DATA
45
45
  BEGIN
46
- DECLARE salt varchar(255);
47
- DECLARE csr CURSOR FOR
48
- SELECT substr(password_hash, 1, 30)
46
+ RETURN (SELECT substr(password_hash, 1, 30)
49
47
  FROM #{table_name}
50
- WHERE acct_id = id;
51
- OPEN csr;
52
- FETCH csr INTO salt;
53
- CLOSE csr;
54
- RETURN salt;
48
+ WHERE acct_id = id);
55
49
  END;
56
50
  END
57
51
 
@@ -6,7 +6,7 @@ module Rodauth
6
6
  MAJOR = 1
7
7
 
8
8
  # The minor version of Rodauth, updated for new feature releases of Rodauth.
9
- MINOR = 18
9
+ MINOR = 19
10
10
 
11
11
  # The patch version of Rodauth, updated only for bug fixes from the last
12
12
  # feature release.
@@ -0,0 +1,285 @@
1
+ require File.expand_path("spec_helper", File.dirname(__FILE__))
2
+
3
+ describe 'Rodauth email auth feature' do
4
+ it "should support logging in use link sent via email, without a password for the account" do
5
+ rodauth do
6
+ enable :login, :email_auth, :logout
7
+ account_password_hash_column :ph
8
+ end
9
+ roda do |r|
10
+ r.rodauth
11
+ r.root{view :content=>""}
12
+ end
13
+
14
+ DB[:accounts].update(:ph=>nil).must_equal 1
15
+
16
+ visit '/login'
17
+ fill_in 'Login', :with=>'foo2@example.com'
18
+ click_button 'Login'
19
+ page.find('#error_flash').text.must_equal 'There was an error logging in'
20
+ page.html.must_include("no matching login")
21
+
22
+ fill_in 'Login', :with=>'foo@example.com'
23
+ click_button 'Login'
24
+ page.find('#notice_flash').text.must_equal "An email has been sent to you with a link to login to your account"
25
+ page.current_path.must_equal '/'
26
+ link = email_link(/(\/email-auth\?key=.+)$/)
27
+
28
+ visit link[0...-1]
29
+ page.find('#error_flash').text.must_equal "invalid email authentication key"
30
+
31
+ visit '/login'
32
+ fill_in 'Login', :with=>'foo@example.com'
33
+ click_button 'Login'
34
+ page.find('#error_flash').text.must_equal "An email has recently been sent to you with a link to login"
35
+ Mail::TestMailer.deliveries.must_equal []
36
+
37
+ DB[:account_email_auth_keys].update(:email_last_sent => Time.now - 250).must_equal 1
38
+ visit '/login'
39
+ fill_in 'Login', :with=>'foo@example.com'
40
+ click_button 'Login'
41
+ page.find('#error_flash').text.must_equal "An email has recently been sent to you with a link to login"
42
+ Mail::TestMailer.deliveries.must_equal []
43
+
44
+ DB[:account_email_auth_keys].update(:email_last_sent => Time.now - 350).must_equal 1
45
+ visit '/login'
46
+ fill_in 'Login', :with=>'foo@example.com'
47
+ click_button 'Login'
48
+ email_link(/(\/email-auth\?key=.+)$/).must_equal link
49
+
50
+ visit link
51
+ page.title.must_equal 'Login'
52
+ click_button 'Login'
53
+ page.find('#notice_flash').text.must_equal 'You have been logged in'
54
+ page.current_path.must_equal '/'
55
+
56
+ logout
57
+
58
+ visit link
59
+ visit '/login'
60
+ fill_in 'Login', :with=>'foo@example.com'
61
+ click_button 'Login'
62
+
63
+ link2 = email_link(/(\/email-auth\?key=.+)$/)
64
+ link2.wont_equal link
65
+
66
+ visit link2
67
+ DB[:account_email_auth_keys].update(:deadline => Time.now - 60).must_equal 1
68
+ click_button 'Login'
69
+ page.find('#error_flash').text.must_equal "There was an error logging you in"
70
+ page.current_path.must_equal '/'
71
+ DB[:account_email_auth_keys].count.must_equal 0
72
+
73
+ visit '/login'
74
+ fill_in 'Login', :with=>'foo@example.com'
75
+ click_button 'Login'
76
+
77
+ visit email_link(/(\/email-auth\?key=.+)$/)
78
+ DB[:account_email_auth_keys].update(:key=>'1').must_equal 1
79
+ click_button 'Login'
80
+ page.find('#error_flash').text.must_equal "There was an error logging you in"
81
+ page.current_path.must_equal '/'
82
+ end
83
+
84
+ it "should support logging in use link sent via email, with a password for the account" do
85
+ rodauth do
86
+ enable :login, :email_auth, :logout
87
+ email_auth_email_last_sent_column nil
88
+ end
89
+ roda do |r|
90
+ r.rodauth
91
+ r.root{view :content=>""}
92
+ end
93
+
94
+ visit '/login'
95
+ fill_in 'Login', :with=>'foo2@example.com'
96
+ click_button 'Login'
97
+ page.find('#error_flash').text.must_equal 'There was an error logging in'
98
+ page.html.must_include("no matching login")
99
+
100
+ fill_in 'Login', :with=>'foo@example.com'
101
+ click_button 'Login'
102
+ click_button 'Send Login Link Via Email'
103
+ page.find('#notice_flash').text.must_equal "An email has been sent to you with a link to login to your account"
104
+ page.current_path.must_equal '/'
105
+ link = email_link(/(\/email-auth\?key=.+)$/)
106
+
107
+ visit link[0...-1]
108
+ page.find('#error_flash').text.must_equal "invalid email authentication key"
109
+
110
+ visit '/login'
111
+ fill_in 'Login', :with=>'foo@example.com'
112
+ click_button 'Login'
113
+ click_button 'Send Login Link Via Email'
114
+ email_link(/(\/email-auth\?key=.+)$/).must_equal link
115
+
116
+ visit link
117
+ page.title.must_equal 'Login'
118
+ click_button 'Login'
119
+ page.find('#notice_flash').text.must_equal 'You have been logged in'
120
+ page.current_path.must_equal '/'
121
+
122
+ logout
123
+
124
+ visit link
125
+ visit '/login'
126
+ fill_in 'Login', :with=>'foo@example.com'
127
+ click_button 'Login'
128
+ click_button 'Send Login Link Via Email'
129
+
130
+ link2 = email_link(/(\/email-auth\?key=.+)$/)
131
+ link2.wont_equal link
132
+
133
+ visit link2
134
+ DB[:account_email_auth_keys].update(:deadline => Time.now - 60).must_equal 1
135
+ click_button 'Login'
136
+ page.find('#error_flash').text.must_equal "There was an error logging you in"
137
+ page.current_path.must_equal '/'
138
+ DB[:account_email_auth_keys].count.must_equal 0
139
+
140
+ visit '/login'
141
+ fill_in 'Login', :with=>'foo@example.com'
142
+ click_button 'Login'
143
+ click_button 'Send Login Link Via Email'
144
+
145
+ visit email_link(/(\/email-auth\?key=.+)$/)
146
+ DB[:account_email_auth_keys].update(:key=>'1').must_equal 1
147
+ click_button 'Login'
148
+ page.find('#error_flash').text.must_equal "There was an error logging you in"
149
+ page.current_path.must_equal '/'
150
+ end
151
+
152
+ it "should allow password login for accounts with password hashes" do
153
+ rodauth do
154
+ enable :login, :email_auth
155
+ end
156
+ roda do |r|
157
+ r.rodauth
158
+ next unless rodauth.logged_in?
159
+ r.root{view :content=>"Logged In"}
160
+ end
161
+
162
+ visit '/login'
163
+ page.title.must_equal 'Login'
164
+ fill_in 'Login', :with=>'foo@example.com'
165
+ click_button 'Login'
166
+ page.html.must_include 'Send Login Link Via Email'
167
+ fill_in 'Password', :with=>'0123456789'
168
+ click_button 'Login'
169
+ page.current_path.must_equal '/'
170
+ page.find('#notice_flash').text.must_equal 'You have been logged in'
171
+ end
172
+
173
+ it "should work with creating accounts without setting passwords" do
174
+ rodauth do
175
+ enable :login, :create_account, :email_auth
176
+ require_login_confirmation? false
177
+ create_account_autologin? false
178
+ create_account_set_password? false
179
+ end
180
+ roda do |r|
181
+ r.rodauth
182
+ r.root{view :content=>""}
183
+ end
184
+
185
+ visit '/create-account'
186
+ fill_in 'Login', :with=>'foo@example2.com'
187
+ click_button 'Create Account'
188
+ page.find('#notice_flash').text.must_equal "Your account has been created"
189
+
190
+ visit '/login'
191
+ fill_in 'Login', :with=>'foo@example2.com'
192
+ click_button 'Login'
193
+ page.current_path.must_equal '/'
194
+ visit email_link(/(\/email-auth\?key=.+)$/, 'foo@example2.com')
195
+ page.title.must_equal 'Login'
196
+ click_button 'Login'
197
+ page.find('#notice_flash').text.must_equal 'You have been logged in'
198
+ page.current_path.must_equal '/'
199
+ end
200
+
201
+ it "should clear email auth token when closing account" do
202
+ rodauth do
203
+ enable :login, :email_auth, :close_account
204
+ end
205
+ roda do |r|
206
+ r.rodauth
207
+ r.root{view :content=>rodauth.logged_in? ? "Logged In" : "Not Logged"}
208
+ end
209
+
210
+ visit '/login'
211
+ page.title.must_equal 'Login'
212
+ fill_in 'Login', :with=>'foo@example.com'
213
+ click_button 'Login'
214
+ click_button 'Send Login Link Via Email'
215
+
216
+ hash = DB[:account_email_auth_keys].first
217
+
218
+ visit email_link(/(\/email-auth\?key=.+)$/)
219
+ click_button 'Login'
220
+
221
+ DB[:account_email_auth_keys].count.must_equal 0
222
+ DB[:account_email_auth_keys].insert(hash)
223
+
224
+ visit '/close-account'
225
+ fill_in 'Password', :with=>'0123456789'
226
+ click_button 'Close Account'
227
+ DB[:account_email_auth_keys].count.must_equal 0
228
+ end
229
+
230
+ it "should handle uniqueness errors raised when inserting email auth token" do
231
+ rodauth do
232
+ enable :login, :email_auth
233
+ end
234
+ roda do |r|
235
+ def rodauth.raised_uniqueness_violation(*) super; true; end
236
+ r.rodauth
237
+ r.root{view :content=>""}
238
+ end
239
+
240
+ visit '/login'
241
+ page.title.must_equal 'Login'
242
+ fill_in 'Login', :with=>'foo@example.com'
243
+ click_button 'Login'
244
+ click_button 'Send Login Link Via Email'
245
+ link = email_link(/(\/email-auth\?key=.+)$/)
246
+
247
+ DB[:account_email_auth_keys].update(:email_last_sent => Time.now - 350).must_equal 1
248
+ visit '/login'
249
+ page.title.must_equal 'Login'
250
+ fill_in 'Login', :with=>'foo@example.com'
251
+ click_button 'Login'
252
+ click_button 'Send Login Link Via Email'
253
+ email_link(/(\/email-auth\?key=.+)$/).must_equal link
254
+ end
255
+
256
+ it "should support email auth for accounts via jwt" do
257
+ rodauth do
258
+ enable :login, :email_auth
259
+ email_auth_email_body{email_auth_email_link}
260
+ end
261
+ roda(:jwt) do |r|
262
+ r.rodauth
263
+ end
264
+
265
+ res = json_request('/email-auth-request')
266
+ res.must_equal [401, {"error"=>"There was an error requesting an email link to authenticate"}]
267
+
268
+ res = json_request('/email-auth-request', :login=>'foo@example2.com')
269
+ res.must_equal [401, {"error"=>"There was an error requesting an email link to authenticate"}]
270
+
271
+ res = json_request('/email-auth-request', :login=>'foo@example.com')
272
+ res.must_equal [200, {"success"=>"An email has been sent to you with a link to login to your account"}]
273
+
274
+ link = email_link(/key=.+$/)
275
+ res = json_request('/email-auth')
276
+ res.must_equal [401, {"error"=>"There was an error logging you in"}]
277
+
278
+ res = json_request('/email-auth', :key=>link[4...-1])
279
+ res.must_equal [401, {"error"=>"There was an error logging you in"}]
280
+
281
+ res = json_request('/email-auth', :key=>link[4..-1])
282
+ res.must_equal [200, {"success"=>"You have been logged in"}]
283
+ end
284
+ end
285
+