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