rodauth 1.22.0 → 2.3.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 +190 -0
- data/MIT-LICENSE +1 -1
- data/README.rdoc +210 -80
- 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 +75 -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 +6 -15
- data/doc/guides/admin_activation.rdoc +46 -0
- data/doc/guides/already_authenticated.rdoc +10 -0
- data/doc/guides/alternative_login.rdoc +46 -0
- data/doc/guides/create_account_programmatically.rdoc +38 -0
- data/doc/guides/delay_password.rdoc +25 -0
- data/doc/guides/email_only.rdoc +16 -0
- data/doc/guides/i18n.rdoc +26 -0
- data/doc/{internals.rdoc → guides/internals.rdoc} +0 -0
- data/doc/guides/links.rdoc +12 -0
- data/doc/guides/login_return.rdoc +37 -0
- data/doc/guides/password_column.rdoc +25 -0
- data/doc/guides/password_confirmation.rdoc +37 -0
- data/doc/guides/password_requirements.rdoc +30 -0
- data/doc/guides/paths.rdoc +36 -0
- data/doc/guides/query_params.rdoc +9 -0
- data/doc/guides/redirects.rdoc +17 -0
- data/doc/guides/registration_field.rdoc +68 -0
- data/doc/guides/require_mfa.rdoc +30 -0
- data/doc/guides/reset_password_autologin.rdoc +21 -0
- data/doc/guides/status_column.rdoc +28 -0
- data/doc/guides/totp_or_recovery.rdoc +16 -0
- 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 +23 -8
- data/doc/lockout.rdoc +17 -15
- data/doc/login.rdoc +17 -2
- data/doc/login_password_requirements_base.rdoc +18 -37
- data/doc/logout.rdoc +2 -2
- data/doc/otp.rdoc +25 -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/1.23.0.txt +32 -0
- data/doc/release_notes/2.0.0.txt +361 -0
- data/doc/release_notes/2.1.0.txt +31 -0
- data/doc/release_notes/2.2.0.txt +39 -0
- data/doc/release_notes/2.3.0.txt +37 -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 +36 -28
- data/lib/rodauth/features/account_expiration.rb +5 -5
- data/lib/rodauth/features/active_sessions.rb +158 -0
- data/lib/rodauth/features/audit_logging.rb +98 -0
- data/lib/rodauth/features/base.rb +144 -43
- data/lib/rodauth/features/change_password_notify.rb +2 -2
- data/lib/rodauth/features/close_account.rb +8 -6
- data/lib/rodauth/features/confirm_password.rb +40 -2
- data/lib/rodauth/features/create_account.rb +8 -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 +31 -30
- data/lib/rodauth/features/email_base.rb +9 -4
- data/lib/rodauth/features/http_basic_auth.rb +55 -35
- data/lib/rodauth/features/jwt.rb +63 -16
- data/lib/rodauth/features/jwt_cors.rb +15 -15
- data/lib/rodauth/features/jwt_refresh.rb +42 -13
- data/lib/rodauth/features/lockout.rb +12 -14
- data/lib/rodauth/features/login.rb +64 -15
- data/lib/rodauth/features/login_password_requirements_base.rb +13 -8
- data/lib/rodauth/features/otp.rb +77 -80
- data/lib/rodauth/features/password_complexity.rb +8 -13
- data/lib/rodauth/features/password_expiration.rb +2 -2
- data/lib/rodauth/features/password_grace_period.rb +17 -10
- data/lib/rodauth/features/recovery_codes.rb +49 -53
- data/lib/rodauth/features/remember.rb +11 -27
- data/lib/rodauth/features/reset_password.rb +26 -26
- data/lib/rodauth/features/session_expiration.rb +7 -10
- data/lib/rodauth/features/single_session.rb +8 -6
- data/lib/rodauth/features/sms_codes.rb +62 -72
- data/lib/rodauth/features/two_factor_base.rb +134 -30
- data/lib/rodauth/features/verify_account.rb +29 -21
- data/lib/rodauth/features/verify_account_grace_period.rb +18 -9
- data/lib/rodauth/features/verify_login_change.rb +12 -11
- data/lib/rodauth/features/webauthn.rb +505 -0
- data/lib/rodauth/features/webauthn_login.rb +70 -0
- data/lib/rodauth/features/webauthn_verify_account.rb +46 -0
- data/lib/rodauth/migrations.rb +16 -5
- 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 +2 -3
- 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 +3 -3
- 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 +4 -4
- data/templates/unlock-account.str +1 -1
- data/templates/verify-account-resend.str +3 -3
- 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 +94 -54
- data/Rakefile +0 -179
- data/doc/verify_change_login.rdoc +0 -11
- data/lib/rodauth/features/verify_change_login.rb +0 -20
- data/spec/account_expiration_spec.rb +0 -225
- data/spec/all.rb +0 -1
- data/spec/change_login_spec.rb +0 -156
- data/spec/change_password_notify_spec.rb +0 -33
- data/spec/change_password_spec.rb +0 -202
- data/spec/close_account_spec.rb +0 -162
- data/spec/confirm_password_spec.rb +0 -70
- data/spec/create_account_spec.rb +0 -127
- data/spec/disallow_common_passwords_spec.rb +0 -93
- data/spec/disallow_password_reuse_spec.rb +0 -179
- data/spec/email_auth_spec.rb +0 -285
- data/spec/http_basic_auth_spec.rb +0 -143
- data/spec/jwt_cors_spec.rb +0 -57
- data/spec/jwt_refresh_spec.rb +0 -256
- data/spec/jwt_spec.rb +0 -235
- data/spec/lockout_spec.rb +0 -250
- data/spec/login_spec.rb +0 -328
- data/spec/migrate/001_tables.rb +0 -184
- data/spec/migrate/002_account_password_hash_column.rb +0 -11
- data/spec/migrate_password/001_tables.rb +0 -73
- data/spec/migrate_travis/001_tables.rb +0 -141
- data/spec/password_complexity_spec.rb +0 -109
- data/spec/password_expiration_spec.rb +0 -244
- data/spec/password_grace_period_spec.rb +0 -93
- data/spec/remember_spec.rb +0 -451
- data/spec/reset_password_spec.rb +0 -229
- data/spec/rodauth_spec.rb +0 -343
- data/spec/session_expiration_spec.rb +0 -58
- data/spec/single_session_spec.rb +0 -127
- data/spec/spec_helper.rb +0 -327
- data/spec/two_factor_spec.rb +0 -1462
- data/spec/update_password_hash_spec.rb +0 -40
- data/spec/verify_account_grace_period_spec.rb +0 -171
- data/spec/verify_account_spec.rb +0 -240
- data/spec/verify_change_login_spec.rb +0 -46
- data/spec/verify_login_change_spec.rb +0 -232
- data/spec/views/layout-other.str +0 -11
- data/spec/views/layout.str +0 -11
- data/spec/views/login.str +0 -21
|
@@ -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,
|
|
@@ -71,6 +70,7 @@ module Rodauth
|
|
|
71
70
|
end
|
|
72
71
|
|
|
73
72
|
r.post do
|
|
73
|
+
verified = false
|
|
74
74
|
if account_from_login(param(login_param)) && allow_resending_verify_account_email?
|
|
75
75
|
if verify_account_email_recently_sent?
|
|
76
76
|
set_redirect_error_flash verify_account_email_recently_sent_error_flash
|
|
@@ -80,8 +80,11 @@ module Rodauth
|
|
|
80
80
|
before_verify_account_email_resend
|
|
81
81
|
if verify_account_email_resend
|
|
82
82
|
after_verify_account_email_resend
|
|
83
|
+
verified = true
|
|
83
84
|
end
|
|
85
|
+
end
|
|
84
86
|
|
|
87
|
+
if verified
|
|
85
88
|
set_notice_flash verify_account_email_sent_notice_flash
|
|
86
89
|
else
|
|
87
90
|
set_redirect_error_status(no_matching_login_error_status)
|
|
@@ -95,10 +98,11 @@ module Rodauth
|
|
|
95
98
|
route do |r|
|
|
96
99
|
verify_account_check_already_logged_in
|
|
97
100
|
before_verify_account_route
|
|
101
|
+
@password_field_autocomplete_value = 'new-password'
|
|
98
102
|
|
|
99
103
|
r.get do
|
|
100
104
|
if key = param_or_nil(verify_account_key_param)
|
|
101
|
-
|
|
105
|
+
set_session_value(verify_account_session_key, key)
|
|
102
106
|
redirect(r.path)
|
|
103
107
|
end
|
|
104
108
|
|
|
@@ -106,7 +110,7 @@ module Rodauth
|
|
|
106
110
|
if account_from_verify_account_key(key)
|
|
107
111
|
verify_account_view
|
|
108
112
|
else
|
|
109
|
-
|
|
113
|
+
remove_session_value(verify_account_session_key)
|
|
110
114
|
set_redirect_error_flash no_matching_verify_account_key_error_flash
|
|
111
115
|
redirect require_login_redirect
|
|
112
116
|
end
|
|
@@ -145,10 +149,10 @@ module Rodauth
|
|
|
145
149
|
end
|
|
146
150
|
|
|
147
151
|
if verify_account_autologin?
|
|
148
|
-
|
|
152
|
+
autologin_session('verify_account')
|
|
149
153
|
end
|
|
150
154
|
|
|
151
|
-
|
|
155
|
+
remove_session_value(verify_account_session_key)
|
|
152
156
|
set_notice_flash verify_account_notice_flash
|
|
153
157
|
redirect verify_account_redirect
|
|
154
158
|
end
|
|
@@ -158,6 +162,10 @@ module Rodauth
|
|
|
158
162
|
end
|
|
159
163
|
end
|
|
160
164
|
|
|
165
|
+
def require_login_confirmation?
|
|
166
|
+
false
|
|
167
|
+
end
|
|
168
|
+
|
|
161
169
|
def allow_resending_verify_account_email?
|
|
162
170
|
account[account_status_column] == account_unverified_status_value
|
|
163
171
|
end
|
|
@@ -201,7 +209,7 @@ module Rodauth
|
|
|
201
209
|
end
|
|
202
210
|
|
|
203
211
|
def send_verify_account_email
|
|
204
|
-
create_verify_account_email
|
|
212
|
+
send_email(create_verify_account_email)
|
|
205
213
|
end
|
|
206
214
|
|
|
207
215
|
def verify_account_email_link
|
|
@@ -220,14 +228,6 @@ module Rodauth
|
|
|
220
228
|
false
|
|
221
229
|
end
|
|
222
230
|
|
|
223
|
-
def login_form_footer
|
|
224
|
-
super + verify_account_resend_link
|
|
225
|
-
end
|
|
226
|
-
|
|
227
|
-
def verify_account_resend_link
|
|
228
|
-
"<p><a href=\"#{prefix}/#{verify_account_resend_route}\">Resend Verify Account Information</a></p>"
|
|
229
|
-
end
|
|
230
|
-
|
|
231
231
|
def create_account_set_password?
|
|
232
232
|
return false if verify_account_set_password?
|
|
233
233
|
super
|
|
@@ -247,6 +247,14 @@ module Rodauth
|
|
|
247
247
|
|
|
248
248
|
private
|
|
249
249
|
|
|
250
|
+
def _login_form_footer_links
|
|
251
|
+
links = super
|
|
252
|
+
if !param_or_nil(login_param) || ((account || account_from_login(param(login_param))) && allow_resending_verify_account_email?)
|
|
253
|
+
links << [30, verify_account_resend_path, verify_account_resend_link_text]
|
|
254
|
+
end
|
|
255
|
+
links
|
|
256
|
+
end
|
|
257
|
+
|
|
250
258
|
def verify_account_email_recently_sent?
|
|
251
259
|
(email_last_sent = get_verify_account_email_last_sent) && (Time.now - email_last_sent < verify_account_skip_resend_email_within)
|
|
252
260
|
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
|
|
@@ -23,7 +23,18 @@ module Rodauth
|
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
def open_account?
|
|
26
|
-
super || account_in_unverified_grace_period?
|
|
26
|
+
super || (account_in_unverified_grace_period? && has_password?)
|
|
27
|
+
end
|
|
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
|
|
27
38
|
end
|
|
28
39
|
|
|
29
40
|
private
|
|
@@ -41,6 +52,10 @@ module Rodauth
|
|
|
41
52
|
super if defined?(super)
|
|
42
53
|
end
|
|
43
54
|
|
|
55
|
+
def allow_email_auth?
|
|
56
|
+
(defined?(super) ? super : true) && !account_in_unverified_grace_period?
|
|
57
|
+
end
|
|
58
|
+
|
|
44
59
|
def verify_account_check_already_logged_in
|
|
45
60
|
nil
|
|
46
61
|
end
|
|
@@ -56,14 +71,8 @@ module Rodauth
|
|
|
56
71
|
s
|
|
57
72
|
end
|
|
58
73
|
|
|
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
74
|
def account_in_unverified_grace_period?
|
|
75
|
+
account || account_from_session
|
|
67
76
|
account[account_status_column] == account_unverified_status_value &&
|
|
68
77
|
verify_account_grace_period &&
|
|
69
78
|
!verify_account_ds.where(Sequel.date_add(verification_requested_at_column, :seconds=>verify_account_grace_period) > Sequel::CURRENT_TIMESTAMP).empty?
|
|
@@ -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
|
|
@@ -118,7 +119,7 @@ module Rodauth
|
|
|
118
119
|
end
|
|
119
120
|
|
|
120
121
|
def send_verify_login_change_email(login)
|
|
121
|
-
create_verify_login_change_email(login)
|
|
122
|
+
send_email(create_verify_login_change_email(login))
|
|
122
123
|
end
|
|
123
124
|
|
|
124
125
|
def verify_login_change_email_link
|
|
@@ -0,0 +1,505 @@
|
|
|
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
|
+
webauthn_keys_ds.where(webauthn_keys_webauthn_id_column=>webauthn_id).delete == 1
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def remove_all_webauthn_keys_and_user_ids
|
|
384
|
+
webauthn_user_ids_ds.delete
|
|
385
|
+
webauthn_keys_ds.delete
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def webauthn_setup?
|
|
389
|
+
!webauthn_keys_ds.empty?
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
def require_webauthn_setup
|
|
393
|
+
unless webauthn_setup?
|
|
394
|
+
set_redirect_error_status(webauthn_not_setup_error_status)
|
|
395
|
+
set_redirect_error_flash webauthn_not_setup_error_flash
|
|
396
|
+
redirect two_factor_need_setup_redirect
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
def two_factor_remove
|
|
401
|
+
super
|
|
402
|
+
remove_all_webauthn_keys_and_user_ids
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
def possible_authentication_methods
|
|
406
|
+
methods = super
|
|
407
|
+
methods << 'webauthn' if webauthn_setup?
|
|
408
|
+
methods
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
private
|
|
412
|
+
|
|
413
|
+
def _two_factor_auth_links
|
|
414
|
+
links = super
|
|
415
|
+
links << [10, webauthn_auth_path, webauthn_auth_link_text] if webauthn_setup? && !two_factor_login_type_match?('webauthn')
|
|
416
|
+
links
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
def _two_factor_setup_links
|
|
420
|
+
super << [10, webauthn_setup_path, webauthn_setup_link_text]
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
def _two_factor_remove_links
|
|
424
|
+
links = super
|
|
425
|
+
links << [10, webauthn_remove_path, webauthn_remove_link_text] if webauthn_setup?
|
|
426
|
+
links
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
def _two_factor_remove_all_from_session
|
|
430
|
+
two_factor_remove_session('webauthn')
|
|
431
|
+
remove_session_value(authenticated_webauthn_id_session_key)
|
|
432
|
+
super
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
def webauthn_account_id
|
|
436
|
+
session_value
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
def webauthn_user_ids_ds
|
|
440
|
+
db[webauthn_user_ids_table].where(webauthn_user_ids_account_id_column => webauthn_account_id)
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
def webauthn_keys_ds
|
|
444
|
+
db[webauthn_keys_table].where(webauthn_keys_account_id_column => webauthn_account_id)
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
def webauthn_auth_credential_from_form_submission
|
|
448
|
+
case auth_data = raw_param(webauthn_auth_param)
|
|
449
|
+
when String
|
|
450
|
+
begin
|
|
451
|
+
auth_data = JSON.parse(auth_data)
|
|
452
|
+
rescue
|
|
453
|
+
throw_error_status(invalid_field_error_status, webauthn_auth_param, webauthn_invalid_auth_param_message)
|
|
454
|
+
end
|
|
455
|
+
when Hash
|
|
456
|
+
# nothing
|
|
457
|
+
else
|
|
458
|
+
throw_error_status(invalid_field_error_status, webauthn_auth_param, webauthn_invalid_auth_param_message)
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
begin
|
|
462
|
+
webauthn_credential = WebAuthn::Credential.from_get(auth_data)
|
|
463
|
+
unless valid_webauthn_credential_auth?(webauthn_credential)
|
|
464
|
+
throw_error_status(invalid_key_error_status, webauthn_auth_param, webauthn_invalid_auth_param_message)
|
|
465
|
+
end
|
|
466
|
+
rescue WebAuthn::SignCountVerificationError
|
|
467
|
+
handle_webauthn_sign_count_verification_error
|
|
468
|
+
rescue WebAuthn::Error, RuntimeError, NoMethodError
|
|
469
|
+
throw_error_status(invalid_field_error_status, webauthn_auth_param, webauthn_invalid_auth_param_message)
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
webauthn_credential
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
def webauthn_setup_credential_from_form_submission
|
|
476
|
+
case setup_data = raw_param(webauthn_setup_param)
|
|
477
|
+
when String
|
|
478
|
+
begin
|
|
479
|
+
setup_data = JSON.parse(setup_data)
|
|
480
|
+
rescue
|
|
481
|
+
throw_error_status(invalid_field_error_status, webauthn_setup_param, webauthn_invalid_setup_param_message)
|
|
482
|
+
end
|
|
483
|
+
when Hash
|
|
484
|
+
# nothing
|
|
485
|
+
else
|
|
486
|
+
throw_error_status(invalid_field_error_status, webauthn_setup_param, webauthn_invalid_setup_param_message)
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
unless two_factor_password_match?(param(password_param))
|
|
490
|
+
throw_error_status(invalid_password_error_status, password_param, invalid_password_message)
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
begin
|
|
494
|
+
webauthn_credential = WebAuthn::Credential.from_create(setup_data)
|
|
495
|
+
unless valid_new_webauthn_credential?(webauthn_credential)
|
|
496
|
+
throw_error_status(invalid_field_error_status, webauthn_setup_param, webauthn_invalid_setup_param_message)
|
|
497
|
+
end
|
|
498
|
+
rescue WebAuthn::Error, RuntimeError, NoMethodError
|
|
499
|
+
throw_error_status(invalid_field_error_status, webauthn_setup_param, webauthn_invalid_setup_param_message)
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
webauthn_credential
|
|
503
|
+
end
|
|
504
|
+
end
|
|
505
|
+
end
|