rodauth 1.22.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (198) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +190 -0
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +210 -80
  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 +2 -3
  42. data/doc/jwt_refresh.rdoc +23 -8
  43. data/doc/lockout.rdoc +17 -15
  44. data/doc/login.rdoc +17 -2
  45. data/doc/login_password_requirements_base.rdoc +18 -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.23.0.txt +32 -0
  53. data/doc/release_notes/2.0.0.txt +361 -0
  54. data/doc/release_notes/2.1.0.txt +31 -0
  55. data/doc/release_notes/2.2.0.txt +39 -0
  56. data/doc/release_notes/2.3.0.txt +37 -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/close_account.rb +8 -6
  79. data/lib/rodauth/features/confirm_password.rb +40 -2
  80. data/lib/rodauth/features/create_account.rb +8 -13
  81. data/lib/rodauth/features/disallow_common_passwords.rb +1 -1
  82. data/lib/rodauth/features/disallow_password_reuse.rb +1 -1
  83. data/lib/rodauth/features/email_auth.rb +31 -30
  84. data/lib/rodauth/features/email_base.rb +9 -4
  85. data/lib/rodauth/features/http_basic_auth.rb +55 -35
  86. data/lib/rodauth/features/jwt.rb +63 -16
  87. data/lib/rodauth/features/jwt_cors.rb +15 -15
  88. data/lib/rodauth/features/jwt_refresh.rb +42 -13
  89. data/lib/rodauth/features/lockout.rb +12 -14
  90. data/lib/rodauth/features/login.rb +64 -15
  91. data/lib/rodauth/features/login_password_requirements_base.rb +13 -8
  92. data/lib/rodauth/features/otp.rb +77 -80
  93. data/lib/rodauth/features/password_complexity.rb +8 -13
  94. data/lib/rodauth/features/password_expiration.rb +2 -2
  95. data/lib/rodauth/features/password_grace_period.rb +17 -10
  96. data/lib/rodauth/features/recovery_codes.rb +49 -53
  97. data/lib/rodauth/features/remember.rb +11 -27
  98. data/lib/rodauth/features/reset_password.rb +26 -26
  99. data/lib/rodauth/features/session_expiration.rb +7 -10
  100. data/lib/rodauth/features/single_session.rb +8 -6
  101. data/lib/rodauth/features/sms_codes.rb +62 -72
  102. data/lib/rodauth/features/two_factor_base.rb +134 -30
  103. data/lib/rodauth/features/verify_account.rb +29 -21
  104. data/lib/rodauth/features/verify_account_grace_period.rb +18 -9
  105. data/lib/rodauth/features/verify_login_change.rb +12 -11
  106. data/lib/rodauth/features/webauthn.rb +505 -0
  107. data/lib/rodauth/features/webauthn_login.rb +70 -0
  108. data/lib/rodauth/features/webauthn_verify_account.rb +46 -0
  109. data/lib/rodauth/migrations.rb +16 -5
  110. data/lib/rodauth/version.rb +2 -2
  111. data/templates/button.str +1 -3
  112. data/templates/change-login.str +1 -2
  113. data/templates/change-password.str +3 -5
  114. data/templates/close-account.str +2 -2
  115. data/templates/confirm-password.str +1 -1
  116. data/templates/create-account.str +1 -1
  117. data/templates/email-auth-request-form.str +2 -3
  118. data/templates/email-auth.str +1 -1
  119. data/templates/global-logout-field.str +6 -0
  120. data/templates/login-confirm-field.str +2 -4
  121. data/templates/login-display.str +3 -2
  122. data/templates/login-field.str +2 -4
  123. data/templates/login-form-footer.str +6 -0
  124. data/templates/login-form.str +7 -0
  125. data/templates/login.str +1 -9
  126. data/templates/logout.str +1 -1
  127. data/templates/multi-phase-login.str +3 -0
  128. data/templates/otp-auth-code-field.str +5 -3
  129. data/templates/otp-auth.str +1 -1
  130. data/templates/otp-disable.str +1 -1
  131. data/templates/otp-setup.str +3 -3
  132. data/templates/password-confirm-field.str +2 -4
  133. data/templates/password-field.str +2 -4
  134. data/templates/recovery-auth.str +3 -6
  135. data/templates/recovery-codes.str +1 -1
  136. data/templates/remember.str +15 -20
  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-request.str +4 -4
  149. data/templates/unlock-account.str +1 -1
  150. data/templates/verify-account-resend.str +3 -3
  151. data/templates/verify-account.str +1 -2
  152. data/templates/verify-login-change.str +1 -1
  153. data/templates/webauthn-auth.str +11 -0
  154. data/templates/webauthn-remove.str +14 -0
  155. data/templates/webauthn-setup.str +12 -0
  156. metadata +94 -54
  157. data/Rakefile +0 -179
  158. data/doc/verify_change_login.rdoc +0 -11
  159. data/lib/rodauth/features/verify_change_login.rb +0 -20
  160. data/spec/account_expiration_spec.rb +0 -225
  161. data/spec/all.rb +0 -1
  162. data/spec/change_login_spec.rb +0 -156
  163. data/spec/change_password_notify_spec.rb +0 -33
  164. data/spec/change_password_spec.rb +0 -202
  165. data/spec/close_account_spec.rb +0 -162
  166. data/spec/confirm_password_spec.rb +0 -70
  167. data/spec/create_account_spec.rb +0 -127
  168. data/spec/disallow_common_passwords_spec.rb +0 -93
  169. data/spec/disallow_password_reuse_spec.rb +0 -179
  170. data/spec/email_auth_spec.rb +0 -285
  171. data/spec/http_basic_auth_spec.rb +0 -143
  172. data/spec/jwt_cors_spec.rb +0 -57
  173. data/spec/jwt_refresh_spec.rb +0 -256
  174. data/spec/jwt_spec.rb +0 -235
  175. data/spec/lockout_spec.rb +0 -250
  176. data/spec/login_spec.rb +0 -328
  177. data/spec/migrate/001_tables.rb +0 -184
  178. data/spec/migrate/002_account_password_hash_column.rb +0 -11
  179. data/spec/migrate_password/001_tables.rb +0 -73
  180. data/spec/migrate_travis/001_tables.rb +0 -141
  181. data/spec/password_complexity_spec.rb +0 -109
  182. data/spec/password_expiration_spec.rb +0 -244
  183. data/spec/password_grace_period_spec.rb +0 -93
  184. data/spec/remember_spec.rb +0 -451
  185. data/spec/reset_password_spec.rb +0 -229
  186. data/spec/rodauth_spec.rb +0 -343
  187. data/spec/session_expiration_spec.rb +0 -58
  188. data/spec/single_session_spec.rb +0 -127
  189. data/spec/spec_helper.rb +0 -327
  190. data/spec/two_factor_spec.rb +0 -1462
  191. data/spec/update_password_hash_spec.rb +0 -40
  192. data/spec/verify_account_grace_period_spec.rb +0 -171
  193. data/spec/verify_account_spec.rb +0 -240
  194. data/spec/verify_change_login_spec.rb +0 -46
  195. data/spec/verify_login_change_spec.rb +0 -232
  196. data/spec/views/layout-other.str +0 -11
  197. data/spec/views/layout.str +0 -11
  198. 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,
