rodauth 1.20.0 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (180) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +170 -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/http_basic_auth.rdoc +10 -1
  19. data/doc/internals.rdoc +1 -1
  20. data/doc/jwt.rdoc +22 -22
  21. data/doc/jwt_cors.rdoc +22 -0
  22. data/doc/jwt_refresh.rdoc +12 -8
  23. data/doc/lockout.rdoc +17 -15
  24. data/doc/login.rdoc +10 -2
  25. data/doc/login_password_requirements_base.rdoc +15 -37
  26. data/doc/logout.rdoc +2 -2
  27. data/doc/otp.rdoc +24 -19
  28. data/doc/password_complexity.rdoc +10 -26
  29. data/doc/password_expiration.rdoc +11 -25
  30. data/doc/password_grace_period.rdoc +16 -2
  31. data/doc/recovery_codes.rdoc +18 -12
  32. data/doc/release_notes/1.21.0.txt +12 -0
  33. data/doc/release_notes/1.22.0.txt +11 -0
  34. data/doc/release_notes/1.23.0.txt +32 -0
  35. data/doc/release_notes/2.0.0.txt +361 -0
  36. data/doc/release_notes/2.1.0.txt +31 -0
  37. data/doc/remember.rdoc +40 -64
  38. data/doc/reset_password.rdoc +12 -9
  39. data/doc/session_expiration.rdoc +1 -0
  40. data/doc/single_session.rdoc +16 -25
  41. data/doc/sms_codes.rdoc +24 -14
  42. data/doc/two_factor_base.rdoc +60 -22
  43. data/doc/verify_account.rdoc +14 -12
  44. data/doc/verify_account_grace_period.rdoc +6 -2
  45. data/doc/verify_login_change.rdoc +9 -8
  46. data/doc/webauthn.rdoc +115 -0
  47. data/doc/webauthn_login.rdoc +15 -0
  48. data/doc/webauthn_verify_account.rdoc +9 -0
  49. data/javascript/webauthn_auth.js +45 -0
  50. data/javascript/webauthn_setup.js +35 -0
  51. data/lib/roda/plugins/rodauth.rb +1 -1
  52. data/lib/rodauth.rb +32 -24
  53. data/lib/rodauth/features/account_expiration.rb +5 -5
  54. data/lib/rodauth/features/active_sessions.rb +160 -0
  55. data/lib/rodauth/features/audit_logging.rb +96 -0
  56. data/lib/rodauth/features/base.rb +144 -43
  57. data/lib/rodauth/features/change_password_notify.rb +2 -2
  58. data/lib/rodauth/features/confirm_password.rb +40 -2
  59. data/lib/rodauth/features/create_account.rb +8 -13
  60. data/lib/rodauth/features/disallow_common_passwords.rb +1 -1
  61. data/lib/rodauth/features/disallow_password_reuse.rb +1 -1
  62. data/lib/rodauth/features/email_auth.rb +30 -29
  63. data/lib/rodauth/features/email_base.rb +9 -4
  64. data/lib/rodauth/features/http_basic_auth.rb +55 -35
  65. data/lib/rodauth/features/jwt.rb +58 -10
  66. data/lib/rodauth/features/jwt_cors.rb +53 -0
  67. data/lib/rodauth/features/jwt_refresh.rb +3 -3
  68. data/lib/rodauth/features/lockout.rb +12 -14
  69. data/lib/rodauth/features/login.rb +54 -10
  70. data/lib/rodauth/features/login_password_requirements_base.rb +4 -4
  71. data/lib/rodauth/features/otp.rb +72 -74
  72. data/lib/rodauth/features/password_complexity.rb +4 -11
  73. data/lib/rodauth/features/password_expiration.rb +2 -2
  74. data/lib/rodauth/features/password_grace_period.rb +17 -10
  75. data/lib/rodauth/features/recovery_codes.rb +49 -53
  76. data/lib/rodauth/features/remember.rb +11 -27
  77. data/lib/rodauth/features/reset_password.rb +26 -26
  78. data/lib/rodauth/features/session_expiration.rb +6 -4
  79. data/lib/rodauth/features/single_session.rb +7 -5
  80. data/lib/rodauth/features/sms_codes.rb +62 -71
  81. data/lib/rodauth/features/two_factor_base.rb +132 -28
  82. data/lib/rodauth/features/verify_account.rb +25 -21
  83. data/lib/rodauth/features/verify_account_grace_period.rb +20 -9
  84. data/lib/rodauth/features/verify_login_change.rb +12 -11
  85. data/lib/rodauth/features/webauthn.rb +507 -0
  86. data/lib/rodauth/features/webauthn_login.rb +70 -0
  87. data/lib/rodauth/features/webauthn_verify_account.rb +46 -0
  88. data/lib/rodauth/version.rb +2 -2
  89. data/templates/button.str +1 -3
  90. data/templates/change-login.str +1 -2
  91. data/templates/change-password.str +3 -5
  92. data/templates/close-account.str +2 -2
  93. data/templates/confirm-password.str +1 -1
  94. data/templates/create-account.str +1 -1
  95. data/templates/email-auth-email.str +1 -1
  96. data/templates/email-auth-request-form.str +2 -3
  97. data/templates/email-auth.str +1 -1
  98. data/templates/global-logout-field.str +6 -0
  99. data/templates/login-confirm-field.str +2 -4
  100. data/templates/login-display.str +3 -2
  101. data/templates/login-field.str +2 -4
  102. data/templates/login-form-footer.str +6 -0
  103. data/templates/login-form.str +7 -0
  104. data/templates/login.str +1 -9
  105. data/templates/logout.str +1 -1
  106. data/templates/multi-phase-login.str +3 -0
  107. data/templates/otp-auth-code-field.str +5 -3
  108. data/templates/otp-auth.str +1 -1
  109. data/templates/otp-disable.str +1 -1
  110. data/templates/otp-setup.str +3 -3
  111. data/templates/password-confirm-field.str +2 -4
  112. data/templates/password-field.str +2 -4
  113. data/templates/recovery-auth.str +3 -6
  114. data/templates/recovery-codes.str +1 -1
  115. data/templates/remember.str +15 -20
  116. data/templates/reset-password-email.str +1 -1
  117. data/templates/reset-password-request.str +3 -3
  118. data/templates/reset-password.str +1 -2
  119. data/templates/sms-auth.str +1 -1
  120. data/templates/sms-code-field.str +5 -3
  121. data/templates/sms-confirm.str +1 -2
  122. data/templates/sms-disable.str +1 -2
  123. data/templates/sms-request.str +1 -1
  124. data/templates/sms-setup.str +6 -4
  125. data/templates/two-factor-auth.str +5 -0
  126. data/templates/two-factor-disable.str +6 -0
  127. data/templates/two-factor-manage.str +16 -0
  128. data/templates/unlock-account-email.str +1 -1
  129. data/templates/unlock-account-request.str +4 -4
  130. data/templates/unlock-account.str +1 -1
  131. data/templates/verify-account-email.str +1 -1
  132. data/templates/verify-account-resend.str +3 -3
  133. data/templates/verify-account.str +1 -2
  134. data/templates/verify-login-change-email.str +2 -1
  135. data/templates/verify-login-change.str +1 -1
  136. data/templates/webauthn-auth.str +11 -0
  137. data/templates/webauthn-remove.str +14 -0
  138. data/templates/webauthn-setup.str +12 -0
  139. metadata +89 -50
  140. data/Rakefile +0 -179
  141. data/doc/verify_change_login.rdoc +0 -11
  142. data/lib/rodauth/features/verify_change_login.rb +0 -20
  143. data/spec/account_expiration_spec.rb +0 -225
  144. data/spec/all.rb +0 -1
  145. data/spec/change_login_spec.rb +0 -156
  146. data/spec/change_password_notify_spec.rb +0 -33
  147. data/spec/change_password_spec.rb +0 -202
  148. data/spec/close_account_spec.rb +0 -162
  149. data/spec/confirm_password_spec.rb +0 -70
  150. data/spec/create_account_spec.rb +0 -127
  151. data/spec/disallow_common_passwords_spec.rb +0 -93
  152. data/spec/disallow_password_reuse_spec.rb +0 -179
  153. data/spec/email_auth_spec.rb +0 -285
  154. data/spec/http_basic_auth_spec.rb +0 -143
  155. data/spec/jwt_refresh_spec.rb +0 -256
  156. data/spec/jwt_spec.rb +0 -235
  157. data/spec/lockout_spec.rb +0 -250
  158. data/spec/login_spec.rb +0 -328
  159. data/spec/migrate/001_tables.rb +0 -184
  160. data/spec/migrate/002_account_password_hash_column.rb +0 -11
  161. data/spec/migrate_password/001_tables.rb +0 -73
  162. data/spec/migrate_travis/001_tables.rb +0 -141
  163. data/spec/password_complexity_spec.rb +0 -109
  164. data/spec/password_expiration_spec.rb +0 -244
  165. data/spec/password_grace_period_spec.rb +0 -93
  166. data/spec/remember_spec.rb +0 -451
  167. data/spec/reset_password_spec.rb +0 -229
  168. data/spec/rodauth_spec.rb +0 -343
  169. data/spec/session_expiration_spec.rb +0 -58
  170. data/spec/single_session_spec.rb +0 -127
  171. data/spec/spec_helper.rb +0 -327
  172. data/spec/two_factor_spec.rb +0 -1423
  173. data/spec/update_password_hash_spec.rb +0 -40
  174. data/spec/verify_account_grace_period_spec.rb +0 -171
  175. data/spec/verify_account_spec.rb +0 -240
  176. data/spec/verify_change_login_spec.rb +0 -46
  177. data/spec/verify_login_change_spec.rb +0 -232
  178. data/spec/views/layout-other.str +0 -11
  179. data/spec/views/layout.str +0 -11
  180. 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
