rodauth 1.21.0 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (200) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +182 -0
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +211 -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 +75 -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 +6 -15
  18. data/doc/guides/admin_activation.rdoc +46 -0
  19. data/doc/guides/already_authenticated.rdoc +10 -0
  20. data/doc/guides/alternative_login.rdoc +46 -0
  21. data/doc/guides/create_account_programmatically.rdoc +38 -0
  22. data/doc/guides/delay_password.rdoc +25 -0
  23. data/doc/guides/email_only.rdoc +16 -0
  24. data/doc/guides/i18n.rdoc +26 -0
  25. data/doc/{internals.rdoc → guides/internals.rdoc} +0 -0
  26. data/doc/guides/links.rdoc +12 -0
  27. data/doc/guides/login_return.rdoc +37 -0
  28. data/doc/guides/password_column.rdoc +25 -0
  29. data/doc/guides/password_confirmation.rdoc +37 -0
  30. data/doc/guides/password_requirements.rdoc +30 -0
  31. data/doc/guides/paths.rdoc +36 -0
  32. data/doc/guides/query_params.rdoc +9 -0
  33. data/doc/guides/redirects.rdoc +17 -0
  34. data/doc/guides/registration_field.rdoc +68 -0
  35. data/doc/guides/require_mfa.rdoc +30 -0
  36. data/doc/guides/reset_password_autologin.rdoc +21 -0
  37. data/doc/guides/status_column.rdoc +28 -0
  38. data/doc/guides/totp_or_recovery.rdoc +16 -0
  39. data/doc/http_basic_auth.rdoc +10 -1
  40. data/doc/jwt.rdoc +22 -22
  41. data/doc/jwt_cors.rdoc +22 -0
  42. data/doc/jwt_refresh.rdoc +18 -8
  43. data/doc/lockout.rdoc +17 -15
  44. data/doc/login.rdoc +10 -2
  45. data/doc/login_password_requirements_base.rdoc +15 -37
  46. data/doc/logout.rdoc +2 -2
  47. data/doc/otp.rdoc +25 -19
  48. data/doc/password_complexity.rdoc +10 -26
  49. data/doc/password_expiration.rdoc +11 -25
  50. data/doc/password_grace_period.rdoc +16 -2
  51. data/doc/recovery_codes.rdoc +18 -12
  52. data/doc/release_notes/1.22.0.txt +11 -0
  53. data/doc/release_notes/1.23.0.txt +32 -0
  54. data/doc/release_notes/2.0.0.txt +361 -0
  55. data/doc/release_notes/2.1.0.txt +31 -0
  56. data/doc/release_notes/2.2.0.txt +39 -0
  57. data/doc/remember.rdoc +40 -64
  58. data/doc/reset_password.rdoc +12 -9
  59. data/doc/session_expiration.rdoc +1 -0
  60. data/doc/single_session.rdoc +16 -25
  61. data/doc/sms_codes.rdoc +24 -14
  62. data/doc/two_factor_base.rdoc +60 -22
  63. data/doc/verify_account.rdoc +14 -12
  64. data/doc/verify_account_grace_period.rdoc +6 -2
  65. data/doc/verify_login_change.rdoc +9 -8
  66. data/doc/webauthn.rdoc +115 -0
  67. data/doc/webauthn_login.rdoc +15 -0
  68. data/doc/webauthn_verify_account.rdoc +9 -0
  69. data/javascript/webauthn_auth.js +45 -0
  70. data/javascript/webauthn_setup.js +35 -0
  71. data/lib/roda/plugins/rodauth.rb +1 -1
  72. data/lib/rodauth.rb +36 -28
  73. data/lib/rodauth/features/account_expiration.rb +5 -5
  74. data/lib/rodauth/features/active_sessions.rb +158 -0
  75. data/lib/rodauth/features/audit_logging.rb +98 -0
  76. data/lib/rodauth/features/base.rb +144 -43
  77. data/lib/rodauth/features/change_password_notify.rb +2 -2
  78. data/lib/rodauth/features/confirm_password.rb +40 -2
  79. data/lib/rodauth/features/create_account.rb +8 -13
  80. data/lib/rodauth/features/disallow_common_passwords.rb +1 -1
  81. data/lib/rodauth/features/disallow_password_reuse.rb +1 -1
  82. data/lib/rodauth/features/email_auth.rb +31 -30
  83. data/lib/rodauth/features/email_base.rb +9 -4
  84. data/lib/rodauth/features/http_basic_auth.rb +55 -35
  85. data/lib/rodauth/features/jwt.rb +63 -16
  86. data/lib/rodauth/features/jwt_cors.rb +53 -0
  87. data/lib/rodauth/features/jwt_refresh.rb +32 -9
  88. data/lib/rodauth/features/lockout.rb +12 -14
  89. data/lib/rodauth/features/login.rb +54 -10
  90. data/lib/rodauth/features/login_password_requirements_base.rb +4 -4
  91. data/lib/rodauth/features/otp.rb +77 -80
  92. data/lib/rodauth/features/password_complexity.rb +8 -13
  93. data/lib/rodauth/features/password_expiration.rb +2 -2
  94. data/lib/rodauth/features/password_grace_period.rb +17 -10
  95. data/lib/rodauth/features/recovery_codes.rb +49 -53
  96. data/lib/rodauth/features/remember.rb +11 -27
  97. data/lib/rodauth/features/reset_password.rb +26 -26
  98. data/lib/rodauth/features/session_expiration.rb +6 -4
  99. data/lib/rodauth/features/single_session.rb +8 -6
  100. data/lib/rodauth/features/sms_codes.rb +62 -72
  101. data/lib/rodauth/features/two_factor_base.rb +134 -30
  102. data/lib/rodauth/features/verify_account.rb +29 -21
  103. data/lib/rodauth/features/verify_account_grace_period.rb +18 -9
  104. data/lib/rodauth/features/verify_login_change.rb +12 -11
  105. data/lib/rodauth/features/webauthn.rb +505 -0
  106. data/lib/rodauth/features/webauthn_login.rb +70 -0
  107. data/lib/rodauth/features/webauthn_verify_account.rb +46 -0
  108. data/lib/rodauth/version.rb +2 -2
  109. data/templates/button.str +1 -3
  110. data/templates/change-login.str +1 -2
  111. data/templates/change-password.str +3 -5
  112. data/templates/close-account.str +2 -2
  113. data/templates/confirm-password.str +1 -1
  114. data/templates/create-account.str +1 -1
  115. data/templates/email-auth-email.str +1 -1
  116. data/templates/email-auth-request-form.str +2 -3
  117. data/templates/email-auth.str +1 -1
  118. data/templates/global-logout-field.str +6 -0
  119. data/templates/login-confirm-field.str +2 -4
  120. data/templates/login-display.str +3 -2
  121. data/templates/login-field.str +2 -4
  122. data/templates/login-form-footer.str +6 -0
  123. data/templates/login-form.str +7 -0
  124. data/templates/login.str +1 -9
  125. data/templates/logout.str +1 -1
  126. data/templates/multi-phase-login.str +3 -0
  127. data/templates/otp-auth-code-field.str +5 -3
  128. data/templates/otp-auth.str +1 -1
  129. data/templates/otp-disable.str +1 -1
  130. data/templates/otp-setup.str +3 -3
  131. data/templates/password-confirm-field.str +2 -4
  132. data/templates/password-field.str +2 -4
  133. data/templates/recovery-auth.str +3 -6
  134. data/templates/recovery-codes.str +1 -1
  135. data/templates/remember.str +15 -20
  136. data/templates/reset-password-email.str +1 -1
  137. data/templates/reset-password-request.str +3 -3
  138. data/templates/reset-password.str +1 -2
  139. data/templates/sms-auth.str +1 -1
  140. data/templates/sms-code-field.str +5 -3
  141. data/templates/sms-confirm.str +1 -2
  142. data/templates/sms-disable.str +1 -2
  143. data/templates/sms-request.str +1 -1
  144. data/templates/sms-setup.str +6 -4
  145. data/templates/two-factor-auth.str +5 -0
  146. data/templates/two-factor-disable.str +6 -0
  147. data/templates/two-factor-manage.str +16 -0
  148. data/templates/unlock-account-email.str +1 -1
  149. data/templates/unlock-account-request.str +4 -4
  150. data/templates/unlock-account.str +1 -1
  151. data/templates/verify-account-email.str +1 -1
  152. data/templates/verify-account-resend.str +3 -3
  153. data/templates/verify-account.str +1 -2
  154. data/templates/verify-login-change-email.str +2 -1
  155. data/templates/verify-login-change.str +1 -1
  156. data/templates/webauthn-auth.str +11 -0
  157. data/templates/webauthn-remove.str +14 -0
  158. data/templates/webauthn-setup.str +12 -0
  159. metadata +110 -52
  160. data/Rakefile +0 -179
  161. data/doc/verify_change_login.rdoc +0 -11
  162. data/lib/rodauth/features/verify_change_login.rb +0 -20
  163. data/spec/account_expiration_spec.rb +0 -225
  164. data/spec/all.rb +0 -1
  165. data/spec/change_login_spec.rb +0 -156
  166. data/spec/change_password_notify_spec.rb +0 -33
  167. data/spec/change_password_spec.rb +0 -202
  168. data/spec/close_account_spec.rb +0 -162
  169. data/spec/confirm_password_spec.rb +0 -70
  170. data/spec/create_account_spec.rb +0 -127
  171. data/spec/disallow_common_passwords_spec.rb +0 -93
  172. data/spec/disallow_password_reuse_spec.rb +0 -179
  173. data/spec/email_auth_spec.rb +0 -285
  174. data/spec/http_basic_auth_spec.rb +0 -143
  175. data/spec/jwt_refresh_spec.rb +0 -256
  176. data/spec/jwt_spec.rb +0 -235
  177. data/spec/lockout_spec.rb +0 -250
  178. data/spec/login_spec.rb +0 -328
  179. data/spec/migrate/001_tables.rb +0 -184
  180. data/spec/migrate/002_account_password_hash_column.rb +0 -11
  181. data/spec/migrate_password/001_tables.rb +0 -73
  182. data/spec/migrate_travis/001_tables.rb +0 -141
  183. data/spec/password_complexity_spec.rb +0 -109
  184. data/spec/password_expiration_spec.rb +0 -244
  185. data/spec/password_grace_period_spec.rb +0 -93
  186. data/spec/remember_spec.rb +0 -451
  187. data/spec/reset_password_spec.rb +0 -229
  188. data/spec/rodauth_spec.rb +0 -343
  189. data/spec/session_expiration_spec.rb +0 -58
  190. data/spec/single_session_spec.rb +0 -127
  191. data/spec/spec_helper.rb +0 -327
  192. data/spec/two_factor_spec.rb +0 -1462
  193. data/spec/update_password_hash_spec.rb +0 -40
  194. data/spec/verify_account_grace_period_spec.rb +0 -171
  195. data/spec/verify_account_spec.rb +0 -240
  196. data/spec/verify_change_login_spec.rb +0 -46
  197. data/spec/verify_login_change_spec.rb +0 -232
  198. data/spec/views/layout-other.str +0 -11
  199. data/spec/views/layout.str +0 -11
  200. data/spec/views/login.str +0 -21
@@ -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
@@ -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).deliver!
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