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,57 @@
1
+ module Booth
2
+ module PasswordResets
3
+ class Create
4
+ include ::Booth::Logging
5
+ include ::Booth::MethodObject
6
+
7
+ option :credential
8
+ option :email
9
+ option :ip
10
+ option :agent
11
+
12
+ def call
13
+ do_check_applicability
14
+ .on_success { do_check_email_syntax }
15
+ .on_success { do_check_cooldown }
16
+ .on_success { do_create_password_reset }
17
+ end
18
+
19
+ private
20
+
21
+ def do_check_applicability
22
+ return Tron.success :credential_has_password if credential.applicable_for_password_reset?
23
+
24
+ debug { 'This credential has no password to be reset' }
25
+ Tron.failure :credential_cannot_reset_password, public_message: I18n.t('booth.password_reset_not_available')
26
+ end
27
+
28
+ def do_check_email_syntax
29
+ check = ::Booth::Syntaxes::Email.call(email)
30
+
31
+ check.on_success do
32
+ @email_address = check.normalized_email
33
+ end
34
+
35
+ check
36
+ end
37
+
38
+ def do_check_cooldown
39
+ ::Booth::Cooldowns::PasswordReset.call(credential:)
40
+ end
41
+
42
+ def do_create_password_reset
43
+ password_reset = nil
44
+
45
+ ::Booth::Models::PasswordReset.transaction do
46
+ password_reset = ::Booth::Models::PasswordReset.create! credential:, creator_ip: ip
47
+ ::Booth::Audits::Register::RequestedPasswordReset.call credential:, ip:, agent:
48
+ end
49
+
50
+ Tron.success :applicable_for_reset, username: credential.username,
51
+ email: @email_address,
52
+ credential_id: credential.id,
53
+ secret_key: password_reset.secret_key
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,36 @@
1
+ module Booth
2
+ module PasswordResets
3
+ class Find
4
+ include ::Booth::Logging
5
+ include ::Booth::MethodObject
6
+
7
+ option :secret_key
8
+
9
+ def call
10
+ do_check_secret_key_syntax
11
+ .on_success { do_find_password_reset }
12
+ end
13
+
14
+ private
15
+
16
+ def do_check_secret_key_syntax
17
+ ::Booth::Syntaxes::SecretKey.call(secret_key)
18
+ end
19
+
20
+ def do_find_password_reset
21
+ debug { "Looking for PasswordReset with secret key #{secret_key.inspect}" }
22
+ @password_reset = ::Booth::Models::PasswordReset.find_by(secret_key:)
23
+
24
+ if @password_reset
25
+ debug { "Found PasswordReset with ID #{@password_reset.id.inspect}" }
26
+ @password_reset.update! accessed_at: Time.current if @password_reset.accessed_at.blank?
27
+ return Tron.success(:found_password_reset, password_reset: @password_reset)
28
+ end
29
+
30
+ message = "Could not find userland PasswordReset with secret key #{secret_key.inspect}"
31
+ debug { message }
32
+ Tron.failure :password_reset_not_found, public_message: I18n.t('booth.unknown_secret_key')
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,36 @@
1
+ module Booth
2
+ module PasswordResets
3
+ class PropagateToCredential
4
+ include ::Booth::Logging
5
+ include ::Booth::MethodObject
6
+
7
+ param :password_reset
8
+
9
+ def call
10
+ debug { 'Propagating PasswordReset to Credential...' }
11
+ raise "Expected PasswordReset to be valid: #{password_reset.errors.to_a.to_sentence}" if password_reset.invalid?
12
+
13
+ password_reset.transaction do
14
+ update_credential!
15
+ invalidate_password_resets!
16
+ finalize_password_reset!
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def update_credential!
23
+ password_reset.credential.update! password_digest: password_reset.password_digest
24
+ end
25
+
26
+ def invalidate_password_resets!
27
+ password_reset.other_password_resets_of_this_credential
28
+ .update_all(revoked_at: Time.current) # rubocop:disable Rails/SkipsModelValidations
29
+ end
30
+
31
+ def finalize_password_reset!
32
+ password_reset.update! propagated_at: Time.current
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,18 @@
1
+ module Booth
2
+ module PasswordResets
3
+ class Step
4
+ include ::Booth::MethodObject
5
+
6
+ param :password_reset
7
+
8
+ def call
9
+ return :completed if password_reset.completed?
10
+ return :revoked if password_reset.revoked?
11
+ return :timed_out unless password_reset.recently_created?
12
+ return :confirm_password if password_reset.password_chosen_at.present?
13
+
14
+ :choose_password
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,45 @@
1
+ module Booth
2
+ module Recoveries
3
+ class Create
4
+ include ::Booth::Logging
5
+ include ::Booth::MethodObject
6
+
7
+ option :scope
8
+ option :email
9
+ option :ip
10
+
11
+ def call
12
+ do_check_cooldown
13
+ .on_success { do_check_email_syntax }
14
+ .on_success { do_create_recovery }
15
+ end
16
+
17
+ private
18
+
19
+ def do_check_cooldown
20
+ # TODO: Check recovery cooldown of email in the table `booth_recoveries`.
21
+ # Respect consumed_at and revoked_at and created_at and creator_ip and email.
22
+ Tron.success :username_rate_limit_ok_dummy
23
+ end
24
+
25
+ def do_check_email_syntax
26
+ check = ::Booth::Syntaxes::Email.call(email)
27
+
28
+ check.on_success do
29
+ @email_address = check.normalized_email
30
+ end
31
+
32
+ check
33
+ end
34
+
35
+ def do_create_recovery
36
+ recovery = ::Booth::Models::Recovery.create! scope: scope,
37
+ email: email,
38
+ creator_ip: ip
39
+
40
+ Tron.success :applicable_for_recovery, email: recovery.email,
41
+ recovery_id: recovery.id
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,106 @@
1
+ module Booth
2
+ # Convenience wrapper for `Rack::Request`.
3
+ class Request
4
+ include ::Booth::Logging
5
+
6
+ # Can be used as a DRY::Initializer Coercer.
7
+ # See https://dry-rb.org/gems/dry-initializer/master/type-constraints/#back-references
8
+ def self.call(request, initializer)
9
+ # `request` is an `ActionDispatch::Request` or a `Rack::Request`.
10
+ # But if it already has been coerced, just return it immediately.
11
+ # This makes it easier to pass the request from one MethodObject to another.
12
+ return request if request.is_a?(self)
13
+
14
+ # `initializer` is an instance that is trying to coerce one of its params into a `Booth::Request`.
15
+ # By convention, that's where we assume the scope to be specified. So we take it from there.
16
+ new(request:, scope: initializer.scope)
17
+ end
18
+
19
+ def initialize(request:, scope:)
20
+ request = ActionDispatch::Request.new(request.env) if request.is_a?(Rack::Request)
21
+
22
+ @request = request
23
+ @scope = ::Booth::Syntaxes::Scope.call(scope).normalized_scope
24
+ end
25
+
26
+ attr_reader :scope
27
+
28
+ def agent
29
+ ::Booth::Requests::Agent.call(request:)
30
+ end
31
+
32
+ def ip
33
+ ::Booth::Requests::Ip.call(request:)
34
+ end
35
+
36
+ def location
37
+ ::Booth::Geolocation.lookup(ip)
38
+ end
39
+
40
+ def authentication
41
+ ::Booth::Requests::Authentication.new(scope:, warden: request.env['warden'])
42
+ end
43
+
44
+ def session(namespace:)
45
+ ::Booth::Requests::Session.new(scope:, namespace:, session: request.session)
46
+ end
47
+
48
+ def sudo
49
+ ::Booth::Requests::Sudo.new(scope:, request: self)
50
+ end
51
+
52
+ def storage
53
+ ::Booth::Requests::Storage.new(scope:, request: self)
54
+ end
55
+
56
+ def params
57
+ # Huh, I thought the following line was needed:
58
+ # return request.param if request.params.is_a?(::ActionController::Parameters)
59
+
60
+ @params ||= ::ActionController::Parameters.new(request.params)
61
+ end
62
+
63
+ def return_path
64
+ ::Booth::Requests::ReturnPath.call(params:)
65
+ end
66
+
67
+ # ------------
68
+ # Requirements
69
+ # ------------
70
+
71
+ def must_be_logged_in!
72
+ return if authentication.logged_in?
73
+
74
+ debug { "Expected someone to be logged in in scope #{scope.inspect}" }
75
+ raise ::Booth::Errors::NotAuthenticated
76
+ end
77
+
78
+ def must_be_html!
79
+ request.format.html? || raise(::Booth::Errors::MustBeHtml)
80
+ end
81
+
82
+ def must_be_json!
83
+ request.format.json? || raise(::Booth::Errors::MustBeJson)
84
+ end
85
+
86
+ def must_be_get!
87
+ request.get? || raise(::Booth::Errors::MustBeGet)
88
+ end
89
+
90
+ def must_be_post!
91
+ request.post? || raise(::Booth::Errors::MustBePost)
92
+ end
93
+
94
+ def must_be_patch!
95
+ request.patch? || raise(::Booth::Errors::MustBePatch)
96
+ end
97
+
98
+ def must_be_delete!
99
+ request.delete? || raise(::Booth::Errors::MustBeDelete)
100
+ end
101
+
102
+ private
103
+
104
+ attr_reader :request
105
+ end
106
+ end
@@ -0,0 +1,14 @@
1
+ module Booth
2
+ module Requests
3
+ class Agent
4
+ include ::Booth::MethodObject
5
+
6
+ option :request
7
+
8
+ def call
9
+ # Truncate to prevent potential cookie overflows and DB bloating.
10
+ request.user_agent.to_s.truncate(255).presence
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,47 @@
1
+ module Booth
2
+ module Requests
3
+ # Convenience wrapper for `Warden::Manager`.
4
+ class Authentication
5
+ delegate :username, :credential_id, :mode,
6
+ to: :passport,
7
+ allow_nil: true
8
+
9
+ def initialize(scope:, warden:)
10
+ @scope = scope
11
+ @warden = warden
12
+ end
13
+
14
+ def login(session:)
15
+ # Whatever instance we pass in to Warden,
16
+ # it needs to be that, which we also want do get out.
17
+ # Because serialization only takes place for the *next* request.
18
+ # In the *same* request, whatever we pass in, is returned back as it is.
19
+ warden.set_user ::Booth::Sessions::ToPassport.call(session), scope:
20
+ end
21
+
22
+ def logged_in?
23
+ warden.authenticated?(scope)
24
+ end
25
+
26
+ def logged_in_as?(credential:)
27
+ logged_in? && credential.id == credential_id
28
+ end
29
+
30
+ def logout
31
+ warden.logout(scope)
32
+ end
33
+
34
+ def session_id
35
+ passport&.id
36
+ end
37
+
38
+ private
39
+
40
+ attr_reader :scope, :warden
41
+
42
+ def passport
43
+ warden.user(scope)
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,28 @@
1
+ module Booth
2
+ module Requests
3
+ class Ip
4
+ include ::Booth::MethodObject
5
+
6
+ option :request
7
+ option :raise_if_invalid, default: -> { true }
8
+
9
+ def call
10
+ check = ::Booth::Syntaxes::Ip.call(raw_ip)
11
+ check.on_success { return check.normalized_ip }
12
+
13
+ raise ArgumentError "Invalid IP: #{raw_ip.inspect}" if raise_if_invalid
14
+
15
+ check
16
+ end
17
+
18
+ private
19
+
20
+ def raw_ip
21
+ # One could also use this:
22
+ # request.env['action_dispatch.remote_ip'].to_s
23
+ # But I think the `Rack::Request#trusted_proxy?` feature makes the following usable enough:
24
+ request.ip
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,34 @@
1
+ module Booth
2
+ module Requests
3
+ class ReturnPath
4
+ include ::Booth::MethodObject
5
+ include ::Booth::Logging
6
+
7
+ option :params
8
+
9
+ def call
10
+ uri = ::URI.parse raw_return_path
11
+
12
+ # Make sure we do not redirect to (external) URLs but only paths (i.e. protocol and domain-local).
13
+ result = [uri.path, uri.query].compact.join '?'
14
+
15
+ # We usually never store the return path in a cookie, but some developer might still try it.
16
+ # To avoid potential cookie overflows, allow only as many characters as reasonably needed.
17
+ result = result[0..1024]
18
+ return if result.blank?
19
+
20
+ # We always assume it is a full path and fix any missing beginning slash.
21
+ result.starts_with?('/') ? result : result.prepend('/')
22
+ rescue URI::InvalidURIError
23
+ debug { "Invalid return path: #{raw_return_path.inspect}" } if raw_return_path
24
+ nil
25
+ end
26
+
27
+ private
28
+
29
+ def raw_return_path
30
+ params.permit![:return_path]
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,106 @@
1
+ module Booth
2
+ module Requests
3
+ # Convenience wrapper for `ActionDispatch::Session::CookieStore`.
4
+ class Session
5
+ include ::Booth::Logging
6
+
7
+ attr_reader :scope
8
+
9
+ def initialize(scope:, namespace:, session:)
10
+ @scope = ::Booth::Syntaxes::Scope.call(scope).normalized_scope
11
+ @namespace = namespace
12
+ @session = session
13
+ end
14
+
15
+ # -------------------
16
+ # Getters and Setters
17
+ # -------------------
18
+
19
+ def [](key)
20
+ return if timed_out?
21
+ raise ArgumentError if key.to_s == 'created_at'
22
+
23
+ reset_if_too_old!
24
+ session[path(key)]
25
+ end
26
+
27
+ def []=(key, value)
28
+ return if timed_out?
29
+ raise ArgumentError if key.to_s == 'created_at'
30
+
31
+ ensure_ticking
32
+ reset_if_too_old!
33
+ session[path(key)] = value
34
+ end
35
+
36
+ def delete(key)
37
+ raise ArgumentError if key.to_s == 'created_at'
38
+
39
+ reset_if_too_old!
40
+ session.delete path(key)
41
+ end
42
+
43
+ def reset
44
+ debug { "Resetting #{namespace.inspect} cookie data in scope #{scope.inspect}" }
45
+
46
+ # There is no `#delete_if` in a `ActionDispatch::Request::Session`.
47
+ keys_to_delete = session.keys.select { _1.to_s.start_with?(path(nil)) }
48
+ keys_to_delete.each { session.delete(_1) }
49
+ end
50
+
51
+ # -----
52
+ # Timer
53
+ # -----
54
+
55
+ def lifespan
56
+ ::Booth.config.interaction_timeout.to_i
57
+ end
58
+
59
+ def seconds_until_auto_reset
60
+ return 0 if @reset_due_to_timeout
61
+
62
+ lifespan - age
63
+ end
64
+
65
+ def timed_out?
66
+ reset_if_too_old!
67
+
68
+ !!@reset_due_to_timeout
69
+ end
70
+
71
+ private
72
+
73
+ attr_reader :namespace, :session
74
+
75
+ def ensure_ticking
76
+ session[path(:created_at)] ||= Time.current.to_i
77
+ end
78
+
79
+ def ticking?
80
+ !!session[path(:created_at)]
81
+ end
82
+
83
+ def age
84
+ return 0 unless ticking?
85
+
86
+ Time.current.to_i - session[path(:created_at)]
87
+ end
88
+
89
+ def reset_if_too_old!
90
+ return if seconds_until_auto_reset.positive?
91
+
92
+ debug do
93
+ "Timeout-resetting #{namespace.inspect} cookie data in scope #{scope.inspect} " \
94
+ "because it is older than #{lifespan} seconds"
95
+ end
96
+
97
+ @reset_due_to_timeout = true
98
+ reset
99
+ end
100
+
101
+ def path(key)
102
+ [:booth, scope, namespace, key].join('.')
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,62 @@
1
+ module Booth
2
+ module Requests
3
+ class Storage
4
+ include ::Booth::Logging
5
+
6
+ def initialize(scope:, request:)
7
+ @scope = scope
8
+ @request = request
9
+ end
10
+
11
+ def login
12
+ @login ||= ::Booth::Requests::Storages::Login.new(
13
+ session: request.session(namespace: :login)
14
+ )
15
+ end
16
+
17
+ def otp
18
+ @otp ||= ::Booth::Requests::Storages::Otp.new(
19
+ session: request.session(namespace: :otp)
20
+ )
21
+ end
22
+
23
+ def webauth
24
+ @webauth ||= ::Booth::Requests::Storages::Webauth.new(
25
+ session: request.session(namespace: :webauth)
26
+ )
27
+ end
28
+
29
+ def password
30
+ @password ||= ::Booth::Requests::Storages::Password.new(
31
+ session: request.session(namespace: :password)
32
+ )
33
+ end
34
+
35
+ def password_reset
36
+ @password_reset ||= ::Booth::Requests::Storages::PasswordReset.new(
37
+ session: request.session(namespace: :password_reset)
38
+ )
39
+ end
40
+
41
+ def recovery
42
+ @recovery ||= ::Booth::Requests::Storages::Recovery.new(
43
+ session: request.session(namespace: :recovery)
44
+ )
45
+ end
46
+
47
+ def registration
48
+ @registration ||= ::Booth::Requests::Storages::Registration.new(
49
+ session: request.session(namespace: :registration)
50
+ )
51
+ end
52
+
53
+ private
54
+
55
+ attr_reader :scope, :request
56
+
57
+ def session
58
+ request.session(namespace: :sudo)
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,108 @@
1
+ module Booth
2
+ module Requests
3
+ module Storages
4
+ class Login
5
+ include ::Booth::Logging
6
+
7
+ delegate :reset, :timed_out?, :lifespan, :seconds_until_auto_reset, to: :session
8
+
9
+ def initialize(session:)
10
+ @session = session
11
+ end
12
+
13
+ # -------
14
+ # Getters
15
+ # -------
16
+
17
+ def username
18
+ session[:username]
19
+ end
20
+
21
+ def credential_for_username
22
+ return @credential_for_username if defined?(@credential_for_username)
23
+
24
+ @credential_for_username = ::Booth::Models::Credential.find_by(id: session[:credential_for_username])
25
+ end
26
+
27
+ def contest_for_username
28
+ return @contest_for_username if defined?(@contest_for_username)
29
+
30
+ @contest_for_username = ::Booth::Models::Contest.find_by(id: session[:contest_for_username])
31
+ end
32
+
33
+ def password_authenticated_credential
34
+ return @password_authenticated_credential if defined?(@password_authenticated_credential)
35
+
36
+ @password_authenticated_credential = ::Booth::Models::Credential.find_by(
37
+ id: session[:password_authenticated_credential]
38
+ )
39
+ end
40
+
41
+ def webauthn_challenge
42
+ session[:webauthn_challenge].presence
43
+ end
44
+
45
+ def skip_remotes?
46
+ session[:skip_remotes].present?
47
+ end
48
+
49
+ # -------
50
+ # Setters
51
+ # -------
52
+
53
+ def username=(new_username)
54
+ session[:username] = new_username
55
+ end
56
+
57
+ def credential_for_username=(new_credential)
58
+ debug { "Persisting credential for username in browser session for scope #{scope.inspect}" }
59
+ session[:credential_for_username] = new_credential.id
60
+ @credential_for_username = nil
61
+ end
62
+
63
+ def contest_for_username=(new_contest)
64
+ debug { "Persisting contest for username in browser session for scope #{scope.inspect}" }
65
+ session[:contest_for_username] = new_contest.id
66
+ @contest_for_username = nil
67
+ end
68
+
69
+ def reset_credential_for_username
70
+ debug { "Resetting credential in browser session for scope #{scope.inspect}" }
71
+ session.delete(:credential_for_username)
72
+ @credential_for_username = nil
73
+ end
74
+
75
+ def password_authenticated_credential=(new_credential)
76
+ debug { "Persisting password authenticated credential in browser session for scope #{scope.inspect}" }
77
+ session[:password_authenticated_credential] = new_credential.id
78
+ @password_authenticated_credential = nil
79
+ end
80
+
81
+ def webauthn_challenge=(new_challenge)
82
+ if new_challenge
83
+ debug do
84
+ "Persisting webauth challenge #{new_challenge.inspect} in browser session for scope #{scope.inspect}"
85
+ end
86
+ else
87
+ debug { "Removing webauth challenge from browser session for scope #{scope.inspect}" }
88
+ end
89
+ session[:webauthn_challenge] = new_challenge.presence
90
+ end
91
+
92
+ def skip_remotes
93
+ session[:skip_remotes] = true
94
+ end
95
+
96
+ def reset_remotes
97
+ session.delete(:skip_remotes)
98
+ end
99
+
100
+ private
101
+
102
+ attr_reader :session
103
+
104
+ delegate :scope, to: :session, private: true
105
+ end
106
+ end
107
+ end
108
+ end