- 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,
@@ -95,10 +94,11 @@ module Rodauth
95
94
  route do |r|
96
95
  verify_account_check_already_logged_in
97
96
  before_verify_account_route
97
+ @password_field_autocomplete_value = 'new-password'
98
98
 
99
99
  r.get do
100
100
  if key = param_or_nil(verify_account_key_param)
101
- session[verify_account_session_key] = key
101
+ set_session_value(verify_account_session_key, key)
102
102
  redirect(r.path)
103
103
  end
104
104
 
@@ -106,7 +106,7 @@ module Rodauth
106
106
  if account_from_verify_account_key(key)
107
107
  verify_account_view
108
108
  else
109
- session[verify_account_session_key] = nil
109
+ remove_session_value(verify_account_session_key)
110
110
  set_redirect_error_flash no_matching_verify_account_key_error_flash
111
111
  redirect require_login_redirect
112
112
  end
@@ -145,10 +145,10 @@ module Rodauth
145
145
  end
146
146
 
147
147
  if verify_account_autologin?
148
- update_session
148
+ autologin_session('verify_account')
149
149
  end
150
150
 
151
- session[verify_account_session_key] = nil
151
+ remove_session_value(verify_account_session_key)
152
152
  set_notice_flash verify_account_notice_flash
153
153
  redirect verify_account_redirect