@@ -71,6 +70,7 @@ module Rodauth
71
70
  end
72
71
 
73
72
  r.post do
73
+ verified = false
74
74
  if account_from_login(param(login_param)) && allow_resending_verify_account_email?
75
75
  if verify_account_email_recently_sent?
76
76
  set_redirect_error_flash verify_account_email_recently_sent_error_flash
@@ -80,8 +80,11 @@ module Rodauth
80
80
  before_verify_account_email_resend
81
81
  if verify_account_email_resend
82
82
  after_verify_account_email_resend
83
+ verified = true
83
84
  end
85
+ end
84
86
 
87
+ if verified
85
88
  set_notice_flash verify_account_email_sent_notice_flash
86
89
  else
87
90
  set_redirect_error_status(no_matching_login_error_status)
@@ -95,10 +98,11 @@ module Rodauth
95
98
  route do |r|
96
99
  verify_account_check_already_logged_in
97
100
  before_verify_account_route
101
+ @password_field_autocomplete_value = 'new-password'
98
102
 
99
103
  r.get do
100
104
  if key = param_or_nil(verify_account_key_param)
101
- session[verify_account_session_key] = key
105
+ set_session_value(verify_account_session_key, key)
102
106
  redirect(r.path)
103
107
  end
104
108
 
@@ -106,7 +110,7 @@ module Rodauth
106
110
  if account_from_verify_account_key(key)
