booth 0.0.1

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 (285) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +4 -0
  3. data/LICENSE.md +22 -0
  4. data/README.md +372 -0
  5. data/app/assets/config/booth_manifest.js +15 -0
  6. data/app/assets/images/booth/browsers/README.md +2 -0
  7. data/app/assets/images/booth/browsers/chrome.svg +1 -0
  8. data/app/assets/images/booth/browsers/edge.svg +1 -0
  9. data/app/assets/images/booth/browsers/firefox.svg +1 -0
  10. data/app/assets/images/booth/browsers/internet_explorer.svg +1 -0
  11. data/app/assets/images/booth/browsers/opera.svg +1 -0
  12. data/app/assets/images/booth/browsers/safari.svg +1 -0
  13. data/app/assets/images/booth/browsers/unknown.svg +1 -0
  14. data/app/assets/images/booth/platforms/README.md +2 -0
  15. data/app/assets/images/booth/platforms/android.svg +6 -0
  16. data/app/assets/images/booth/platforms/apple.svg +6 -0
  17. data/app/assets/images/booth/platforms/linux.svg +6 -0
  18. data/app/assets/images/booth/platforms/unknown.svg +1 -0
  19. data/app/assets/images/booth/platforms/windows.svg +6 -0
  20. data/app/assets/javascripts/booth/all.js +162 -0
  21. data/app/assets/javascripts/booth/all.js.map +1 -0
  22. data/app/assets/javascripts/booth/booth.ts +194 -0
  23. data/app/assets/javascripts/booth/webauthn-json.ts +99 -0
  24. data/config/locales/de.yml +84 -0
  25. data/config/locales/en.yml +79 -0
  26. data/lib/booth/adminland/credentials/create.rb +30 -0
  27. data/lib/booth/adminland/onboardings/create.rb +63 -0
  28. data/lib/booth/adminland/onboardings/destroy.rb +50 -0
  29. data/lib/booth/adminland/onboardings/find.rb +93 -0
  30. data/lib/booth/adminland/onboardings/index.rb +23 -0
  31. data/lib/booth/adminland/periodic_cleanup.rb +11 -0
  32. data/lib/booth/adminland/recoveries/consume.rb +70 -0
  33. data/lib/booth/adminland.rb +48 -0
  34. data/lib/booth/audits/register/added_otp.rb +22 -0
  35. data/lib/booth/audits/register/changed_otp.rb +22 -0
  36. data/lib/booth/audits/register/completed_onboarding.rb +22 -0
  37. data/lib/booth/audits/register/correct_otp.rb +42 -0
  38. data/lib/booth/audits/register/correct_password.rb +43 -0
  39. data/lib/booth/audits/register/logout.rb +22 -0
  40. data/lib/booth/audits/register/requested_password_reset.rb +22 -0
  41. data/lib/booth/audits/register/wrong_otp.rb +22 -0
  42. data/lib/booth/audits/register/wrong_password.rb +25 -0
  43. data/lib/booth/authenticators/confirm.rb +34 -0
  44. data/lib/booth/authenticators/credential_mode_after_confirmation.rb +25 -0
  45. data/lib/booth/authenticators/step.rb +19 -0
  46. data/lib/booth/concerns/action.rb +58 -0
  47. data/lib/booth/concerns/transition.rb +17 -0
  48. data/lib/booth/configuration.rb +116 -0
  49. data/lib/booth/configure.rb +37 -0
  50. data/lib/booth/contests/get.rb +36 -0
  51. data/lib/booth/contests/respond.rb +78 -0
  52. data/lib/booth/contests/set_for_login.rb +28 -0
  53. data/lib/booth/cooldowns/distance_of_time.rb +46 -0
  54. data/lib/booth/cooldowns/otp.rb +22 -0
  55. data/lib/booth/cooldowns/password.rb +44 -0
  56. data/lib/booth/cooldowns/password_reset.rb +24 -0
  57. data/lib/booth/cooldowns/strategies/exponential.rb +82 -0
  58. data/lib/booth/cooldowns/strategies/global.rb +62 -0
  59. data/lib/booth/cooldowns/strategies/result.rb +22 -0
  60. data/lib/booth/credentials/create.rb +28 -0
  61. data/lib/booth/credentials/create_with_onboarding.rb +26 -0
  62. data/lib/booth/credentials/find_by_username.rb +45 -0
  63. data/lib/booth/credentials/mode.rb +69 -0
  64. data/lib/booth/credentials/modes/otp_addable.rb +23 -0
  65. data/lib/booth/credentials/modes/otp_changeable.rb +23 -0
  66. data/lib/booth/credentials/modes/otp_manageable.rb +17 -0
  67. data/lib/booth/credentials/modes/otp_removable.rb +23 -0
  68. data/lib/booth/credentials/modes/password_addable.rb +29 -0
  69. data/lib/booth/credentials/modes/password_changeable.rb +31 -0
  70. data/lib/booth/credentials/modes/password_manageable.rb +17 -0
  71. data/lib/booth/credentials/modes/password_removable.rb +24 -0
  72. data/lib/booth/credentials/modes/password_removal_requires_user_verifiable_webauth.rb +16 -0
  73. data/lib/booth/credentials/modes/webauth_addable.rb +26 -0
  74. data/lib/booth/credentials/modes/webauth_manageable.rb +16 -0
  75. data/lib/booth/credentials/modes/webauth_removable.rb +25 -0
  76. data/lib/booth/credentials/otp_authentication.rb +59 -0
  77. data/lib/booth/credentials/password_authentication.rb +72 -0
  78. data/lib/booth/credentials/webauth_challenge.rb +28 -0
  79. data/lib/booth/engine.rb +25 -0
  80. data/lib/booth/errors.rb +86 -0
  81. data/lib/booth/geolocation.rb +20 -0
  82. data/lib/booth/hooks/after_fetch.rb +54 -0
  83. data/lib/booth/hooks/before_logout.rb +29 -0
  84. data/lib/booth/hooks/serialize_from_session.rb +24 -0
  85. data/lib/booth/hooks/serialize_into_session.rb +14 -0
  86. data/lib/booth/logger.rb +41 -0
  87. data/lib/booth/logging.rb +59 -0
  88. data/lib/booth/method_object.rb +73 -0
  89. data/lib/booth/mode.rb +22 -0
  90. data/lib/booth/models/application_record.rb +7 -0
  91. data/lib/booth/models/audit.rb +24 -0
  92. data/lib/booth/models/authenticator.rb +45 -0
  93. data/lib/booth/models/concerns/modeable.rb +50 -0
  94. data/lib/booth/models/concerns/otpable.rb +37 -0
  95. data/lib/booth/models/concerns/passwordable.rb +58 -0
  96. data/lib/booth/models/contest.rb +55 -0
  97. data/lib/booth/models/contests/scopes/recently_created.rb +23 -0
  98. data/lib/booth/models/contests/scopes/recently_responded.rb +32 -0
  99. data/lib/booth/models/credential.rb +61 -0
  100. data/lib/booth/models/onboarding.rb +61 -0
  101. data/lib/booth/models/password_reset.rb +41 -0
  102. data/lib/booth/models/recovery.rb +32 -0
  103. data/lib/booth/models/registration.rb +10 -0
  104. data/lib/booth/models/session.rb +47 -0
  105. data/lib/booth/models/user_agent.rb +50 -0
  106. data/lib/booth/modes/base.rb +25 -0
  107. data/lib/booth/modes/username_and_password.rb +7 -0
  108. data/lib/booth/modes/username_and_webauth.rb +7 -0
  109. data/lib/booth/modes/username_password_and_otp.rb +7 -0
  110. data/lib/booth/modes/username_password_and_webauth.rb +7 -0
  111. data/lib/booth/onboardings/find.rb +35 -0
  112. data/lib/booth/onboardings/propagate_to_credential.rb +63 -0
  113. data/lib/booth/onboardings/step.rb +68 -0
  114. data/lib/booth/password_resets/create.rb +57 -0
  115. data/lib/booth/password_resets/find.rb +36 -0
  116. data/lib/booth/password_resets/propagate_to_credential.rb +36 -0
  117. data/lib/booth/password_resets/step.rb +18 -0
  118. data/lib/booth/recoveries/create.rb +45 -0
  119. data/lib/booth/request.rb +106 -0
  120. data/lib/booth/requests/agent.rb +14 -0
  121. data/lib/booth/requests/authentication.rb +47 -0
  122. data/lib/booth/requests/ip.rb +28 -0
  123. data/lib/booth/requests/return_path.rb +34 -0
  124. data/lib/booth/requests/session.rb +106 -0
  125. data/lib/booth/requests/storage.rb +62 -0
  126. data/lib/booth/requests/storages/login.rb +108 -0
  127. data/lib/booth/requests/storages/otp.rb +54 -0
  128. data/lib/booth/requests/storages/password.rb +49 -0
  129. data/lib/booth/requests/storages/password_reset.rb +35 -0
  130. data/lib/booth/requests/storages/recovery.rb +35 -0
  131. data/lib/booth/requests/storages/registration.rb +27 -0
  132. data/lib/booth/requests/storages/webauth.rb +38 -0
  133. data/lib/booth/requests/sudo.rb +110 -0
  134. data/lib/booth/routes/userland.rb +80 -0
  135. data/lib/booth/sessions/create_and_login.rb +46 -0
  136. data/lib/booth/sessions/historical_locations.rb +18 -0
  137. data/lib/booth/sessions/index.rb +59 -0
  138. data/lib/booth/sessions/revoke.rb +51 -0
  139. data/lib/booth/sessions/revoke_all_others.rb +43 -0
  140. data/lib/booth/sessions/to_passport.rb +51 -0
  141. data/lib/booth/syntaxes/contest_code.rb +58 -0
  142. data/lib/booth/syntaxes/email.rb +97 -0
  143. data/lib/booth/syntaxes/ip.rb +37 -0
  144. data/lib/booth/syntaxes/otp.rb +57 -0
  145. data/lib/booth/syntaxes/scope.rb +21 -0
  146. data/lib/booth/syntaxes/scope_comparison.rb +28 -0
  147. data/lib/booth/syntaxes/secret_key.rb +64 -0
  148. data/lib/booth/syntaxes/username.rb +85 -0
  149. data/lib/booth/syntaxes/uuid.rb +23 -0
  150. data/lib/booth/test/helpers.rb +63 -0
  151. data/lib/booth/test/support/assert_all_partials_were_covered.rb +63 -0
  152. data/lib/booth/test/support/assert_logged_in.rb +49 -0
  153. data/lib/booth/test/support/assert_logged_out.rb +30 -0
  154. data/lib/booth/test/support/assert_partial.rb +29 -0
  155. data/lib/booth/test/support/force_login.rb +26 -0
  156. data/lib/booth/test/support/get_session_value.rb +35 -0
  157. data/lib/booth/test/support/otp_code_from_session.rb +30 -0
  158. data/lib/booth/test/support/soft_reset_session.rb +22 -0
  159. data/lib/booth/test/userland/logins/missing_authenticators.rb +72 -0
  160. data/lib/booth/test/userland/logins/missing_onboarding.rb +35 -0
  161. data/lib/booth/test/userland/logins/username_and_password.rb +40 -0
  162. data/lib/booth/test/userland/logins/username_and_webauth.rb +75 -0
  163. data/lib/booth/test/userland/logins/username_password_and_otp.rb +45 -0
  164. data/lib/booth/test/userland/logins/username_password_and_webauth.rb +86 -0
  165. data/lib/booth/test/userland/onboardings/already_logged_in.rb +64 -0
  166. data/lib/booth/test/userland/onboardings/otp.rb +63 -0
  167. data/lib/booth/test/userland/onboardings/password.rb +49 -0
  168. data/lib/booth/test/userland/onboardings/timeout.rb +47 -0
  169. data/lib/booth/test/userland/otps/manage.rb +86 -0
  170. data/lib/booth/test/userland/password_resets/reset.rb +102 -0
  171. data/lib/booth/test/userland.rb +38 -0
  172. data/lib/booth/test/webauthn/disable.rb +17 -0
  173. data/lib/booth/test/webauthn/enable.rb +19 -0
  174. data/lib/booth/test/webauthn/virtual_authenticators/create.rb +38 -0
  175. data/lib/booth/test/webauthn/virtual_authenticators/destroy.rb +20 -0
  176. data/lib/booth/test.rb +53 -0
  177. data/lib/booth/to_struct.rb +11 -0
  178. data/lib/booth/userland/extract_flash_messages.rb +35 -0
  179. data/lib/booth/userland/logins/create.rb +28 -0
  180. data/lib/booth/userland/logins/destroy.rb +37 -0
  181. data/lib/booth/userland/logins/new.rb +70 -0
  182. data/lib/booth/userland/logins/transitions/create/choose_username.rb +41 -0
  183. data/lib/booth/userland/logins/transitions/create/enter_otp.rb +70 -0
  184. data/lib/booth/userland/logins/transitions/create/skip_remotes.rb +24 -0
  185. data/lib/booth/userland/logins/transitions/create/verify_password.rb +70 -0
  186. data/lib/booth/userland/logins/transitions/create/webauth_authentication_initiation.rb +55 -0
  187. data/lib/booth/userland/logins/transitions/create/webauth_authentication_verification.rb +80 -0
  188. data/lib/booth/userland/logins/transitions/new/already_logged_in.rb +21 -0
  189. data/lib/booth/userland/logins/transitions/new/fallible.rb +27 -0
  190. data/lib/booth/userland/logins/transitions/new/mode_first_time.rb +20 -0
  191. data/lib/booth/userland/logins/transitions/new/mode_username_and_password.rb +20 -0
  192. data/lib/booth/userland/logins/transitions/new/mode_username_and_webauth.rb +26 -0
  193. data/lib/booth/userland/logins/transitions/new/mode_username_password_and_otp.rb +24 -0
  194. data/lib/booth/userland/logins/transitions/new/mode_username_password_and_webauth.rb +24 -0
  195. data/lib/booth/userland/logins/transitions/new/no_username_chosen.rb +19 -0
  196. data/lib/booth/userland/logins/transitions/new/remote_session_available.rb +52 -0
  197. data/lib/booth/userland/logins/transitions/new/timed_out.rb +25 -0
  198. data/lib/booth/userland/onboardings/show.rb +74 -0
  199. data/lib/booth/userland/onboardings/transitions/update/choose_mode.rb +58 -0
  200. data/lib/booth/userland/onboardings/transitions/update/choose_password.rb +41 -0
  201. data/lib/booth/userland/onboardings/transitions/update/choose_webauth_nickname.rb +50 -0
  202. data/lib/booth/userland/onboardings/transitions/update/confirm_otp.rb +58 -0
  203. data/lib/booth/userland/onboardings/transitions/update/confirm_password.rb +49 -0
  204. data/lib/booth/userland/onboardings/transitions/update/register_otp.rb +31 -0
  205. data/lib/booth/userland/onboardings/transitions/update/reset_otp.rb +40 -0
  206. data/lib/booth/userland/onboardings/transitions/update/reset_password.rb +35 -0
  207. data/lib/booth/userland/onboardings/transitions/update/reset_webauth.rb +46 -0
  208. data/lib/booth/userland/onboardings/transitions/update/webauth_authentication_initiation.rb +40 -0
  209. data/lib/booth/userland/onboardings/transitions/update/webauth_authentication_verification.rb +59 -0
  210. data/lib/booth/userland/onboardings/transitions/update/webauth_registration_initiation.rb +46 -0
  211. data/lib/booth/userland/onboardings/transitions/update/webauth_registration_verification.rb +56 -0
  212. data/lib/booth/userland/onboardings/update.rb +68 -0
  213. data/lib/booth/userland/otps/destroy.rb +42 -0
  214. data/lib/booth/userland/otps/edit.rb +72 -0
  215. data/lib/booth/userland/otps/guards/manageable.rb +21 -0
  216. data/lib/booth/userland/otps/guards/sudo.rb +23 -0
  217. data/lib/booth/userland/otps/show.rb +36 -0
  218. data/lib/booth/userland/otps/sudo.rb +51 -0
  219. data/lib/booth/userland/otps/transitions/update/confirm.rb +84 -0
  220. data/lib/booth/userland/otps/transitions/update/register.rb +40 -0
  221. data/lib/booth/userland/otps/transitions/update/reset.rb +31 -0
  222. data/lib/booth/userland/otps/update.rb +34 -0
  223. data/lib/booth/userland/password_resets/create.rb +73 -0
  224. data/lib/booth/userland/password_resets/guards/logged_out.rb +21 -0
  225. data/lib/booth/userland/password_resets/new.rb +57 -0
  226. data/lib/booth/userland/password_resets/show.rb +77 -0
  227. data/lib/booth/userland/password_resets/transitions/update/choose_password.rb +48 -0
  228. data/lib/booth/userland/password_resets/transitions/update/confirm_password.rb +54 -0
  229. data/lib/booth/userland/password_resets/transitions/update/reset_password.rb +29 -0
  230. data/lib/booth/userland/password_resets/update.rb +65 -0
  231. data/lib/booth/userland/passwords/destroy.rb +41 -0
  232. data/lib/booth/userland/passwords/edit.rb +54 -0
  233. data/lib/booth/userland/passwords/guards/manageable.rb +21 -0
  234. data/lib/booth/userland/passwords/guards/removable.rb +21 -0
  235. data/lib/booth/userland/passwords/guards/sudo.rb +21 -0
  236. data/lib/booth/userland/passwords/remove.rb +34 -0
  237. data/lib/booth/userland/passwords/show.rb +32 -0
  238. data/lib/booth/userland/passwords/sudo.rb +55 -0
  239. data/lib/booth/userland/passwords/transitions/remove/step.rb +27 -0
  240. data/lib/booth/userland/passwords/transitions/update/choose_password.rb +62 -0
  241. data/lib/booth/userland/passwords/transitions/update/confirm_password.rb +82 -0
  242. data/lib/booth/userland/passwords/update.rb +33 -0
  243. data/lib/booth/userland/personal_contests/show.rb +60 -0
  244. data/lib/booth/userland/personal_contests/update.rb +37 -0
  245. data/lib/booth/userland/recoveries/create.rb +48 -0
  246. data/lib/booth/userland/recoveries/new.rb +35 -0
  247. data/lib/booth/userland/registrations/create.rb +56 -0
  248. data/lib/booth/userland/registrations/new.rb +39 -0
  249. data/lib/booth/userland/sessions/destroy_one_or_other.rb +41 -0
  250. data/lib/booth/userland/sessions/index.rb +27 -0
  251. data/lib/booth/userland/sessions/show.rb +31 -0
  252. data/lib/booth/userland/sessions/transitions/destroy/enter_password.rb +50 -0
  253. data/lib/booth/userland/sessions/transitions/destroy/enter_webauth.rb +56 -0
  254. data/lib/booth/userland/sessions/transitions/destroy/verify_password.rb +83 -0
  255. data/lib/booth/userland/sessions/transitions/destroy/webauth_authentication_initiation.rb +38 -0
  256. data/lib/booth/userland/sessions/transitions/destroy/webauth_authentication_verification.rb +61 -0
  257. data/lib/booth/userland/sessions/transitions/show/enter_webauth.rb +56 -0
  258. data/lib/booth/userland/webauths/create.rb +83 -0
  259. data/lib/booth/userland/webauths/destroy.rb +60 -0
  260. data/lib/booth/userland/webauths/guards/manageable.rb +21 -0
  261. data/lib/booth/userland/webauths/guards/sudo.rb +22 -0
  262. data/lib/booth/userland/webauths/index.rb +43 -0
  263. data/lib/booth/userland/webauths/new.rb +70 -0
  264. data/lib/booth/userland/webauths/sudo.rb +25 -0
  265. data/lib/booth/userland/webauths/transitions/create/authentication_initiation.rb +52 -0
  266. data/lib/booth/userland/webauths/transitions/create/authentication_verification.rb +64 -0
  267. data/lib/booth/userland/webauths/transitions/create/choose_nickname.rb +50 -0
  268. data/lib/booth/userland/webauths/transitions/create/registration_initiation.rb +61 -0
  269. data/lib/booth/userland/webauths/transitions/create/registration_verification.rb +68 -0
  270. data/lib/booth/userland/webauths/transitions/create/reset.rb +36 -0
  271. data/lib/booth/userland/webauths/transitions/new/step.rb +23 -0
  272. data/lib/booth/userland/webauths/transitions/sudo/authentication_initiation.rb +47 -0
  273. data/lib/booth/userland/webauths/transitions/sudo/authentication_verification.rb +34 -0
  274. data/lib/booth/userland.rb +192 -0
  275. data/lib/booth/version.rb +3 -0
  276. data/lib/booth/webauth/authentication_verification.rb +68 -0
  277. data/lib/booth/webauth/demand_user_verification.rb +29 -0
  278. data/lib/booth/webauth/options_for_create.rb +46 -0
  279. data/lib/booth/webauth/options_for_get.rb +29 -0
  280. data/lib/booth.rb +267 -0
  281. data/lib/generators/booth/migration/migration_generator.rb +25 -0
  282. data/lib/generators/booth/migration/templates/add_credential_to_users.erb +18 -0
  283. data/lib/generators/booth/migration/templates/create_booth_mode_types.erb +20 -0
  284. data/lib/generators/booth/migration/templates/create_booth_tables.erb +135 -0
  285. metadata +861 -0