154
154
  end
@@ -158,6 +158,10 @@ module Rodauth
158
158
  end
159
159
  end
160
160
 
161
+ def require_login_confirmation?
162
+ false
163
+ end
164
+
161
165
  def allow_resending_verify_account_email?
162
166
  account[account_status_column] == account_unverified_status_value
163
167
  end
@@ -201,7 +205,7 @@ module Rodauth
201
205
  end
202
206
 
203
207
  def send_verify_account_email
204
- create_verify_account_email.deliver!
208
+ send_email(create_verify_account_email)
205
209
  end
206
210
 
207
211
  def verify_account_email_link
@@ -220,14 +224,6 @@ module Rodauth
220
224
  false
221
225
  end
222
226
 
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
227
  def create_account_set_password?
232
228
  return false if verify_account_set_password?
233
229
  super
@@ -247,6 +243,14 @@ module Rodauth
247
243
 
248
244
  private
249
245
 
246
+ def _login_form_footer_links
247
+ links = super
248
+ if !param_or_nil(login_param) || ((account || account_from_login(param(login_param))) && allow_resending_verify_account_email?)
249
+ links << [30, verify_account_resend_path, verify_account_resend_link_text]
250
+ end
251
+ links
252
+ end
253
+
250
254
  def verify_account_email_recently_sent?
251
255
  (email_last_sent = get_verify_account_email_last_sent) && (Time.now - email_last_sent < verify_account_skip_resend_email_within)
252
256
  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
@@ -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,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
@@ -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,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