rodauth 1.18.0 → 1.19.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|