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
|
@@ -4,10 +4,6 @@ module Rodauth
|
|
|
4
4
|
Feature.define(:verify_account, :VerifyAccount) do
|
|
5
5
|
depends :login, :create_account, :email_base
|
|
6
6
|
|
|
7
|
-
def_deprecated_alias :attempt_to_create_unverified_account_error_flash, :attempt_to_create_unverified_account_notice_message
|
|
8
|
-
def_deprecated_alias :attempt_to_login_to_unverified_account_error_flash, :attempt_to_login_to_unverified_account_notice_message
|
|
9
|
-
def_deprecated_alias :no_matching_verify_account_key_error_flash, :no_matching_verify_account_key_message
|
|
10
|
-
|
|
11
7
|
error_flash "Unable to verify account"
|
|
12
8
|
error_flash "Unable to resend verify account email", 'verify_account_resend'
|
|
13
9
|
error_flash "An email has recently been sent to you with a link to verify your account", 'verify_account_email_recently_sent'
|
|
@@ -31,26 +27,29 @@ module Rodauth
|
|
|
31
27
|
redirect(:verify_account_email_sent){default_post_email_redirect}
|
|
32
28
|
redirect(:verify_account_email_recently_sent){default_post_email_redirect}
|
|
33
29
|
|
|
34
|
-
|
|
30
|
+
translatable_method :verify_account_email_subject, 'Verify Account'
|
|
35
31
|
auth_value_method :verify_account_key_param, 'key'
|
|
36
32
|
auth_value_method :verify_account_autologin?, true
|
|
37
33
|
auth_value_method :verify_account_table, :account_verification_keys
|
|
38
34
|
auth_value_method :verify_account_id_column, :id
|
|
39
|
-
auth_value_method :verify_account_email_last_sent_column,
|
|
35
|
+
auth_value_method :verify_account_email_last_sent_column, :email_last_sent
|
|
40
36
|
auth_value_method :verify_account_skip_resend_email_within, 300
|
|
41
37
|
auth_value_method :verify_account_key_column, :key
|
|
42
|
-
|
|
38
|
+
translatable_method :verify_account_resend_explanatory_text, "<p>If you no longer have the email to verify the account, you can request that it be resent to you:</p>"
|
|
39
|
+
translatable_method :verify_account_resend_link_text, "Resend Verify Account Information"
|
|
43
40
|
session_key :verify_account_session_key, :verify_account_key
|
|
44
|
-
auth_value_method :verify_account_set_password?,
|
|
41
|
+
auth_value_method :verify_account_set_password?, true
|
|
45
42
|
|
|
46
43
|
auth_methods(
|
|
47
44
|
:allow_resending_verify_account_email?,
|
|
48
45
|
:create_verify_account_key,
|
|
49
46
|
:create_verify_account_email,
|
|
50
47
|
:get_verify_account_key,
|
|
48
|
+
:get_verify_account_email_last_sent,
|
|
51
49
|
:remove_verify_account_key,
|
|
52
50
|
:resend_verify_account_view,
|
|
53
51
|
:send_verify_account_email,
|
|
52
|
+
:set_verify_account_email_last_sent,
|
|
54
53
|
:verify_account,
|
|
55
54
|
:verify_account_email_body,
|
|
56
55
|
:verify_account_email_link,
|
|
@@ -98,7 +97,7 @@ module Rodauth
|
|
|
98
97
|
|
|
99
98
|
r.get do
|
|
100
99
|
if key = param_or_nil(verify_account_key_param)
|
|
101
|
-
|
|
100
|
+
set_session_value(verify_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_verify_account_key(key)
|
|
107
106
|
verify_account_view
|
|
108
107
|
else
|
|
109
|
-
|
|
108
|
+
remove_session_value(verify_account_session_key)
|
|
110
109
|
set_redirect_error_flash no_matching_verify_account_key_error_flash
|
|
111
110
|
redirect require_login_redirect
|
|
112
111
|
end
|
|
@@ -145,10 +144,10 @@ module Rodauth
|
|
|
145
144
|
end
|
|
146
145
|
|
|
147
146
|
if verify_account_autologin?
|
|
148
|
-
|
|
147
|
+
autologin_session('verify_account')
|
|
149
148
|
end
|
|
150
149
|
|
|
151
|
-
|
|
150
|
+
remove_session_value(verify_account_session_key)
|
|
152
151
|
set_notice_flash verify_account_notice_flash
|
|
153
152
|
redirect verify_account_redirect
|
|
154
153
|
end
|
|
@@ -158,6 +157,10 @@ module Rodauth
|
|
|
158
157
|
end
|
|
159
158
|
end
|
|
160
159
|
|
|
160
|
+
def require_login_confirmation?
|
|
161
|
+
false
|
|
162
|
+
end
|
|
163
|
+
|
|
161
164
|
def allow_resending_verify_account_email?
|
|
162
165
|
account[account_status_column] == account_unverified_status_value
|
|
163
166
|
end
|
|
@@ -220,14 +223,6 @@ module Rodauth
|
|
|
220
223
|
false
|
|
221
224
|
end
|
|
222
225
|
|
|
223
|
-
def login_form_footer
|
|
224
|
-
super + verify_account_resend_link
|
|
225
|
-
end
|
|
226
|
-
|
|
227
|
-
def verify_account_resend_link
|
|
228
|
-
"<p><a href=\"#{verify_account_resend_path}\">Resend Verify Account Information</a></p>"
|
|
229
|
-
end
|
|
230
|
-
|
|
231
226
|
def create_account_set_password?
|
|
232
227
|
return false if verify_account_set_password?
|
|
233
228
|
super
|
|
@@ -247,6 +242,14 @@ module Rodauth
|
|
|
247
242
|
|
|
248
243
|
private
|
|
249
244
|
|
|
245
|
+
def _login_form_footer_links
|
|
246
|
+
links = super
|
|
247
|
+
if !param_or_nil(login_param) || ((account || account_from_login(param(login_param))) && allow_resending_verify_account_email?)
|
|
248
|
+
links << [30, verify_account_resend_path, verify_account_resend_link_text]
|
|
249
|
+
end
|
|
250
|
+
links
|
|
251
|
+
end
|
|
252
|
+
|
|
250
253
|
def verify_account_email_recently_sent?
|
|
251
254
|
(email_last_sent = get_verify_account_email_last_sent) && (Time.now - email_last_sent < verify_account_skip_resend_email_within)
|
|
252
255
|
end
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module Rodauth
|
|
4
4
|
Feature.define(:verify_account_grace_period, :VerifyAccountGracePeriod) do
|
|
5
5
|
depends :verify_account
|
|
6
|
-
error_flash "
|
|
6
|
+
error_flash "Please verify this account before changing the login", "unverified_change_login"
|
|
7
7
|
redirect :unverified_change_login
|
|
8
8
|
|
|
9
9
|
auth_value_method :verification_requested_at_column, :requested_at
|
|
@@ -26,6 +26,17 @@ module Rodauth
|
|
|
26
26
|
super || account_in_unverified_grace_period?
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
+
def verify_account_set_password?
|
|
30
|
+
false
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def update_session
|
|
34
|
+
super
|
|
35
|
+
if account_in_unverified_grace_period?
|
|
36
|
+
set_session_value(unverified_account_session_key, true)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
29
40
|
private
|
|
30
41
|
|
|
31
42
|
def after_close_account
|
|
@@ -41,6 +52,13 @@ module Rodauth
|
|
|
41
52
|
super if defined?(super)
|
|
42
53
|
end
|
|
43
54
|
|
|
55
|
+
def allow_email_auth?
|
|
56
|
+
if defined?(super)
|
|
57
|
+
return false unless super
|
|
58
|
+
end
|
|
59
|
+
!account_in_unverified_grace_period?
|
|
60
|
+
end
|
|
61
|
+
|
|
44
62
|
def verify_account_check_already_logged_in
|
|
45
63
|
nil
|
|
46
64
|
end
|
|
@@ -56,13 +74,6 @@ module Rodauth
|
|
|
56
74
|
s
|
|
57
75
|
end
|
|
58
76
|
|
|
59
|
-
def update_session
|
|
60
|
-
super
|
|
61
|
-
if account_in_unverified_grace_period?
|
|
62
|
-
session[unverified_account_session_key] = true
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
|
|
66
77
|
def account_in_unverified_grace_period?
|
|
67
78
|
account[account_status_column] == account_unverified_status_value &&
|
|
68
79
|
verify_account_grace_period &&
|
|
@@ -4,10 +4,8 @@ module Rodauth
|
|
|
4
4
|
Feature.define(:verify_login_change, :VerifyLoginChange) do
|
|
5
5
|
depends :change_login, :email_base
|
|
6
6
|
|
|
7
|
-
def_deprecated_alias :no_matching_verify_login_change_key_error_flash, :no_matching_verify_login_change_key_message
|
|
8
|
-
|
|
9
7
|
error_flash "Unable to verify login change"
|
|
10
|
-
error_flash "Unable to change login as there is already an account with the new login",
|
|
8
|
+
error_flash "Unable to change login as there is already an account with the new login", 'verify_login_change_duplicate_account'
|
|
11
9
|
error_flash "There was an error verifying your login change: invalid verify login change key", 'no_matching_verify_login_change_key'
|
|
12
10
|
notice_flash "Your login change has been verified"
|
|
13
11
|
loaded_templates %w'verify-login-change verify-login-change-email'
|
|
@@ -23,8 +21,8 @@ module Rodauth
|
|
|
23
21
|
|
|
24
22
|
auth_value_method :verify_login_change_autologin?, false
|
|
25
23
|
auth_value_method :verify_login_change_deadline_column, :deadline
|
|
26
|
-
auth_value_method :verify_login_change_deadline_interval, {:days=>1}
|
|
27
|
-
|
|
24
|
+
auth_value_method :verify_login_change_deadline_interval, {:days=>1}.freeze
|
|
25
|
+
translatable_method :verify_login_change_email_subject, 'Verify Login Change'
|
|
28
26
|
auth_value_method :verify_login_change_id_column, :id
|
|
29
27
|
auth_value_method :verify_login_change_key_column, :key
|
|
30
28
|
auth_value_method :verify_login_change_key_param, 'key'
|
|
@@ -52,12 +50,11 @@ module Rodauth
|
|
|
52
50
|
)
|
|
53
51
|
|
|
54
52
|
route do |r|
|
|
55
|
-
check_already_logged_in
|
|
56
53
|
before_verify_login_change_route
|
|
57
54
|
|
|
58
55
|
r.get do
|
|
59
56
|
if key = param_or_nil(verify_login_change_key_param)
|
|
60
|
-
|
|
57
|
+
set_session_value(verify_login_change_session_key, key)
|
|
61
58
|
redirect(r.path)
|
|
62
59
|
end
|
|
63
60
|
|
|
@@ -65,7 +62,7 @@ module Rodauth
|
|
|
65
62
|
if account_from_verify_login_change_key(key)
|
|
66
63
|
verify_login_change_view
|
|
67
64
|
else
|
|
68
|
-
|
|
65
|
+
remove_session_value(verify_login_change_session_key)
|
|
69
66
|
set_redirect_error_flash no_matching_verify_login_change_key_error_flash
|
|
70
67
|
redirect require_login_redirect
|
|
71
68
|
end
|
|
@@ -92,15 +89,19 @@ module Rodauth
|
|
|
92
89
|
end
|
|
93
90
|
|
|
94
91
|
if verify_login_change_autologin?
|
|
95
|
-
|
|
92
|
+
autologin_session('verify_login_change')
|
|
96
93
|
end
|
|
97
94
|
|
|
98
|
-
|
|
95
|
+
remove_session_value(verify_login_change_session_key)
|
|
99
96
|
set_notice_flash verify_login_change_notice_flash
|
|
100
97
|
redirect verify_login_change_redirect
|
|
101
98
|
end
|
|
102
99
|
end
|
|
103
100
|
|
|
101
|
+
def require_login_confirmation?
|
|
102
|
+
false
|
|
103
|
+
end
|
|
104
|
+
|
|
104
105
|
def remove_verify_login_change_key
|
|
105
106
|
verify_login_change_ds.delete
|
|
106
107
|
end
|
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
# frozen-string-literal: true
|
|
2
|
+
|
|
3
|
+
require 'webauthn'
|
|
4
|
+
|
|
5
|
+
module Rodauth
|
|
6
|
+
Feature.define(:webauthn, :Webauthn) do
|
|
7
|
+
depends :two_factor_base
|
|
8
|
+
|
|
9
|
+
loaded_templates %w'webauthn-setup webauthn-auth webauthn-remove'
|
|
10
|
+
|
|
11
|
+
view 'webauthn-setup', 'Setup WebAuthn Authentication', 'webauthn_setup'
|
|
12
|
+
view 'webauthn-auth', 'Authenticate Using WebAuthn', 'webauthn_auth'
|
|
13
|
+
view 'webauthn-remove', 'Remove WebAuthn Authenticator', 'webauthn_remove'
|
|
14
|
+
|
|
15
|
+
additional_form_tags 'webauthn_setup'
|
|
16
|
+
additional_form_tags 'webauthn_auth'
|
|
17
|
+
additional_form_tags 'webauthn_remove'
|
|
18
|
+
|
|
19
|
+
before :webauthn_setup
|
|
20
|
+
before :webauthn_auth
|
|
21
|
+
before :webauthn_remove
|
|
22
|
+
|
|
23
|
+
after :webauthn_setup
|
|
24
|
+
after :webauthn_auth_failure
|
|
25
|
+
after :webauthn_remove
|
|
26
|
+
|
|
27
|
+
button 'Setup WebAuthn Authentication', 'webauthn_setup'
|
|
28
|
+
button 'Authenticate Using WebAuthn', 'webauthn_auth'
|
|
29
|
+
button 'Remove WebAuthn Authenticator', 'webauthn_remove'
|
|
30
|
+
|
|
31
|
+
redirect :webauthn_setup
|
|
32
|
+
redirect :webauthn_remove
|
|
33
|
+
|
|
34
|
+
notice_flash "WebAuthn authentication is now setup", 'webauthn_setup'
|
|
35
|
+
notice_flash "WebAuthn authenticator has been removed", 'webauthn_remove'
|
|
36
|
+
|
|
37
|
+
error_flash "Error setting up WebAuthn authentication", 'webauthn_setup'
|
|
38
|
+
error_flash "Error authenticating using WebAuthn", 'webauthn_auth'
|
|
39
|
+
error_flash 'This account has not been setup for WebAuthn authentication', 'webauthn_not_setup'
|
|
40
|
+
error_flash "Error removing WebAuthn authenticator", 'webauthn_remove'
|
|
41
|
+
|
|
42
|
+
session_key :authenticated_webauthn_id_session_key, :webauthn_id
|
|
43
|
+
|
|
44
|
+
translatable_method :webauthn_auth_link_text, "Authenticate Using WebAuthn"
|
|
45
|
+
translatable_method :webauthn_setup_link_text, "Setup WebAuthn Authentication"
|
|
46
|
+
translatable_method :webauthn_remove_link_text, "Remove WebAuthn Authenticator"
|
|
47
|
+
|
|
48
|
+
auth_value_method :webauthn_setup_param, 'webauthn_setup'
|
|
49
|
+
auth_value_method :webauthn_auth_param, 'webauthn_auth'
|
|
50
|
+
auth_value_method :webauthn_remove_param, 'webauthn_remove'
|
|
51
|
+
auth_value_method :webauthn_setup_challenge_param, 'webauthn_setup_challenge'
|
|
52
|
+
auth_value_method :webauthn_setup_challenge_hmac_param, 'webauthn_setup_challenge_hmac'
|
|
53
|
+
auth_value_method :webauthn_auth_challenge_param, 'webauthn_auth_challenge'
|
|
54
|
+
auth_value_method :webauthn_auth_challenge_hmac_param, 'webauthn_auth_challenge_hmac'
|
|
55
|
+
|
|
56
|
+
auth_value_method :webauthn_keys_account_id_column, :account_id
|
|
57
|
+
auth_value_method :webauthn_keys_webauthn_id_column, :webauthn_id
|
|
58
|
+
auth_value_method :webauthn_keys_public_key_column, :public_key
|
|
59
|
+
auth_value_method :webauthn_keys_sign_count_column, :sign_count
|
|
60
|
+
auth_value_method :webauthn_keys_last_use_column, :last_use
|
|
61
|
+
auth_value_method :webauthn_keys_table, :account_webauthn_keys
|
|
62
|
+
|
|
63
|
+
auth_value_method :webauthn_user_ids_account_id_column, :id
|
|
64
|
+
auth_value_method :webauthn_user_ids_webauthn_id_column, :webauthn_id
|
|
65
|
+
auth_value_method :webauthn_user_ids_table, :account_webauthn_user_ids
|
|
66
|
+
|
|
67
|
+
auth_value_method :webauthn_setup_js, File.binread(File.expand_path('../../../../javascript/webauthn_setup.js', __FILE__)).freeze
|
|
68
|
+
auth_value_method :webauthn_auth_js, File.binread(File.expand_path('../../../../javascript/webauthn_auth.js', __FILE__)).freeze
|
|
69
|
+
auth_value_method :webauthn_js_host, ''
|
|
70
|
+
|
|
71
|
+
auth_value_method :webauthn_setup_timeout, 120000
|
|
72
|
+
auth_value_method :webauthn_auth_timeout, 60000
|
|
73
|
+
auth_value_method :webauthn_user_verification, 'discouraged'
|
|
74
|
+
auth_value_method :webauthn_attestation, 'none'
|
|
75
|
+
|
|
76
|
+
auth_value_method :webauthn_not_setup_error_status, 403
|
|
77
|
+
|
|
78
|
+
translatable_method :webauthn_invalid_setup_param_message, "invalid webauthn setup param"
|
|
79
|
+
translatable_method :webauthn_duplicate_webauthn_id_message, "attempt to insert duplicate webauthn id"
|
|
80
|
+
translatable_method :webauthn_invalid_auth_param_message, "invalid webauthn authentication param"
|
|
81
|
+
translatable_method :webauthn_invalid_sign_count_message, "webauthn credential has invalid sign count"
|
|
82
|
+
translatable_method :webauthn_invalid_remove_param_message, "must select valid webauthn authenticator to remove"
|
|
83
|
+
|
|
84
|
+
auth_value_methods(
|
|
85
|
+
:webauthn_authenticator_selection,
|
|
86
|
+
:webauthn_extensions,
|
|
87
|
+
:webauthn_origin,
|
|
88
|
+
:webauthn_rp_id,
|
|
89
|
+
:webauthn_rp_name,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
auth_methods(
|
|
93
|
+
:account_webauthn_ids,
|
|
94
|
+
:account_webauthn_usage,
|
|
95
|
+
:account_webauthn_user_id,
|
|
96
|
+
:add_webauthn_credential,
|
|
97
|
+
:authenticated_webauthn_id,
|
|
98
|
+
:handle_webauthn_sign_count_verification_error,
|
|
99
|
+
:new_webauthn_credential,
|
|
100
|
+
:remove_webauthn_key,
|
|
101
|
+
:remove_all_webauthn_keys_and_user_ids,
|
|
102
|
+
:valid_new_webauthn_credential?,
|
|
103
|
+
:valid_webauthn_credential_auth?,
|
|
104
|
+
:webauthn_auth_js_path,
|
|
105
|
+
:webauth_credential_options_for_get,
|
|
106
|
+
:webauthn_remove_authenticated_session,
|
|
107
|
+
:webauthn_setup_js_path,
|
|
108
|
+
:webauthn_update_session,
|
|
109
|
+
:webauthn_user_name,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
route(:webauthn_auth_js) do |r|
|
|
113
|
+
before_webauthn_auth_js_route
|
|
114
|
+
r.get do
|
|
115
|
+
response['Content-Type'] = 'text/javascript'
|
|
116
|
+
webauthn_auth_js
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
route(:webauthn_auth) do |r|
|
|
121
|
+
require_login
|
|
122
|
+
require_account_session
|
|
123
|
+
require_two_factor_not_authenticated('webauthn')
|
|
124
|
+
require_webauthn_setup
|
|
125
|
+
before_webauthn_auth_route
|
|
126
|
+
|
|
127
|
+
r.get do
|
|
128
|
+
webauthn_auth_view
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
r.post do
|
|
132
|
+
catch_error do
|
|
133
|
+
webauthn_credential = webauthn_auth_credential_from_form_submission
|
|
134
|
+
transaction do
|
|
135
|
+
before_webauthn_auth
|
|
136
|
+
webauthn_update_session(webauthn_credential.id)
|
|
137
|
+
two_factor_authenticate('webauthn')
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
after_webauthn_auth_failure
|
|
142
|
+
set_error_flash webauthn_auth_error_flash
|
|
143
|
+
webauthn_auth_view
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
route(:webauthn_setup_js) do |r|
|
|
148
|
+
before_webauthn_setup_js_route
|
|
149
|
+
r.get do
|
|
150
|
+
response['Content-Type'] = 'text/javascript'
|
|
151
|
+
webauthn_setup_js
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
route(:webauthn_setup) do |r|
|
|
156
|
+
require_authentication unless two_factor_login_type_match?('webauthn')
|
|
157
|
+
require_account_session
|
|
158
|
+
before_webauthn_setup_route
|
|
159
|
+
|
|
160
|
+
r.get do
|
|
161
|
+
webauthn_setup_view
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
r.post do
|
|
165
|
+
catch_error do
|
|
166
|
+
webauthn_credential = webauthn_setup_credential_from_form_submission
|
|
167
|
+
throw_error = false
|
|
168
|
+
|
|
169
|
+
transaction do
|
|
170
|
+
before_webauthn_setup
|
|
171
|
+
|
|
172
|
+
if raises_uniqueness_violation?{add_webauthn_credential(webauthn_credential)}
|
|
173
|
+
throw_error = true
|
|
174
|
+
raise Sequel::Rollback
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
unless two_factor_authenticated?
|
|
178
|
+
webauthn_update_session(webauthn_credential.id)
|
|
179
|
+
two_factor_update_session('webauthn')
|
|
180
|
+
end
|
|
181
|
+
after_webauthn_setup
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
if throw_error
|
|
185
|
+
throw_error_status(invalid_field_error_status, webauthn_setup_param, webauthn_duplicate_webauthn_id_message)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
set_notice_flash webauthn_setup_notice_flash
|
|
189
|
+
redirect webauthn_setup_redirect
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
set_error_flash webauthn_setup_error_flash
|
|
193
|
+
webauthn_setup_view
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
route(:webauthn_remove) do |r|
|
|
198
|
+
require_authentication unless two_factor_login_type_match?('webauthn')
|
|
199
|
+
require_account_session
|
|
200
|
+
require_webauthn_setup
|
|
201
|
+
before_webauthn_remove_route
|
|
202
|
+
|
|
203
|
+
r.get do
|
|
204
|
+
webauthn_remove_view
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
r.post do
|
|
208
|
+
catch_error do
|
|
209
|
+
unless webauthn_id = param_or_nil(webauthn_remove_param)
|
|
210
|
+
throw_error_status(invalid_field_error_status, webauthn_remove_param, webauthn_invalid_remove_param_message)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
unless two_factor_password_match?(param(password_param))
|
|
214
|
+
throw_error_status(invalid_password_error_status, password_param, invalid_password_message)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
transaction do
|
|
218
|
+
before_webauthn_remove
|
|
219
|
+
unless remove_webauthn_key(webauthn_id)
|
|
220
|
+
throw_error_status(invalid_field_error_status, webauthn_remove_param, webauthn_invalid_remove_param_message)
|
|
221
|
+
end
|
|
222
|
+
if authenticated_webauthn_id == webauthn_id && two_factor_login_type_match?('webauthn')
|
|
223
|
+
webauthn_remove_authenticated_session
|
|
224
|
+
two_factor_remove_session('webauthn')
|
|
225
|
+
end
|
|
226
|
+
after_webauthn_remove
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
set_notice_flash webauthn_remove_notice_flash
|
|
230
|
+
redirect webauthn_remove_redirect
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
set_error_flash webauthn_remove_error_flash
|
|
234
|
+
webauthn_remove_view
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def webauthn_auth_form_path
|
|
239
|
+
webauthn_auth_path
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def authenticated_webauthn_id
|
|
243
|
+
session[authenticated_webauthn_id_session_key]
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def webauthn_remove_authenticated_session
|
|
247
|
+
remove_session_value(authenticated_webauthn_id_session_key)
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def webauthn_update_session(webauthn_id)
|
|
251
|
+
set_session_value(authenticated_webauthn_id_session_key, webauthn_id)
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def webauthn_authenticator_selection
|
|
255
|
+
{'requireResidentKey' => false, 'userVerification' => webauthn_user_verification}
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def webauthn_extensions
|
|
259
|
+
{}
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def account_webauthn_ids
|
|
263
|
+
webauthn_keys_ds.select_map(webauthn_keys_webauthn_id_column)
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def account_webauthn_usage
|
|
267
|
+
webauthn_keys_ds.select_hash(webauthn_keys_webauthn_id_column, webauthn_keys_last_use_column)
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def account_webauthn_user_id
|
|
271
|
+
unless webauthn_id = webauthn_user_ids_ds.get(webauthn_user_ids_webauthn_id_column)
|
|
272
|
+
webauthn_id = WebAuthn.generate_user_id
|
|
273
|
+
if e = raised_uniqueness_violation do
|
|
274
|
+
webauthn_user_ids_ds.insert(
|
|
275
|
+
webauthn_user_ids_account_id_column => webauthn_account_id,
|
|
276
|
+
webauthn_user_ids_webauthn_id_column => webauthn_id
|
|
277
|
+
)
|
|
278
|
+
end
|
|
279
|
+
# If two requests to create a webauthn user id are sent at the same time and an insert
|
|
280
|
+
# is attempted for both, one will fail with a unique constraint violation. In that case
|
|
281
|
+
# it is safe for the second one to use the webauthn user id inserted by the other request.
|
|
282
|
+
# If there is still no webauthn user id at this point, then we'll just reraise the
|
|
283
|
+
# exception.
|
|
284
|
+
# :nocov:
|
|
285
|
+
raise e unless webauthn_id = webauthn_user_ids_ds.get(webauthn_user_ids_webauthn_id_column)
|
|
286
|
+
# :nocov:
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
webauthn_id
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def new_webauthn_credential
|
|
294
|
+
WebAuthn::Credential.options_for_create(
|
|
295
|
+
:timeout => webauthn_setup_timeout,
|
|
296
|
+
:rp => {:name=>webauthn_rp_name, :id=>webauthn_rp_id},
|
|
297
|
+
:user => {:id=>account_webauthn_user_id, :name=>webauthn_user_name},
|
|
298
|
+
:authenticator_selection => webauthn_authenticator_selection,
|
|
299
|
+
:attestation => webauthn_attestation,
|
|
300
|
+
:extensions => webauthn_extensions,
|
|
301
|
+
:exclude => account_webauthn_ids,
|
|
302
|
+
)
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def valid_new_webauthn_credential?(webauthn_credential)
|
|
306
|
+
# Hack around inability to override expected_origin
|
|
307
|
+
origin = webauthn_origin
|
|
308
|
+
webauthn_credential.response.define_singleton_method(:verify) do |expected_challenge, expected_origin = nil, **kw|
|
|
309
|
+
super(expected_challenge, expected_origin || origin, **kw)
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
(challenge = param_or_nil(webauthn_setup_challenge_param)) &&
|
|
313
|
+
(hmac = param_or_nil(webauthn_setup_challenge_hmac_param)) &&
|
|
314
|
+
timing_safe_eql?(compute_hmac(challenge), hmac) &&
|
|
315
|
+
webauthn_credential.verify(challenge)
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def webauth_credential_options_for_get
|
|
319
|
+
WebAuthn::Credential.options_for_get(
|
|
320
|
+
:allow => account_webauthn_ids,
|
|
321
|
+
:timeout => webauthn_auth_timeout,
|
|
322
|
+
:rp_id => webauthn_rp_id,
|
|
323
|
+
:user_verification => webauthn_user_verification,
|
|
324
|
+
:extensions => webauthn_extensions,
|
|
325
|
+
)
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def webauthn_user_name
|
|
329
|
+
(account || account_from_session)[login_column]
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def webauthn_origin
|
|
333
|
+
base_url
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def webauthn_rp_id
|
|
337
|
+
webauthn_origin.sub(/\Ahttps?:\/\//, '')
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def webauthn_rp_name
|
|
341
|
+
webauthn_rp_id
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def handle_webauthn_sign_count_verification_error
|
|
345
|
+
throw_error_status(invalid_field_error_status, webauthn_auth_param, webauthn_invalid_sign_count_message)
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def add_webauthn_credential(webauthn_credential)
|
|
349
|
+
webauthn_keys_ds.insert(
|
|
350
|
+
webauthn_keys_account_id_column => webauthn_account_id,
|
|
351
|
+
webauthn_keys_webauthn_id_column => webauthn_credential.id,
|
|
352
|
+
webauthn_keys_public_key_column => webauthn_credential.public_key,
|
|
353
|
+
webauthn_keys_sign_count_column => Integer(webauthn_credential.sign_count)
|
|
354
|
+
)
|
|
355
|
+
super if defined?(super)
|
|
356
|
+
nil
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def valid_webauthn_credential_auth?(webauthn_credential)
|
|
360
|
+
ds = webauthn_keys_ds.where(webauthn_keys_webauthn_id_column => webauthn_credential.id)
|
|
361
|
+
pub_key, sign_count = ds.get([webauthn_keys_public_key_column, webauthn_keys_sign_count_column])
|
|
362
|
+
|
|
363
|
+
# Hack around inability to override expected_origin
|
|
364
|
+
origin = webauthn_origin
|
|
365
|
+
webauthn_credential.response.define_singleton_method(:verify) do |expected_challenge, expected_origin = nil, **kw|
|
|
366
|
+
super(expected_challenge, expected_origin || origin, **kw)
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
(challenge = param_or_nil(webauthn_auth_challenge_param)) &&
|
|
370
|
+
(hmac = param_or_nil(webauthn_auth_challenge_hmac_param)) &&
|
|
371
|
+
timing_safe_eql?(compute_hmac(challenge), hmac) &&
|
|
372
|
+
webauthn_credential.verify(challenge, public_key: pub_key, sign_count: sign_count) &&
|
|
373
|
+
ds.update(
|
|
374
|
+
webauthn_keys_sign_count_column => Integer(webauthn_credential.sign_count),
|
|
375
|
+
webauthn_keys_last_use_column => Sequel::CURRENT_TIMESTAMP
|
|
376
|
+
) == 1
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def remove_webauthn_key(webauthn_id)
|
|
380
|
+
ret = webauthn_keys_ds.where(webauthn_keys_webauthn_id_column=>webauthn_id).delete == 1
|
|
381
|
+
super if defined?(super)
|
|
382
|
+
ret
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
def remove_all_webauthn_keys_and_user_ids
|
|
386
|
+
webauthn_user_ids_ds.delete
|
|
387
|
+
webauthn_keys_ds.delete
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def webauthn_setup?
|
|
391
|
+
!webauthn_keys_ds.empty?
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
def require_webauthn_setup
|
|
395
|
+
unless webauthn_setup?
|
|
396
|
+
set_redirect_error_status(webauthn_not_setup_error_status)
|
|
397
|
+
set_redirect_error_flash webauthn_not_setup_error_flash
|
|
398
|
+
redirect two_factor_need_setup_redirect
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
def two_factor_remove
|
|
403
|
+
super
|
|
404
|
+
remove_all_webauthn_keys_and_user_ids
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
def possible_authentication_methods
|
|
408
|
+
methods = super
|
|
409
|
+
methods << 'webauthn' if webauthn_setup?
|
|
410
|
+
methods
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
private
|
|
414
|
+
|
|
415
|
+
def _two_factor_auth_links
|
|
416
|
+
links = super
|
|
417
|
+
links << [10, webauthn_auth_path, webauthn_auth_link_text] if webauthn_setup? && !two_factor_login_type_match?('webauthn')
|
|
418
|
+
links
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
def _two_factor_setup_links
|
|
422
|
+
super << [10, webauthn_setup_path, webauthn_setup_link_text]
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
def _two_factor_remove_links
|
|
426
|
+
links = super
|
|
427
|
+
links << [10, webauthn_remove_path, webauthn_remove_link_text] if webauthn_setup?
|
|
428
|
+
links
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
def _two_factor_remove_all_from_session
|
|
432
|
+
two_factor_remove_session('webauthn')
|
|
433
|
+
remove_session_value(authenticated_webauthn_id_session_key)
|
|
434
|
+
super
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
def webauthn_account_id
|
|
438
|
+
session_value
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
def webauthn_user_ids_ds
|
|
442
|
+
db[webauthn_user_ids_table].where(webauthn_user_ids_account_id_column => webauthn_account_id)
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
def webauthn_keys_ds
|
|
446
|
+
db[webauthn_keys_table].where(webauthn_keys_account_id_column => webauthn_account_id)
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
def webauthn_auth_credential_from_form_submission
|
|
450
|
+
case auth_data = raw_param(webauthn_auth_param)
|
|
451
|
+
when String
|
|
452
|
+
begin
|
|
453
|
+
auth_data = JSON.parse(auth_data)
|
|
454
|
+
rescue
|
|
455
|
+
throw_error_status(invalid_field_error_status, webauthn_auth_param, webauthn_invalid_auth_param_message)
|
|
456
|
+
end
|
|
457
|
+
when Hash
|
|
458
|
+
# nothing
|
|
459
|
+
else
|
|
460
|
+
throw_error_status(invalid_field_error_status, webauthn_auth_param, webauthn_invalid_auth_param_message)
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
begin
|
|
464
|
+
webauthn_credential = WebAuthn::Credential.from_get(auth_data)
|
|
465
|
+
unless valid_webauthn_credential_auth?(webauthn_credential)
|
|
466
|
+
throw_error_status(invalid_key_error_status, webauthn_auth_param, webauthn_invalid_auth_param_message)
|
|
467
|
+
end
|
|
468
|
+
rescue WebAuthn::SignCountVerificationError
|
|
469
|
+
handle_webauthn_sign_count_verification_error
|
|
470
|
+
rescue WebAuthn::Error, RuntimeError, NoMethodError
|
|
471
|
+
throw_error_status(invalid_field_error_status, webauthn_auth_param, webauthn_invalid_auth_param_message)
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
webauthn_credential
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
def webauthn_setup_credential_from_form_submission
|
|
478
|
+
case setup_data = raw_param(webauthn_setup_param)
|
|
479
|
+
when String
|
|
480
|
+
begin
|
|
481
|
+
setup_data = JSON.parse(setup_data)
|
|
482
|
+
rescue
|
|
483
|
+
throw_error_status(invalid_field_error_status, webauthn_setup_param, webauthn_invalid_setup_param_message)
|
|
484
|
+
end
|
|
485
|
+
when Hash
|
|
486
|
+
# nothing
|
|
487
|
+
else
|
|
488
|
+
throw_error_status(invalid_field_error_status, webauthn_setup_param, webauthn_invalid_setup_param_message)
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
unless two_factor_password_match?(param(password_param))
|
|
492
|
+
throw_error_status(invalid_password_error_status, password_param, invalid_password_message)
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
begin
|
|
496
|
+
webauthn_credential = WebAuthn::Credential.from_create(setup_data)
|
|
497
|
+
unless valid_new_webauthn_credential?(webauthn_credential)
|
|
498
|
+
throw_error_status(invalid_field_error_status, webauthn_setup_param, webauthn_invalid_setup_param_message)
|
|
499
|
+
end
|
|
500
|
+
rescue WebAuthn::Error, RuntimeError, NoMethodError
|
|
501
|
+
throw_error_status(invalid_field_error_status, webauthn_setup_param, webauthn_invalid_setup_param_message)
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
webauthn_credential
|
|
505
|
+
end
|
|
506
|
+
end
|
|
507
|
+
end
|