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.
Files changed (130) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +132 -0
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +207 -79
  5. data/doc/account_expiration.rdoc +12 -26
  6. data/doc/active_sessions.rdoc +49 -0
  7. data/doc/audit_logging.rdoc +44 -0
  8. data/doc/base.rdoc +74 -128
  9. data/doc/change_login.rdoc +7 -14
  10. data/doc/change_password.rdoc +9 -13
  11. data/doc/change_password_notify.rdoc +2 -2
  12. data/doc/close_account.rdoc +9 -16
  13. data/doc/confirm_password.rdoc +12 -5
  14. data/doc/create_account.rdoc +11 -22
  15. data/doc/disallow_password_reuse.rdoc +6 -13
  16. data/doc/email_auth.rdoc +15 -14
  17. data/doc/email_base.rdoc +5 -15
  18. data/doc/http_basic_auth.rdoc +10 -1
  19. data/doc/jwt.rdoc +22 -22
  20. data/doc/jwt_cors.rdoc +2 -3
  21. data/doc/jwt_refresh.rdoc +12 -8
  22. data/doc/lockout.rdoc +17 -15
  23. data/doc/login.rdoc +10 -2
  24. data/doc/login_password_requirements_base.rdoc +15 -37
  25. data/doc/logout.rdoc +2 -2
  26. data/doc/otp.rdoc +24 -19
  27. data/doc/password_complexity.rdoc +10 -26
  28. data/doc/password_expiration.rdoc +11 -25
  29. data/doc/password_grace_period.rdoc +16 -2
  30. data/doc/recovery_codes.rdoc +18 -12
  31. data/doc/release_notes/2.0.0.txt +361 -0
  32. data/doc/remember.rdoc +40 -64
  33. data/doc/reset_password.rdoc +12 -9
  34. data/doc/session_expiration.rdoc +1 -0
  35. data/doc/single_session.rdoc +16 -25
  36. data/doc/sms_codes.rdoc +24 -14
  37. data/doc/two_factor_base.rdoc +60 -22
  38. data/doc/verify_account.rdoc +14 -12
  39. data/doc/verify_account_grace_period.rdoc +6 -2
  40. data/doc/verify_login_change.rdoc +9 -8
  41. data/doc/webauthn.rdoc +115 -0
  42. data/doc/webauthn_login.rdoc +15 -0
  43. data/doc/webauthn_verify_account.rdoc +9 -0
  44. data/javascript/webauthn_auth.js +45 -0
  45. data/javascript/webauthn_setup.js +35 -0
  46. data/lib/roda/plugins/rodauth.rb +1 -1
  47. data/lib/rodauth.rb +29 -24
  48. data/lib/rodauth/features/account_expiration.rb +5 -5
  49. data/lib/rodauth/features/active_sessions.rb +160 -0
  50. data/lib/rodauth/features/audit_logging.rb +96 -0
  51. data/lib/rodauth/features/base.rb +131 -47
  52. data/lib/rodauth/features/change_password_notify.rb +1 -1
  53. data/lib/rodauth/features/confirm_password.rb +40 -2
  54. data/lib/rodauth/features/create_account.rb +7 -13
  55. data/lib/rodauth/features/disallow_common_passwords.rb +1 -1
  56. data/lib/rodauth/features/disallow_password_reuse.rb +1 -1
  57. data/lib/rodauth/features/email_auth.rb +29 -27
  58. data/lib/rodauth/features/email_base.rb +3 -3
  59. data/lib/rodauth/features/http_basic_auth.rb +44 -37
  60. data/lib/rodauth/features/jwt.rb +51 -8
  61. data/lib/rodauth/features/jwt_refresh.rb +3 -3
  62. data/lib/rodauth/features/lockout.rb +11 -13
  63. data/lib/rodauth/features/login.rb +48 -8
  64. data/lib/rodauth/features/login_password_requirements_base.rb +4 -4
  65. data/lib/rodauth/features/otp.rb +71 -81
  66. data/lib/rodauth/features/password_complexity.rb +4 -11
  67. data/lib/rodauth/features/password_expiration.rb +1 -1
  68. data/lib/rodauth/features/password_grace_period.rb +17 -10
  69. data/lib/rodauth/features/recovery_codes.rb +47 -51
  70. data/lib/rodauth/features/remember.rb +11 -27
  71. data/lib/rodauth/features/reset_password.rb +25 -25
  72. data/lib/rodauth/features/session_expiration.rb +6 -4
  73. data/lib/rodauth/features/single_session.rb +7 -5
  74. data/lib/rodauth/features/sms_codes.rb +58 -67
  75. data/lib/rodauth/features/two_factor_base.rb +132 -28
  76. data/lib/rodauth/features/verify_account.rb +23 -20
  77. data/lib/rodauth/features/verify_account_grace_period.rb +19 -8
  78. data/lib/rodauth/features/verify_login_change.rb +11 -10
  79. data/lib/rodauth/features/webauthn.rb +507 -0
  80. data/lib/rodauth/features/webauthn_login.rb +70 -0
  81. data/lib/rodauth/features/webauthn_verify_account.rb +46 -0
  82. data/lib/rodauth/version.rb +2 -2
  83. data/templates/button.str +1 -3
  84. data/templates/change-login.str +1 -2
  85. data/templates/change-password.str +3 -5
  86. data/templates/close-account.str +2 -2
  87. data/templates/confirm-password.str +1 -1
  88. data/templates/create-account.str +1 -1
  89. data/templates/email-auth-request-form.str +1 -2
  90. data/templates/email-auth.str +1 -1
  91. data/templates/global-logout-field.str +6 -0
  92. data/templates/login-confirm-field.str +2 -4
  93. data/templates/login-display.str +3 -2
  94. data/templates/login-field.str +2 -4
  95. data/templates/login-form-footer.str +6 -0
  96. data/templates/login-form.str +7 -0
  97. data/templates/login.str +1 -9
  98. data/templates/logout.str +1 -1
  99. data/templates/multi-phase-login.str +3 -0
  100. data/templates/otp-auth-code-field.str +5 -3
  101. data/templates/otp-auth.str +1 -1
  102. data/templates/otp-disable.str +1 -1
  103. data/templates/otp-setup.str +3 -3
  104. data/templates/password-confirm-field.str +2 -4
  105. data/templates/password-field.str +2 -4
  106. data/templates/recovery-auth.str +3 -6
  107. data/templates/recovery-codes.str +1 -1
  108. data/templates/remember.str +15 -20
  109. data/templates/reset-password-request.str +2 -2
  110. data/templates/reset-password.str +1 -2
  111. data/templates/sms-auth.str +1 -1
  112. data/templates/sms-code-field.str +5 -3
  113. data/templates/sms-confirm.str +1 -2
  114. data/templates/sms-disable.str +1 -2
  115. data/templates/sms-request.str +1 -1
  116. data/templates/sms-setup.str +6 -4
  117. data/templates/two-factor-auth.str +5 -0
  118. data/templates/two-factor-disable.str +6 -0
  119. data/templates/two-factor-manage.str +16 -0
  120. data/templates/unlock-account-request.str +2 -2
  121. data/templates/unlock-account.str +1 -1
  122. data/templates/verify-account-resend.str +1 -1
  123. data/templates/verify-account.str +1 -2
  124. data/templates/verify-login-change.str +1 -1
  125. data/templates/webauthn-auth.str +11 -0
  126. data/templates/webauthn-remove.str +14 -0
  127. data/templates/webauthn-setup.str +12 -0
  128. metadata +64 -11
  129. data/doc/verify_change_login.rdoc +0 -11
  130. 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
- auth_value_method :verify_account_email_subject, 'Verify Account'
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, nil
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
- auth_value_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>"
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?, false
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
- session[verify_account_session_key] = key
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
- session[verify_account_session_key] = nil
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
- update_session
147
+ autologin_session('verify_account')
149
148
  end
150
149
 
151
- session[verify_account_session_key] = nil
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 "Cannot change login for unverified account. Please verify this account before changing the login.", "unverified_change_login"
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", :verify_login_change_duplicate_account
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
- auth_value_method :verify_login_change_email_subject, 'Verify Login Change'
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
- session[verify_login_change_session_key] = key
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
- session[verify_login_change_session_key] = nil
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
- update_session
92
+ autologin_session('verify_login_change')
96
93
  end
97
94
 
98
- session[verify_login_change_session_key] = nil
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