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.
- checksums.yaml +4 -4
- data/CHANGELOG +24 -0
- data/README.rdoc +20 -11
- data/doc/base.rdoc +2 -2
- data/doc/email_auth.rdoc +53 -0
- data/doc/email_base.rdoc +4 -0
- data/doc/internals.rdoc +3 -3
- data/doc/lockout.rdoc +28 -48
- data/doc/login.rdoc +4 -4
- data/doc/otp.rdoc +1 -3
- data/doc/release_notes/1.19.0.txt +116 -0
- data/doc/reset_password.rdoc +29 -49
- data/doc/verify_account.rdoc +30 -50
- data/doc/verify_login_change.rdoc +4 -0
- data/lib/rodauth/features/base.rb +0 -1
- data/lib/rodauth/features/change_login.rb +4 -0
- data/lib/rodauth/features/disallow_common_passwords.rb +1 -1
- data/lib/rodauth/features/email_auth.rb +253 -0
- data/lib/rodauth/features/email_base.rb +2 -0
- data/lib/rodauth/features/lockout.rb +35 -6
- data/lib/rodauth/features/login.rb +46 -9
- data/lib/rodauth/features/otp.rb +8 -4
- data/lib/rodauth/features/recovery_codes.rb +0 -2
- data/lib/rodauth/features/remember.rb +1 -1
- data/lib/rodauth/features/reset_password.rb +32 -4
- data/lib/rodauth/features/sms_codes.rb +2 -8
- data/lib/rodauth/features/two_factor_base.rb +22 -15
- data/lib/rodauth/features/verify_account.rb +27 -1
- data/lib/rodauth/features/verify_login_change.rb +30 -7
- data/lib/rodauth/migrations.rb +2 -8
- data/lib/rodauth/version.rb +1 -1
- data/spec/email_auth_spec.rb +285 -0
- data/spec/lockout_spec.rb +24 -2
- data/spec/login_spec.rb +47 -1
- data/spec/migrate/001_tables.rb +13 -0
- data/spec/migrate_travis/001_tables.rb +10 -0
- data/spec/reset_password_spec.rb +20 -2
- data/spec/two_factor_spec.rb +46 -0
- data/spec/verify_account_grace_period_spec.rb +1 -1
- data/spec/verify_account_spec.rb +33 -3
- data/spec/verify_login_change_spec.rb +54 -1
- data/templates/email-auth-email.str +5 -0
- data/templates/email-auth-request-form.str +7 -0
- data/templates/email-auth.str +5 -0
- data/templates/login-display.str +4 -0
- data/templates/login.str +2 -2
- data/templates/otp-setup.str +13 -11
- metadata +12 -2
@@ -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
|
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
|
-
|
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
|
data/lib/rodauth/features/otp.rb
CHANGED
@@ -18,9 +18,13 @@ module Rodauth
|
|
18
18
|
before 'otp_authentication'
|
19
19
|
before 'otp_setup'
|
20
20
|
before 'otp_disable'
|
21
|
-
|
22
|
-
|
23
|
-
|
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
|
-
|
111
|
+
before_otp_auth_route
|
108
112
|
|
109
113
|
r.get do
|
110
114
|
otp_auth_view
|
@@ -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
|
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 :
|
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
|
-
|
43
|
-
|
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
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
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
|