@@ -0,0 +1,56 @@
1
+ module Booth
2
+ module Userland
3
+ module Sessions
4
+ module Transitions
5
+ module Show
6
+ class EnterWebauth
7
+ include ::Booth::Concerns::Transition
8
+
9
+ def self.applicable?(params:)
10
+ !params[:webauth]
11
+ end
12
+
13
+ def call
14
+ if sudo.webauth?
15
+ if session_id_param
16
+ debug { 'Having webauth sudo, revoking the desired session...' }
17
+ return ::Booth::Sessions::Revoke.call credential_id: authentication.credential_id,
18
+ session_id: session_id_param
19
+ else
20
+ debug { 'Having webauth sudo, revoking all other sessions...' }
21
+ return ::Booth::Sessions::RevokeAllOthers.call credential_id: authentication.credential_id,
22
+ surviving_session_id: authentication.session_id
23
+ end
24
+ end
25
+
26
+ if session_id_param
27
+ Tron.failure :need_sudo_to_show_session,
28
+ step: :enter_webauth_to_show,
29
+ session_id: session_id_param
30
+ else
31
+ Tron.failure :need_sudo_to_show_all_other_sessions,
32
+ step: :enter_webauth_to_show_all_others
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def session_id_param
39
+ # If params[:id] is a UUID, then it's an ID for a `Booth::Models::Session` in the DB.
40
+ # If params[:id] is something else, then it's just a WebAuth Ceremony argument.
41
+ ::Booth::Syntaxes::Uuid.call(params[:id], raise_if_invalid: false).uuid
42
+ end
43
+
44
+ def authentication
45
+ request.authentication
46
+ end
47
+
48
+ def sudo
49
+ request.sudo
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,83 @@
1
+ module Booth
2
+ module Userland
3
+ module Webauths
4
+ class Create
5
+ include ::Booth::Concerns::Action
6
+
7
+ def call
8
+ request.must_be_post!
9
+ request.must_be_logged_in!
10
+
11
+ ::Booth::Userland::Webauths::Guards::Manageable.call(credential:) { return _1 }
12
+ ::Booth::Userland::Webauths::Guards::Sudo.call(request:, credential:) { return _1 }
13
+
14
+ do_ensure_authenticator
15
+ .on_success { do_require_editable_authentictor }
16
+ .on_success { do_transition }
17
+ end
18
+
19
+ private
20
+
21
+ def do_ensure_authenticator
22
+ return Tron.success :authenticator_exists if storage.authenticator
23
+
24
+ # TODO: test that one credential cannot have multiple deviceless authenticators
25
+ authenticator = credential.authenticators.find_or_initialize_by(device_id: nil)
26
+ authenticator.generate_webauth_id
27
+
28
+ webauth = ::Booth::Webauth::OptionsForCreate.call(
29
+ webauthn_id: authenticator.generate_webauth_id,
30
+ username: credential.username,
31
+ requires_user_verification: enforce_user_verification?
32
+ )
33
+
34
+ authenticator.challenge = webauth.challenge
35
+
36
+ if authenticator.save
37
+ storage.authenticator_id = authenticator.id
38
+ return Tron.success :authenticator_created if storage.authenticator
39
+ end
40
+
41
+ Tron.failure :authenticator_creation_failed
42
+ end
43
+
44
+ def do_require_editable_authentictor
45
+ if %i[register choose_nickname confirm].include?(storage.authenticator.step)
46
+ return Tron.success :authenticator_is_editable
47
+ end
48
+
49
+ Tron.failure :authenticator_already_completed
50
+ end
51
+
52
+ def transitions
53
+ [
54
+ ::Booth::Userland::Webauths::Transitions::Create::AuthenticationInitiation,
55
+ ::Booth::Userland::Webauths::Transitions::Create::AuthenticationVerification,
56
+ ::Booth::Userland::Webauths::Transitions::Create::ChooseNickname,
57
+ ::Booth::Userland::Webauths::Transitions::Create::RegistrationInitiation,
58
+ ::Booth::Userland::Webauths::Transitions::Create::RegistrationVerification,
59
+ ::Booth::Userland::Webauths::Transitions::Create::Reset,
60
+ ]
61
+ end
62
+
63
+ # When switching from with-password login to passwordless,
64
+ # we need to enable a way to add user-verifiable webauth tokens,
65
+ # even though we are currently not passwordless. That's what the param is for.
66
+ def enforce_user_verification?
67
+ ::Booth::Webauth::DemandUserVerification.call(
68
+ credential:,
69
+ force: request.params[:enforce_user_verification].present?
70
+ )
71
+ end
72
+
73
+ def storage
74
+ request.storage.webauth
75
+ end
76
+
77
+ def credential
78
+ @credential ||= ::Booth::Models::Credential.find(request.authentication.credential_id)
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,60 @@
1
+ module Booth
2
+ module Userland
3
+ module Webauths
4
+ class Destroy
5
+ include ::Booth::Concerns::Action
6
+
7
+ def call
8
+ request.must_be_delete!
9
+ request.must_be_html!
10
+ request.must_be_logged_in!
11
+
12
+ ::Booth::Userland::Webauths::Guards::Manageable.call(credential:) { return _1 }
13
+ ::Booth::Userland::Webauths::Guards::Sudo.call(request:, credential:) { return _1 }
14
+
15
+ do_require_eligible_credential
16
+ .on_success { do_find_authenticator }
17
+ .on_success { do_destroy_authenticator }
18
+ .on_success { do_update_credential }
19
+ end
20
+
21
+ private
22
+
23
+ def do_require_eligible_credential
24
+ return Tron.success :can_remove_webauth if ::Booth::Credentials::Modes::WebauthRemovable.call(credential)
25
+
26
+ Tron.failure :credential_not_webauth_removable, public_message: I18n.t('booth.webauth_irremovable')
27
+ end
28
+
29
+ def do_find_authenticator
30
+ @authenticator = credential.authenticators.find_by(id: params[:id])
31
+ return Tron.success :found_authenticator if @authenticator
32
+
33
+ Tron.failure :authenticator_not_found, public_message: I18n.t('booth.authenticator_not_found')
34
+ end
35
+
36
+ def do_destroy_authenticator
37
+ return Tron.success :authenticator_removed if @authenticator.destroy
38
+
39
+ Tron.failure :could_not_remove_authenticator, public_message: I18n.t('booth.authenticator_removal_failed')
40
+ end
41
+
42
+ def do_update_credential
43
+ if credential.authenticators.present?
44
+ return Tron.success(:some_authenticator_removed, public_message: I18n.t('booth.some_authenticator_removed'))
45
+ end
46
+
47
+ if credential.mode_username_and_password!
48
+ return Tron.success :webauth_removed, public_message: I18n.t('booth.webauth_removed')
49
+ end
50
+
51
+ raise "Could not switch Credential #{credential.id} to `username_and_password`: #{credential.errors.to_a}"
52
+ end
53
+
54
+ def credential
55
+ @credential ||= ::Booth::Models::Credential.find(request.authentication.credential_id)
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,21 @@
1
+ module Booth
2
+ module Userland
3
+ module Webauths
4
+ module Guards
5
+ class Manageable
6
+ include ::Booth::Logging
7
+ include ::Booth::MethodObject
8
+
9
+ option :credential
10
+
11
+ def call
12
+ return if ::Booth::Credentials::Modes::WebauthManageable.call(credential)
13
+
14
+ debug { 'Webauth is not relevant to this credential' }
15
+ yield Tron.failure :webauth_not_configurable, public_message: I18n.t('booth.webauth_unavailable')
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,22 @@
1
+ module Booth
2
+ module Userland
3
+ module Webauths
4
+ module Guards
5
+ class Sudo
6
+ include ::Booth::MethodObject
7
+
8
+ option :request
9
+ option :credential
10
+
11
+ def call
12
+ return if credential.mode_first_time?
13
+ return if credential.mode_username_and_password?
14
+ return if credential.mode_username_password_and_otp?
15
+
16
+ request.sudo.guard_with_webauth { yield _1 }
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,43 @@
1
+ module Booth
2
+ module Userland
3
+ module Webauths
4
+ class Index
5
+ include ::Booth::Concerns::Action
6
+
7
+ def call
8
+ request.must_be_get!
9
+ request.must_be_html!
10
+ request.must_be_logged_in!
11
+
12
+ ::Booth::Userland::Webauths::Guards::Manageable.call(credential:) { return _1 }
13
+ ::Booth::Userland::Webauths::Guards::Sudo.call(request:, credential:) { return _1 }
14
+
15
+ do_index
16
+ end
17
+
18
+ private
19
+
20
+ def do_index
21
+ request.storage.webauth.reset if params[:reset]
22
+
23
+ Tron.success :current_webauth, step: :index,
24
+ authenticators: authenticator_structs
25
+ end
26
+
27
+ def authenticator_structs
28
+ credential.authenticators.registered_scope.map do |authenticator|
29
+ ::Booth::ToStruct.call(
30
+ authenticator.attributes
31
+ .symbolize_keys
32
+ .slice(:id, :confirmed_at, :nickname, :supports_user_verification)
33
+ )
34
+ end
35
+ end
36
+
37
+ def credential
38
+ @credential ||= ::Booth::Models::Credential.find(request.authentication.credential_id)
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,70 @@
1
+ module Booth
2
+ module Userland
3
+ module Webauths
4
+ class New
5
+ include ::Booth::Concerns::Action
6
+
7
+ def call
8
+ request.must_be_get!
9
+ request.must_be_html!
10
+ request.must_be_logged_in!
11
+
12
+ ::Booth::Userland::Webauths::Guards::Manageable.call(credential:) { return _1 }
13
+
14
+ # This check must come before the Sudo requirement.
15
+ if authenticator&.confirmed?
16
+ return Tron.success :completed, step: :completed,
17
+ nickname: authenticator&.nickname,
18
+ should_add_more_authenticators: should_add_more_authenticators?
19
+ end
20
+
21
+ ::Booth::Userland::Webauths::Guards::Sudo.call(request:, credential:) { return _1 }
22
+
23
+ do_new
24
+ end
25
+
26
+ private
27
+
28
+ def do_new
29
+ debug { "WebAuthn Authenticator registration in step #{step}" }
30
+ # debug { authenticator.inspect }
31
+
32
+ Tron.success :add_webauth, step:,
33
+ nickname: authenticator&.nickname,
34
+ enforce_user_verification: enforce_user_verification?
35
+ end
36
+
37
+ def step
38
+ return :register unless authenticator
39
+
40
+ authenticator.step
41
+ end
42
+
43
+ def should_add_more_authenticators?
44
+ return true if credential.authenticators.registered_scope.count < 2
45
+ return false unless request.authentication.mode == :username_and_webauth
46
+
47
+ credential.authenticators.registered_scope.supports_user_verification_scope.count < 2
48
+ end
49
+
50
+ # When switching from with-password login to passwordless,
51
+ # we need to enable a way to add user-verifiable webauth tokens,
52
+ # even though we are currently not passwordless. That's what the param is for.
53
+ def enforce_user_verification?
54
+ ::Booth::Webauth::DemandUserVerification.call(
55
+ credential:,
56
+ force: params[:enforce_user_verification].present?
57
+ )
58
+ end
59
+
60
+ def authenticator
61
+ request.storage.webauth.authenticator
62
+ end
63
+
64
+ def credential
65
+ @credential ||= ::Booth::Models::Credential.find(request.authentication.credential_id)
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,25 @@
1
+ module Booth
2
+ module Userland
3
+ module Webauths
4
+ class Sudo
5
+ include ::Booth::Concerns::Action
6
+
7
+ def call
8
+ request.must_be_post!
9
+ request.must_be_logged_in!
10
+
11
+ do_transition
12
+ end
13
+
14
+ private
15
+
16
+ def transitions
17
+ [
18
+ ::Booth::Userland::Webauths::Transitions::Sudo::AuthenticationInitiation,
19
+ ::Booth::Userland::Webauths::Transitions::Sudo::AuthenticationVerification,
20
+ ]
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,52 @@
1
+ module Booth
2
+ module Userland
3
+ module Webauths
4
+ module Transitions
5
+ module Create
6
+ class AuthenticationInitiation
7
+ include ::Booth::Concerns::Transition
8
+
9
+ def self.applicable?(params:)
10
+ params.key?(:test) && !params&.key?(:webauth)
11
+ end
12
+
13
+ def call
14
+ do_challenge
15
+ end
16
+
17
+ private
18
+
19
+ def do_challenge
20
+ webauth = ::Booth::Webauth::OptionsForGet.call(
21
+ allowed_device_ids: authenticator.device_id,
22
+ requires_user_verification: enforce_user_verification?
23
+ )
24
+
25
+ debug { "Remembering test challenge #{webauth.challenge.inspect}" }
26
+
27
+ if authenticator.update challenge: webauth.challenge
28
+ Tron.success :test_challenge_created, public_json: webauth.as_json, http_status: :created
29
+ else
30
+ Tron.failure :storing_test_challenge_failed
31
+ end
32
+ end
33
+
34
+ def authenticator
35
+ request.storage.webauth.authenticator
36
+ end
37
+
38
+ def credential
39
+ @credential ||= ::Booth::Models::Credential.find(request.authentication.credential_id)
40
+ end
41
+
42
+ # I.e. modes `first_time` and `username_and_webauth`.
43
+ # TODO: Also when force-adding an authenticator for removing a password.
44
+ def enforce_user_verification?
45
+ !credential.passworded?
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,64 @@
1
+ module Booth
2
+ module Userland
3
+ module Webauths
4
+ module Transitions
5
+ module Create
6
+ class AuthenticationVerification
7
+ include ::Booth::Concerns::Transition
8
+
9
+ def self.applicable?(params:)
10
+ params.key?(:test) && params[:webauth].key?(:rawId)
11
+ end
12
+
13
+ def call
14
+ request.must_be_json!
15
+
16
+ do_verify_response
17
+ .on_success { do_persist_confirmation }
18
+ end
19
+
20
+ private
21
+
22
+ def do_verify_response
23
+ debug { 'Verifying challenge...' }
24
+ # TODO: Replate with ::Booth::Webauth::AuthenticationVerification
25
+ webauth.verify(authenticator.challenge,
26
+ public_key: authenticator.public_key,
27
+ sign_count: authenticator.sign_count)
28
+
29
+ Tron.success :challenge_response_correct
30
+ rescue WebAuthn::Error => e
31
+ debug { "Webauth Handshake failed: #{e.message}" }
32
+ Tron.failure :invalid_challenge_response
33
+ end
34
+
35
+ def do_persist_confirmation
36
+ confirmation = ::Booth::Authenticators::Confirm.call(authenticator:,
37
+ sign_count: webauth.sign_count)
38
+
39
+ if confirmation.success?
40
+ return Tron.success(:webauth_authentication_verification_successful, public_json: {},
41
+ http_status: :created)
42
+ end
43
+
44
+ Tron.failure :storing_response_failed, public_json: {},
45
+ http_status: :bad_request
46
+ end
47
+
48
+ def webauth
49
+ @webauth ||= ::WebAuthn::Credential.from_get(webauth_params)
50
+ end
51
+
52
+ def webauth_params
53
+ params.require(:webauth).permit!
54
+ end
55
+
56
+ def authenticator
57
+ request.storage.webauth.authenticator
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,50 @@
1
+ module Booth
2
+ module Userland
3
+ module Webauths
4
+ module Transitions
5
+ module Create
6
+ class ChooseNickname
7
+ include ::Booth::Concerns::Transition
8
+
9
+ def self.applicable?(params:)
10
+ params.dig(:webauth, :nickname)
11
+ end
12
+
13
+ def call
14
+ do_check_blank_nickname
15
+ .on_success { do_update_authenticator }
16
+ end
17
+
18
+ private
19
+
20
+ def do_check_blank_nickname
21
+ return Tron.success :nickname_is_present if nickname_param.present?
22
+
23
+ debug { 'The nickname was blank' }
24
+ Tron.failure :nickname_is_blank, public_message: I18n.t('booth.blank_nickname')
25
+ end
26
+
27
+ def do_update_authenticator
28
+ if authenticator.update nickname: nickname_param
29
+ debug { 'The nickname successfully changed' }
30
+ Tron.success :nickname_saved
31
+ else
32
+ public_message = authenticator.errors.to_a.to_sentence
33
+ debug { "The nickname could not be updated: #{public_message}" }
34
+ Tron.failure :nickname_failed, public_message:
35
+ end
36
+ end
37
+
38
+ def nickname_param
39
+ params.require(:webauth).permit(:nickname)[:nickname]
40
+ end
41
+
42
+ def authenticator
43
+ request.storage.webauth.authenticator
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,61 @@
1
+ module Booth
2
+ module Userland
3
+ module Webauths
4
+ module Transitions
5
+ module Create
6
+ class RegistrationInitiation
7
+ include ::Booth::Concerns::Transition
8
+
9
+ def self.applicable?(params:)
10
+ params&.key?(:register) && !params&.key?(:webauth)
11
+ end
12
+
13
+ def call
14
+ do_require_authenticator
15
+ .on_success { do_challenge }
16
+ end
17
+
18
+ private
19
+
20
+ def do_require_authenticator
21
+ return Tron.success :authenticator_waiting_for_registration if storage.authenticator.step == :register
22
+
23
+ Tron.failure :authenticator_not_registerable
24
+ end
25
+
26
+ def do_challenge
27
+ debug { "Remembering registration challenge #{webauth.challenge.inspect}" }
28
+
29
+ if storage.authenticator.update challenge: webauth.challenge,
30
+ supports_user_verification: enforce_user_verification?
31
+ return Tron.success :registration_challenge_created, public_json: webauth.as_json,
32
+ http_status: :created
33
+ end
34
+
35
+ Tron.failure :storing_registration_challenge_failed
36
+ end
37
+
38
+ def webauth
39
+ @webauth ||= ::Booth::Webauth::OptionsForCreate.call(
40
+ webauthn_id: storage.authenticator.webauthn_id,
41
+ username: storage.authenticator.credential.username,
42
+ requires_user_verification: enforce_user_verification?
43
+ )
44
+ end
45
+
46
+ def enforce_user_verification?
47
+ ::Booth::Webauth::DemandUserVerification.call(
48
+ credential: storage.authenticator.credential,
49
+ force: request.params[:enforce_user_verification].present?
50
+ )
51
+ end
52
+
53
+ def storage
54
+ request.storage.webauth
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,68 @@
1
+ module Booth
2
+ module Userland
3
+ module Webauths
4
+ module Transitions
5
+ module Create
6
+ class RegistrationVerification
7
+ include ::Booth::Concerns::Transition
8
+
9
+ def self.applicable?(params:)
10
+ params&.key?(:register) && params[:webauth]&.key?(:rawId)
11
+ end
12
+
13
+ def call
14
+ do_verify_response
15
+ .on_success { do_persist_keys }
16
+ end
17
+
18
+ private
19
+
20
+ def do_verify_response
21
+ debug { "Verifying challenge #{authenticator.challenge.inspect}" }
22
+ # TODO: Replate with ::Booth::Webauth::RegistrationVerification (?)
23
+ webauth.verify(authenticator.challenge)
24
+
25
+ Tron.success :challenge_response_correct
26
+ rescue WebAuthn::Error => e
27
+ debug { "Webauth Handshake failed: #{e.message}" }
28
+ Tron.failure :invalid_challenge_response
29
+ end
30
+
31
+ def do_persist_keys
32
+ debug { "Persisting authenticator information, the sign count is now at #{webauth.sign_count}..." }
33
+ if authenticator.update(device_id: ::Base64.strict_encode64(webauth.raw_id),
34
+ public_key: webauth.public_key,
35
+ sign_count: webauth.sign_count)
36
+
37
+ return Tron.success :challenge_accepted, public_json: {}, http_status: :created
38
+ end
39
+
40
+ Tron.failure :storing_response_failed
41
+ end
42
+
43
+ def webauth
44
+ @webauth ||= ::WebAuthn::Credential.from_create(webauth_params)
45
+ end
46
+
47
+ def webauth_params
48
+ params.require(:webauth).permit!
49
+ end
50
+
51
+ # def credential
52
+ # @credential ||= ::Booth::Models::Credential.find(request.authentication.credential_id)
53
+ # end
54
+
55
+ def mode_after_update
56
+ @mode_after_update ||= ::Booth::Webauth::ModeAfterAdd.call(credential:,
57
+ authenticator:)
58
+ end
59
+
60
+ def authenticator
61
+ request.storage.webauth.authenticator
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end