rodauth 1.23.0 → 2.0.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 +132 -0
- data/MIT-LICENSE +1 -1
- data/README.rdoc +207 -79
- data/doc/account_expiration.rdoc +12 -26
- data/doc/active_sessions.rdoc +49 -0
- data/doc/audit_logging.rdoc +44 -0
- data/doc/base.rdoc +74 -128
- data/doc/change_login.rdoc +7 -14
- data/doc/change_password.rdoc +9 -13
- data/doc/change_password_notify.rdoc +2 -2
- data/doc/close_account.rdoc +9 -16
- data/doc/confirm_password.rdoc +12 -5
- data/doc/create_account.rdoc +11 -22
- data/doc/disallow_password_reuse.rdoc +6 -13
- data/doc/email_auth.rdoc +15 -14
- data/doc/email_base.rdoc +5 -15
- data/doc/http_basic_auth.rdoc +10 -1
- data/doc/jwt.rdoc +22 -22
- data/doc/jwt_cors.rdoc +2 -3
- data/doc/jwt_refresh.rdoc +12 -8
- data/doc/lockout.rdoc +17 -15
- data/doc/login.rdoc +10 -2
- data/doc/login_password_requirements_base.rdoc +15 -37
- data/doc/logout.rdoc +2 -2
- data/doc/otp.rdoc +24 -19
- data/doc/password_complexity.rdoc +10 -26
- data/doc/password_expiration.rdoc +11 -25
- data/doc/password_grace_period.rdoc +16 -2
- data/doc/recovery_codes.rdoc +18 -12
- data/doc/release_notes/2.0.0.txt +361 -0
- data/doc/remember.rdoc +40 -64
- data/doc/reset_password.rdoc +12 -9
- data/doc/session_expiration.rdoc +1 -0
- data/doc/single_session.rdoc +16 -25
- data/doc/sms_codes.rdoc +24 -14
- data/doc/two_factor_base.rdoc +60 -22
- data/doc/verify_account.rdoc +14 -12
- data/doc/verify_account_grace_period.rdoc +6 -2
- data/doc/verify_login_change.rdoc +9 -8
- data/doc/webauthn.rdoc +115 -0
- data/doc/webauthn_login.rdoc +15 -0
- data/doc/webauthn_verify_account.rdoc +9 -0
- data/javascript/webauthn_auth.js +45 -0
- data/javascript/webauthn_setup.js +35 -0
- data/lib/roda/plugins/rodauth.rb +1 -1
- data/lib/rodauth.rb +29 -24
- data/lib/rodauth/features/account_expiration.rb +5 -5
- data/lib/rodauth/features/active_sessions.rb +160 -0
- data/lib/rodauth/features/audit_logging.rb +96 -0
- data/lib/rodauth/features/base.rb +131 -47
- data/lib/rodauth/features/change_password_notify.rb +1 -1
- data/lib/rodauth/features/confirm_password.rb +40 -2
- data/lib/rodauth/features/create_account.rb +7 -13
- data/lib/rodauth/features/disallow_common_passwords.rb +1 -1
- data/lib/rodauth/features/disallow_password_reuse.rb +1 -1
- data/lib/rodauth/features/email_auth.rb +29 -27
- data/lib/rodauth/features/email_base.rb +3 -3
- data/lib/rodauth/features/http_basic_auth.rb +44 -37
- data/lib/rodauth/features/jwt.rb +51 -8
- data/lib/rodauth/features/jwt_refresh.rb +3 -3
- data/lib/rodauth/features/lockout.rb +11 -13
- data/lib/rodauth/features/login.rb +48 -8
- data/lib/rodauth/features/login_password_requirements_base.rb +4 -4
- data/lib/rodauth/features/otp.rb +71 -81
- data/lib/rodauth/features/password_complexity.rb +4 -11
- data/lib/rodauth/features/password_expiration.rb +1 -1
- data/lib/rodauth/features/password_grace_period.rb +17 -10
- data/lib/rodauth/features/recovery_codes.rb +47 -51
- data/lib/rodauth/features/remember.rb +11 -27
- data/lib/rodauth/features/reset_password.rb +25 -25
- data/lib/rodauth/features/session_expiration.rb +6 -4
- data/lib/rodauth/features/single_session.rb +7 -5
- data/lib/rodauth/features/sms_codes.rb +58 -67
- data/lib/rodauth/features/two_factor_base.rb +132 -28
- data/lib/rodauth/features/verify_account.rb +23 -20
- data/lib/rodauth/features/verify_account_grace_period.rb +19 -8
- data/lib/rodauth/features/verify_login_change.rb +11 -10
- data/lib/rodauth/features/webauthn.rb +507 -0
- data/lib/rodauth/features/webauthn_login.rb +70 -0
- data/lib/rodauth/features/webauthn_verify_account.rb +46 -0
- data/lib/rodauth/version.rb +2 -2
- data/templates/button.str +1 -3
- data/templates/change-login.str +1 -2
- data/templates/change-password.str +3 -5
- data/templates/close-account.str +2 -2
- data/templates/confirm-password.str +1 -1
- data/templates/create-account.str +1 -1
- data/templates/email-auth-request-form.str +1 -2
- data/templates/email-auth.str +1 -1
- data/templates/global-logout-field.str +6 -0
- data/templates/login-confirm-field.str +2 -4
- data/templates/login-display.str +3 -2
- data/templates/login-field.str +2 -4
- data/templates/login-form-footer.str +6 -0
- data/templates/login-form.str +7 -0
- data/templates/login.str +1 -9
- data/templates/logout.str +1 -1
- data/templates/multi-phase-login.str +3 -0
- data/templates/otp-auth-code-field.str +5 -3
- data/templates/otp-auth.str +1 -1
- data/templates/otp-disable.str +1 -1
- data/templates/otp-setup.str +3 -3
- data/templates/password-confirm-field.str +2 -4
- data/templates/password-field.str +2 -4
- data/templates/recovery-auth.str +3 -6
- data/templates/recovery-codes.str +1 -1
- data/templates/remember.str +15 -20
- data/templates/reset-password-request.str +2 -2
- data/templates/reset-password.str +1 -2
- data/templates/sms-auth.str +1 -1
- data/templates/sms-code-field.str +5 -3
- data/templates/sms-confirm.str +1 -2
- data/templates/sms-disable.str +1 -2
- data/templates/sms-request.str +1 -1
- data/templates/sms-setup.str +6 -4
- data/templates/two-factor-auth.str +5 -0
- data/templates/two-factor-disable.str +6 -0
- data/templates/two-factor-manage.str +16 -0
- data/templates/unlock-account-request.str +2 -2
- data/templates/unlock-account.str +1 -1
- data/templates/verify-account-resend.str +1 -1
- data/templates/verify-account.str +1 -2
- data/templates/verify-login-change.str +1 -1
- data/templates/webauthn-auth.str +11 -0
- data/templates/webauthn-remove.str +14 -0
- data/templates/webauthn-setup.str +12 -0
- metadata +64 -11
- data/doc/verify_change_login.rdoc +0 -11
- data/lib/rodauth/features/verify_change_login.rb +0 -20
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen-string-literal: true
|
|
2
2
|
|
|
3
3
|
module Rodauth
|
|
4
|
-
|
|
4
|
+
Feature.define(:jwt_refresh, :JwtRefresh) do
|
|
5
5
|
depends :jwt
|
|
6
6
|
|
|
7
7
|
after 'refresh_token'
|
|
@@ -10,10 +10,10 @@ module Rodauth
|
|
|
10
10
|
auth_value_method :jwt_access_token_key, 'access_token'
|
|
11
11
|
auth_value_method :jwt_access_token_not_before_period, 5
|
|
12
12
|
auth_value_method :jwt_access_token_period, 1800
|
|
13
|
-
|
|
13
|
+
translatable_method :jwt_refresh_invalid_token_message, 'invalid JWT refresh token'
|
|
14
14
|
auth_value_method :jwt_refresh_token_account_id_column, :account_id
|
|
15
15
|
auth_value_method :jwt_refresh_token_deadline_column, :deadline
|
|
16
|
-
auth_value_method :jwt_refresh_token_deadline_interval, {:days=>14}
|
|
16
|
+
auth_value_method :jwt_refresh_token_deadline_interval, {:days=>14}.freeze
|
|
17
17
|
auth_value_method :jwt_refresh_token_id_column, :id
|
|
18
18
|
auth_value_method :jwt_refresh_token_key, 'refresh_token'
|
|
19
19
|
auth_value_method :jwt_refresh_token_key_column, :key
|
|
@@ -4,8 +4,6 @@ module Rodauth
|
|
|
4
4
|
Feature.define(:lockout, :Lockout) do
|
|
5
5
|
depends :login, :email_base
|
|
6
6
|
|
|
7
|
-
def_deprecated_alias :no_matching_unlock_account_key_error_flash, :no_matching_unlock_account_key_message
|
|
8
|
-
|
|
9
7
|
loaded_templates %w'unlock-account-request unlock-account password-field unlock-account-email'
|
|
10
8
|
view 'unlock-account-request', 'Request Account Unlock', 'unlock_account_request'
|
|
11
9
|
view 'unlock-account', 'Unlock Account', 'unlock_account'
|
|
@@ -19,7 +17,7 @@ module Rodauth
|
|
|
19
17
|
button 'Unlock Account', 'unlock_account'
|
|
20
18
|
button 'Request Account Unlock', 'unlock_account_request'
|
|
21
19
|
error_flash "There was an error unlocking your account", 'unlock_account'
|
|
22
|
-
error_flash "This account is currently locked out and cannot be logged in to
|
|
20
|
+
error_flash "This account is currently locked out and cannot be logged in to", "login_lockout"
|
|
23
21
|
error_flash "An email has recently been sent to you with a link to unlock the account", 'unlock_account_email_recently_sent'
|
|
24
22
|
error_flash "There was an error unlocking your account: invalid or expired unlock account key", 'no_matching_unlock_account_key'
|
|
25
23
|
notice_flash "Your account has been unlocked", 'unlock_account'
|
|
@@ -36,12 +34,12 @@ module Rodauth
|
|
|
36
34
|
auth_value_method :account_lockouts_table, :account_lockouts
|
|
37
35
|
auth_value_method :account_lockouts_id_column, :id
|
|
38
36
|
auth_value_method :account_lockouts_key_column, :key
|
|
39
|
-
auth_value_method :account_lockouts_email_last_sent_column,
|
|
37
|
+
auth_value_method :account_lockouts_email_last_sent_column, :email_last_sent
|
|
40
38
|
auth_value_method :account_lockouts_deadline_column, :deadline
|
|
41
|
-
auth_value_method :account_lockouts_deadline_interval, {:days=>1}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
39
|
+
auth_value_method :account_lockouts_deadline_interval, {:days=>1}.freeze
|
|
40
|
+
translatable_method :unlock_account_email_subject, 'Unlock Account'
|
|
41
|
+
translatable_method :unlock_account_explanatory_text, '<p>This account is currently locked out. You can unlock the account:</p>'
|
|
42
|
+
translatable_method :unlock_account_request_explanatory_text, '<p>This account is currently locked out. You can request that the account be unlocked:</p>'
|
|
45
43
|
auth_value_method :unlock_account_key_param, 'key'
|
|
46
44
|
auth_value_method :unlock_account_requires_password?, false
|
|
47
45
|
auth_value_method :unlock_account_skip_resend_email_within, 300
|
|
@@ -75,6 +73,7 @@ module Rodauth
|
|
|
75
73
|
redirect unlock_account_email_recently_sent_redirect
|
|
76
74
|
end
|
|
77
75
|
|
|
76
|
+
@unlock_account_key_value = get_unlock_account_key
|
|
78
77
|
transaction do
|
|
79
78
|
before_unlock_account_request
|
|
80
79
|
set_unlock_account_email_last_sent
|
|
@@ -98,7 +97,7 @@ module Rodauth
|
|
|
98
97
|
|
|
99
98
|
r.get do
|
|
100
99
|
if key = param_or_nil(unlock_account_key_param)
|
|
101
|
-
|
|
100
|
+
set_session_value(unlock_account_session_key, key)
|
|
102
101
|
redirect(r.path)
|
|
103
102
|
end
|
|
104
103
|
|
|
@@ -106,7 +105,7 @@ module Rodauth
|
|
|
106
105
|
if account_from_unlock_key(key)
|
|
107
106
|
unlock_account_view
|
|
108
107
|
else
|
|
109
|
-
|
|
108
|
+
remove_session_value(unlock_account_session_key)
|
|
110
109
|
set_redirect_error_flash no_matching_unlock_account_key_error_flash
|
|
111
110
|
redirect require_login_redirect
|
|
112
111
|
end
|
|
@@ -127,11 +126,11 @@ module Rodauth
|
|
|
127
126
|
unlock_account
|
|
128
127
|
after_unlock_account
|
|
129
128
|
if unlock_account_autologin?
|
|
130
|
-
|
|
129
|
+
autologin_session('unlock_account')
|
|
131
130
|
end
|
|
132
131
|
end
|
|
133
132
|
|
|
134
|
-
|
|
133
|
+
remove_session_value(unlock_account_session_key)
|
|
135
134
|
set_notice_flash unlock_account_notice_flash
|
|
136
135
|
redirect unlock_account_redirect
|
|
137
136
|
else
|
|
@@ -217,7 +216,6 @@ module Rodauth
|
|
|
217
216
|
end
|
|
218
217
|
|
|
219
218
|
def send_unlock_account_email
|
|
220
|
-
@unlock_account_key_value = get_unlock_account_key
|
|
221
219
|
send_email(create_unlock_account_email)
|
|
222
220
|
end
|
|
223
221
|
|
|
@@ -5,16 +5,24 @@ module Rodauth
|
|
|
5
5
|
notice_flash "You have been logged in"
|
|
6
6
|
notice_flash "Login recognized, please enter your password", "need_password"
|
|
7
7
|
error_flash "There was an error logging in"
|
|
8
|
-
loaded_templates %w'login login-field password-field login-display'
|
|
8
|
+
loaded_templates %w'login login-form login-form-footer multi-phase-login login-field password-field login-display'
|
|
9
9
|
view 'login', 'Login'
|
|
10
|
+
view 'multi-phase-login', 'Login', 'multi_phase_login'
|
|
10
11
|
additional_form_tags
|
|
11
12
|
button 'Login'
|
|
12
13
|
redirect
|
|
13
14
|
|
|
14
15
|
auth_value_method :login_error_status, 401
|
|
15
|
-
|
|
16
|
+
translatable_method :login_form_footer_links_heading, '<h2 class="rodauth-login-form-footer-links-heading">Other Options</h2>'
|
|
17
|
+
auth_value_method :login_return_to_requested_location?, false
|
|
16
18
|
auth_value_method :use_multi_phase_login?, false
|
|
17
19
|
|
|
20
|
+
session_key :login_redirect_session_key, :login_redirect
|
|
21
|
+
|
|
22
|
+
auth_cached_method :multi_phase_login_forms
|
|
23
|
+
auth_cached_method :login_form_footer_links
|
|
24
|
+
auth_cached_method :login_form_footer
|
|
25
|
+
|
|
18
26
|
route do |r|
|
|
19
27
|
check_already_logged_in
|
|
20
28
|
before_login_route
|
|
@@ -24,8 +32,8 @@ module Rodauth
|
|
|
24
32
|
end
|
|
25
33
|
|
|
26
34
|
r.post do
|
|
27
|
-
clear_session
|
|
28
35
|
skip_error_flash = false
|
|
36
|
+
view = :login_view
|
|
29
37
|
|
|
30
38
|
catch_error do
|
|
31
39
|
unless account_from_login(param(login_param))
|
|
@@ -40,6 +48,7 @@ module Rodauth
|
|
|
40
48
|
|
|
41
49
|
if use_multi_phase_login?
|
|
42
50
|
@valid_login_entered = true
|
|
51
|
+
view = :multi_phase_login_view
|
|
43
52
|
|
|
44
53
|
unless param_or_nil(password_param)
|
|
45
54
|
after_login_entered_during_multi_phase_login
|
|
@@ -53,18 +62,28 @@ module Rodauth
|
|
|
53
62
|
throw_error_status(login_error_status, password_param, invalid_password_message)
|
|
54
63
|
end
|
|
55
64
|
|
|
56
|
-
_login
|
|
65
|
+
_login('password')
|
|
57
66
|
end
|
|
58
67
|
|
|
59
68
|
set_error_flash login_error_flash unless skip_error_flash
|
|
60
|
-
|
|
69
|
+
send(view)
|
|
61
70
|
end
|
|
62
71
|
end
|
|
63
72
|
|
|
64
73
|
attr_reader :login_form_header
|
|
65
74
|
|
|
75
|
+
def login_required
|
|
76
|
+
if login_return_to_requested_location?
|
|
77
|
+
set_session_value(login_redirect_session_key, request.fullpath)
|
|
78
|
+
end
|
|
79
|
+
super
|
|
80
|
+
end
|
|
81
|
+
|
|
66
82
|
def after_login_entered_during_multi_phase_login
|
|
67
83
|
set_notice_now_flash need_password_notice_flash
|
|
84
|
+
if multi_phase_login_forms.length == 1 && (meth = multi_phase_login_forms[0][2])
|
|
85
|
+
send(meth)
|
|
86
|
+
end
|
|
68
87
|
end
|
|
69
88
|
|
|
70
89
|
def skip_login_field_on_login?
|
|
@@ -85,16 +104,37 @@ module Rodauth
|
|
|
85
104
|
"<input type='hidden' name=\"#{login_param}\" value=\"#{scope.h param(login_param)}\" />"
|
|
86
105
|
end
|
|
87
106
|
|
|
107
|
+
def render_multi_phase_login_forms
|
|
108
|
+
multi_phase_login_forms.sort.map{|_, form, _| form}.join("\n")
|
|
109
|
+
end
|
|
110
|
+
|
|
88
111
|
private
|
|
89
112
|
|
|
90
|
-
def
|
|
113
|
+
def _login_form_footer_links
|
|
114
|
+
[]
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def _multi_phase_login_forms
|
|
118
|
+
forms = []
|
|
119
|
+
forms << [10, render("login-form"), nil] if has_password?
|
|
120
|
+
forms
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def _login_form_footer
|
|
124
|
+
return '' if _login_form_footer_links.empty?
|
|
125
|
+
render('login-form-footer')
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def _login(auth_type)
|
|
129
|
+
saved_login_redirect = remove_session_value(login_redirect_session_key)
|
|
91
130
|
transaction do
|
|
92
131
|
before_login
|
|
93
|
-
|
|
132
|
+
login_session(auth_type)
|
|
133
|
+
yield if block_given?
|
|
94
134
|
after_login
|
|
95
135
|
end
|
|
96
136
|
set_notice_flash login_notice_flash
|
|
97
|
-
redirect login_redirect
|
|
137
|
+
redirect(saved_login_redirect || login_redirect)
|
|
98
138
|
end
|
|
99
139
|
end
|
|
100
140
|
end
|
|
@@ -2,18 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
module Rodauth
|
|
4
4
|
Feature.define(:login_password_requirements_base, :LoginPasswordRequirementsBase) do
|
|
5
|
-
|
|
5
|
+
translatable_method :already_an_account_with_this_login_message, 'already an account with this login'
|
|
6
6
|
auth_value_method :login_confirm_param, 'login-confirm'
|
|
7
7
|
auth_value_method :login_minimum_length, 3
|
|
8
8
|
auth_value_method :login_maximum_length, 255
|
|
9
|
-
|
|
9
|
+
translatable_method :logins_do_not_match_message, 'logins do not match'
|
|
10
10
|
auth_value_method :password_confirm_param, 'password-confirm'
|
|
11
11
|
auth_value_method :password_minimum_length, 6
|
|
12
|
-
|
|
12
|
+
translatable_method :passwords_do_not_match_message, 'passwords do not match'
|
|
13
13
|
auth_value_method :require_email_address_logins?, true
|
|
14
14
|
auth_value_method :require_login_confirmation?, true
|
|
15
15
|
auth_value_method :require_password_confirmation?, true
|
|
16
|
-
|
|
16
|
+
translatable_method :same_as_existing_password_message, "invalid password, same as current password"
|
|
17
17
|
|
|
18
18
|
auth_value_methods(
|
|
19
19
|
:login_confirm_label,
|
data/lib/rodauth/features/otp.rb
CHANGED
|
@@ -19,62 +19,59 @@ module Rodauth
|
|
|
19
19
|
before 'otp_setup'
|
|
20
20
|
before 'otp_disable'
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
before_otp_auth_route(&block)
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
button 'Authenticate via 2nd Factor', 'otp_auth'
|
|
30
|
-
button 'Disable Two Factor Authentication', 'otp_disable'
|
|
31
|
-
button 'Setup Two Factor Authentication', 'otp_setup'
|
|
22
|
+
button 'Authenticate Using TOTP', 'otp_auth'
|
|
23
|
+
button 'Disable TOTP Authentication', 'otp_disable'
|
|
24
|
+
button 'Setup TOTP Authentication', 'otp_setup'
|
|
32
25
|
|
|
33
|
-
error_flash "Error disabling
|
|
34
|
-
error_flash "Error logging in via
|
|
35
|
-
error_flash "Error setting up
|
|
36
|
-
error_flash "You have already setup
|
|
26
|
+
error_flash "Error disabling TOTP authentication", 'otp_disable'
|
|
27
|
+
error_flash "Error logging in via TOTP authentication", 'otp_auth'
|
|
28
|
+
error_flash "Error setting up TOTP authentication", 'otp_setup'
|
|
29
|
+
error_flash "You have already setup TOTP authentication", 'otp_already_setup'
|
|
30
|
+
error_flash "TOTP authentication code use locked out due to numerous failures", 'otp_lockout'
|
|
37
31
|
|
|
38
|
-
notice_flash "
|
|
39
|
-
notice_flash "
|
|
32
|
+
notice_flash "TOTP authentication has been disabled", 'otp_disable'
|
|
33
|
+
notice_flash "TOTP authentication is now setup", 'otp_setup'
|
|
40
34
|
|
|
41
35
|
redirect :otp_disable
|
|
42
36
|
redirect :otp_already_setup
|
|
43
37
|
redirect :otp_setup
|
|
38
|
+
redirect(:otp_lockout){two_factor_auth_required_redirect}
|
|
44
39
|
|
|
45
40
|
loaded_templates %w'otp-disable otp-auth otp-setup otp-auth-code-field password-field'
|
|
46
|
-
view 'otp-disable', 'Disable
|
|
41
|
+
view 'otp-disable', 'Disable TOTP Authentication', 'otp_disable'
|
|
47
42
|
view 'otp-auth', 'Enter Authentication Code', 'otp_auth'
|
|
48
|
-
view 'otp-setup', 'Setup
|
|
43
|
+
view 'otp-setup', 'Setup TOTP Authentication', 'otp_setup'
|
|
44
|
+
|
|
45
|
+
translatable_method :otp_auth_link_text, "Authenticate Using TOTP"
|
|
46
|
+
translatable_method :otp_setup_link_text, "Setup TOTP Authentication"
|
|
47
|
+
translatable_method :otp_disable_link_text, "Disable TOTP Authentication"
|
|
49
48
|
|
|
50
49
|
auth_value_method :otp_auth_failures_limit, 5
|
|
51
|
-
|
|
50
|
+
translatable_method :otp_auth_label, 'Authentication Code'
|
|
52
51
|
auth_value_method :otp_auth_param, 'otp'
|
|
53
52
|
auth_value_method :otp_class, ROTP::TOTP
|
|
54
53
|
auth_value_method :otp_digits, nil
|
|
55
|
-
auth_value_method :otp_drift,
|
|
54
|
+
auth_value_method :otp_drift, 30
|
|
56
55
|
auth_value_method :otp_interval, nil
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
translatable_method :otp_invalid_auth_code_message, "Invalid authentication code"
|
|
57
|
+
translatable_method :otp_invalid_secret_message, "invalid secret"
|
|
59
58
|
auth_value_method :otp_keys_column, :key
|
|
60
59
|
auth_value_method :otp_keys_id_column, :id
|
|
61
60
|
auth_value_method :otp_keys_failures_column, :num_failures
|
|
62
61
|
auth_value_method :otp_keys_table, :account_otp_keys
|
|
63
62
|
auth_value_method :otp_keys_last_use_column, :last_use
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
translatable_method :otp_provisioning_uri_label, 'Provisioning URL'
|
|
64
|
+
translatable_method :otp_secret_label, 'Secret'
|
|
66
65
|
auth_value_method :otp_setup_param, 'otp_secret'
|
|
67
66
|
auth_value_method :otp_setup_raw_param, 'otp_raw_secret'
|
|
67
|
+
translatable_method :otp_auth_form_footer, ''
|
|
68
68
|
|
|
69
69
|
auth_cached_method :otp_key
|
|
70
70
|
auth_cached_method :otp
|
|
71
71
|
private :otp
|
|
72
72
|
|
|
73
73
|
auth_value_methods(
|
|
74
|
-
:otp_auth_form_footer,
|
|
75
74
|
:otp_issuer,
|
|
76
|
-
:otp_lockout_error_flash,
|
|
77
|
-
:otp_lockout_redirect,
|
|
78
75
|
:otp_keys_use_hmac?
|
|
79
76
|
)
|
|
80
77
|
|
|
@@ -103,18 +100,13 @@ module Rodauth
|
|
|
103
100
|
route(:otp_auth) do |r|
|
|
104
101
|
require_login
|
|
105
102
|
require_account_session
|
|
106
|
-
require_two_factor_not_authenticated
|
|
103
|
+
require_two_factor_not_authenticated('totp')
|
|
107
104
|
require_otp_setup
|
|
108
105
|
|
|
109
106
|
if otp_locked_out?
|
|
110
107
|
set_response_error_status(lockout_error_status)
|
|
111
108
|
set_redirect_error_flash otp_lockout_error_flash
|
|
112
|
-
|
|
113
|
-
redirect redir
|
|
114
|
-
else
|
|
115
|
-
clear_session
|
|
116
|
-
redirect require_login_redirect
|
|
117
|
-
end
|
|
109
|
+
redirect otp_lockout_redirect
|
|
118
110
|
end
|
|
119
111
|
|
|
120
112
|
before_otp_auth_route
|
|
@@ -126,7 +118,7 @@ module Rodauth
|
|
|
126
118
|
r.post do
|
|
127
119
|
if otp_valid_code?(param(otp_auth_param)) && otp_update_last_use
|
|
128
120
|
before_otp_authentication
|
|
129
|
-
two_factor_authenticate(
|
|
121
|
+
two_factor_authenticate('totp')
|
|
130
122
|
end
|
|
131
123
|
|
|
132
124
|
otp_record_authentication_failure
|
|
@@ -178,7 +170,9 @@ module Rodauth
|
|
|
178
170
|
transaction do
|
|
179
171
|
before_otp_setup
|
|
180
172
|
otp_add_key
|
|
181
|
-
|
|
173
|
+
unless two_factor_authenticated?
|
|
174
|
+
two_factor_update_session('totp')
|
|
175
|
+
end
|
|
182
176
|
after_otp_setup
|
|
183
177
|
end
|
|
184
178
|
set_notice_flash otp_setup_notice_flash
|
|
@@ -204,7 +198,9 @@ module Rodauth
|
|
|
204
198
|
transaction do
|
|
205
199
|
before_otp_disable
|
|
206
200
|
otp_remove
|
|
207
|
-
|
|
201
|
+
if two_factor_login_type_match?('totp')
|
|
202
|
+
two_factor_remove_session('totp')
|
|
203
|
+
end
|
|
208
204
|
after_otp_disable
|
|
209
205
|
end
|
|
210
206
|
set_notice_flash otp_disable_notice_flash
|
|
@@ -218,20 +214,6 @@ module Rodauth
|
|
|
218
214
|
end
|
|
219
215
|
end
|
|
220
216
|
|
|
221
|
-
def two_factor_authentication_setup?
|
|
222
|
-
return true if super
|
|
223
|
-
return false if @otp_tmp_key
|
|
224
|
-
otp_exists?
|
|
225
|
-
end
|
|
226
|
-
|
|
227
|
-
def two_factor_need_setup_redirect
|
|
228
|
-
otp_setup_path
|
|
229
|
-
end
|
|
230
|
-
|
|
231
|
-
def two_factor_auth_required_redirect
|
|
232
|
-
otp_auth_path
|
|
233
|
-
end
|
|
234
|
-
|
|
235
217
|
def two_factor_remove
|
|
236
218
|
super
|
|
237
219
|
otp_remove
|
|
@@ -242,19 +224,6 @@ module Rodauth
|
|
|
242
224
|
otp_remove_auth_failures
|
|
243
225
|
end
|
|
244
226
|
|
|
245
|
-
def otp_auth_form_footer
|
|
246
|
-
super if defined?(super)
|
|
247
|
-
end
|
|
248
|
-
|
|
249
|
-
def otp_lockout_redirect
|
|
250
|
-
return super if defined?(super)
|
|
251
|
-
nil
|
|
252
|
-
end
|
|
253
|
-
|
|
254
|
-
def otp_lockout_error_flash
|
|
255
|
-
"Authentication code use locked out due to numerous failures.#{super if defined?(super)}"
|
|
256
|
-
end
|
|
257
|
-
|
|
258
227
|
def require_otp_setup
|
|
259
228
|
unless otp_exists?
|
|
260
229
|
set_redirect_error_status(two_factor_not_setup_error_status)
|
|
@@ -272,11 +241,11 @@ module Rodauth
|
|
|
272
241
|
ot_pass = ot_pass.gsub(/\s+/, '')
|
|
273
242
|
if drift = otp_drift
|
|
274
243
|
if otp.respond_to?(:verify_with_drift)
|
|
244
|
+
# :nocov:
|
|
275
245
|
otp.verify_with_drift(ot_pass, drift)
|
|
276
|
-
else
|
|
277
246
|
# :nocov:
|
|
247
|
+
else
|
|
278
248
|
otp.verify(ot_pass, :drift_behind=>drift, :drift_ahead=>drift)
|
|
279
|
-
# :nocov:
|
|
280
249
|
end
|
|
281
250
|
else
|
|
282
251
|
otp.verify(ot_pass)
|
|
@@ -285,6 +254,7 @@ module Rodauth
|
|
|
285
254
|
|
|
286
255
|
def otp_remove
|
|
287
256
|
otp_key_ds.delete
|
|
257
|
+
@otp_key = nil
|
|
288
258
|
super if defined?(super)
|
|
289
259
|
end
|
|
290
260
|
|
|
@@ -316,7 +286,7 @@ module Rodauth
|
|
|
316
286
|
end
|
|
317
287
|
|
|
318
288
|
def otp_issuer
|
|
319
|
-
|
|
289
|
+
domain
|
|
320
290
|
end
|
|
321
291
|
|
|
322
292
|
def otp_provisioning_name
|
|
@@ -339,8 +309,37 @@ module Rodauth
|
|
|
339
309
|
!!hmac_secret
|
|
340
310
|
end
|
|
341
311
|
|
|
312
|
+
def possible_authentication_methods
|
|
313
|
+
methods = super
|
|
314
|
+
methods << 'totp' if otp_exists? && !@otp_tmp_key
|
|
315
|
+
methods
|
|
316
|
+
end
|
|
317
|
+
|
|
342
318
|
private
|
|
343
319
|
|
|
320
|
+
def _two_factor_auth_links
|
|
321
|
+
links = super
|
|
322
|
+
links << [20, otp_auth_path, otp_auth_link_text] if otp_exists? && !otp_locked_out?
|
|
323
|
+
links
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def _two_factor_setup_links
|
|
327
|
+
links = super
|
|
328
|
+
links << [20, otp_setup_path, otp_setup_link_text] unless otp_exists?
|
|
329
|
+
links
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def _two_factor_remove_links
|
|
333
|
+
links = super
|
|
334
|
+
links << [20, otp_disable_path, otp_disable_link_text] if otp_exists?
|
|
335
|
+
links
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def _two_factor_remove_all_from_session
|
|
339
|
+
two_factor_remove_session('totp')
|
|
340
|
+
super
|
|
341
|
+
end
|
|
342
|
+
|
|
344
343
|
def clear_cached_otp
|
|
345
344
|
remove_instance_variable(:@otp) if defined?(@otp)
|
|
346
345
|
end
|
|
@@ -364,29 +363,20 @@ module Rodauth
|
|
|
364
363
|
end
|
|
365
364
|
|
|
366
365
|
if ROTP::Base32.respond_to?(:random_base32)
|
|
367
|
-
# :nocov:
|
|
368
366
|
def otp_new_secret
|
|
369
367
|
ROTP::Base32.random_base32.downcase
|
|
370
368
|
end
|
|
371
|
-
# :nocov:
|
|
372
369
|
else
|
|
370
|
+
# :nocov:
|
|
373
371
|
def otp_new_secret
|
|
374
372
|
ROTP::Base32.random.downcase
|
|
375
373
|
end
|
|
374
|
+
# :nocov:
|
|
376
375
|
end
|
|
377
376
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
chars = 'abcdefghijklmnopqrstuvwxyz234567'
|
|
382
|
-
length.times.map{|i|chars[data[i] % 32].chr}.join
|
|
383
|
-
end
|
|
384
|
-
# :nocov:
|
|
385
|
-
else
|
|
386
|
-
def base32_encode(data, length)
|
|
387
|
-
chars = 'abcdefghijklmnopqrstuvwxyz234567'
|
|
388
|
-
length.times.map{|i|chars[data[i].ord % 32]}.join
|
|
389
|
-
end
|
|
377
|
+
def base32_encode(data, length)
|
|
378
|
+
chars = 'abcdefghijklmnopqrstuvwxyz234567'
|
|
379
|
+
length.times.map{|i|chars[data[i].ord % 32]}.join
|
|
390
380
|
end
|
|
391
381
|
|
|
392
382
|
def _otp_tmp_key(secret)
|