rodauth 1.22.0 → 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
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