107
111
  verify_account_view
108
112
  else
109
- session[verify_account_session_key] = nil
113
+ remove_session_value(verify_account_session_key)
110
114
  set_redirect_error_flash no_matching_verify_account_key_error_flash
111
115
  redirect require_login_redirect
112
116
  end
@@ -145,10 +149,10 @@ module Rodauth
145
149
  end
146
150
 
147
151
  if verify_account_autologin?
148
- update_session
152
+ autologin_session('verify_account')
149
153
  end
150
154
 
151
- session[verify_account_session_key] = nil
155
+ remove_session_value(verify_account_session_key)
152
156
  set_notice_flash verify_account_notice_flash
153
157
  redirect verify_account_redirect
154
158
  end
@@ -158,6 +162,10 @@ module Rodauth
158
162
  end
159
163
  end
160
164
 
165
+ def require_login_confirmation?
166
+ false
167
+ end
168
+
161
169
  def allow_resending_verify_account_email?
162
170
  account[account_status_column] == account_unverified_status_value
163
171
  end
@@ -201,7 +209,7 @@ module Rodauth
201
209
  end
202
210
 
203
211
  def send_verify_account_email
204
- create_verify_account_email.deliver!
212
+ send_email(create_verify_account_email)
205
213
  end
206
214
 
207
215
  def verify_account_email_link
@@ -220,14 +228,6 @@ module Rodauth
220
228
  false
221
229
  end
222
230
 
223
- def login_form_footer
224
- super + verify_account_resend_link
225
- end
226
-
227
- def verify_account_resend_link
228
- "<p><a href=\"#{prefix}/#{verify_account_resend_route}\">Resend Verify Account Information</a></p>"
229
- end
230
-
231
231
  def create_account_set_password?
232
232
  return false if verify_account_set_password?
233
233
  super
@@ -247,6 +247,14 @@ module Rodauth
247
247
 
248
248
  private
249
249
 
250
+ def _login_form_footer_links
251
+ links = super
252
+ if !param_or_nil(login_param) || ((account || account_from_login(param(login_param))) && allow_resending_verify_account_email?)
253
+ links << [30, verify_account_resend_path, verify_account_resend_link_text]
254
+ end
255
+ links
256
+ end
257
+
250
258
  def verify_account_email_recently_sent?
251
259
  (email_last_sent = get_verify_account_email_last_sent) && (Time.now - email_last_sent < verify_account_skip_resend_email_within)
252
260
  end
@@ -3,7 +3,7 @@
3
3
  module Rodauth
4
4
  Feature.define(:verify_account_grace_period, :VerifyAccountGracePeriod) do
5
5
  depends :verify_account
6
- error_flash "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,10 @@ module Rodauth
41
52
  super if defined?(super)
42
53
  end
43
54
 
55
+ def allow_email_auth?
56
+ (defined?(super) ? super : true) && !account_in_unverified_grace_period?
57
+ end
58
+
44
59
  def verify_account_check_already_logged_in
45
60
  nil
46
61
  end
@@ -56,14 +71,8 @@ module Rodauth
56
71
  s
57
72
  end
58
73
 
59
- def update_session
60
- super
61
- if account_in_unverified_grace_period?
62
- session[unverified_account_session_key] = true
63
- end
64
- end
65
-
66
74
  def account_in_unverified_grace_period?
75
+ account || account_from_session
67
76
  account[account_status_column] == account_unverified_status_value &&
68
77
  verify_account_grace_period &&
69
78
  !verify_account_ds.where(Sequel.date_add(verification_requested_at_column, :seconds=>verify_account_grace_period) > Sequel::CURRENT_TIMESTAMP).empty?
@@ -4,10 +4,8 @@ module Rodauth
4
4
  Feature.define(:verify_login_change, :VerifyLoginChange) do
5
5
  depends :change_login, :email_base
6
6
 
7
- def_deprecated_alias :no_matching_verify_login_change_key_error_flash, :no_matching_verify_login_change_key_message
8
-
9
7
  error_flash "Unable to verify login change"
10
- error_flash "Unable to change login as there is already an account with the new login", :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