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,42 @@
1
+ module Booth
2
+ module Userland
3
+ module Otps
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
+ request.sudo.guard_with_otp { return _1 }
12
+
13
+ do_require_eligible_credential
14
+ .on_success { do_destroy }
15
+ end
16
+
17
+ private
18
+
19
+ def do_require_eligible_credential
20
+ return Tron.success :can_remove_otp if ::Booth::Credentials::Modes::OtpRemovable.call(credential)
21
+
22
+ Tron.failure :credential_not_otpable, public_message: I18n.t('booth.otp_not_eligible')
23
+ end
24
+
25
+ def do_destroy
26
+ if credential.mode_username_and_password!
27
+ credential.otp_regenerate_secret
28
+ credential.save # Not critical if this one fails
29
+
30
+ return Tron.success :otp_removed, public_message: I18n.t('booth.otp_removed')
31
+ end
32
+
33
+ Tron.failure :otp_removal_failed
34
+ end
35
+
36
+ def credential
37
+ @credential ||= ::Booth::Models::Credential.find(request.authentication.credential_id)
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,72 @@
1
+ module Booth
2
+ module Userland
3
+ module Otps
4
+ class Edit
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::Otps::Guards::Manageable.call(credential:) { return _1 }
13
+ ::Booth::Userland::Otps::Guards::Sudo.call(request:, credential:) { return _1 }
14
+
15
+ do_require_eligible_credential
16
+ # .on_success { do_clear_previous_editing }
17
+ .on_success { do_ensure_temporary_secret_key }
18
+ .on_success { do_reveal_otp_secret }
19
+ end
20
+
21
+ private
22
+
23
+ def do_require_eligible_credential
24
+ return Tron.success :can_add_otp if ::Booth::Credentials::Modes::OtpAddable.call(credential)
25
+ return Tron.success :can_change_otp if ::Booth::Credentials::Modes::OtpChangeable.call(credential)
26
+
27
+ Tron.failure :credential_not_otpable, public_message: I18n.t('booth.otp_not_eligible')
28
+ end
29
+
30
+ # Hm, tests failed with this.
31
+ # def do_clear_previous_editing
32
+ # return Tron.success :no_previous_editing unless storage.secret_key_recently_changed?
33
+ # return Tron.success :reset_not_requested unless request.params[:reset]
34
+
35
+ # #storage.reset
36
+ # Tron.success :previous_editing_cleared
37
+ # end
38
+
39
+ def do_ensure_temporary_secret_key
40
+ return Tron.success :secret_key_exists if storage.secret_key
41
+
42
+ storage.generate_secret_key!
43
+ Tron.success :secret_generated
44
+ end
45
+
46
+ def do_reveal_otp_secret
47
+ dummy = ::Booth::Models::Credential.new otp_secret_key: storage.secret_key, scope: scope
48
+
49
+ Tron.success :register_otp, step:,
50
+ otp_provisioning_svg: dummy.otp_provisioning_svg,
51
+ otp_provisioning_url: dummy.otp_provisioning_url
52
+ end
53
+
54
+ def step
55
+ return :successfully_changed if storage.secret_key_recently_changed?
56
+
57
+ return :confirm if storage.secret_key_has_been_seen?
58
+
59
+ :register
60
+ end
61
+
62
+ def storage
63
+ request.storage.otp
64
+ end
65
+
66
+ def credential
67
+ @credential ||= ::Booth::Models::Credential.find(request.authentication.credential_id)
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,21 @@
1
+ module Booth
2
+ module Userland
3
+ module Otps
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::OtpManageable.call(credential)
13
+
14
+ debug { 'OTP is not relevant to this credential' }
15
+ yield Tron.failure :otp_not_configurable, public_message: I18n.t('booth.otp_unavailable')
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,23 @@
1
+ module Booth
2
+ module Userland
3
+ module Otps
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_webauth?
15
+ return if credential.mode_username_and_webauth?
16
+
17
+ request.sudo.guard_with_otp { yield _1 }
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,36 @@
1
+ module Booth
2
+ module Userland
3
+ module Otps
4
+ class Show
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::Otps::Guards::Manageable.call(credential:) { return _1 }
13
+ ::Booth::Userland::Otps::Guards::Sudo.call(request:, credential:) { return _1 }
14
+
15
+ do_show
16
+ end
17
+
18
+ private
19
+
20
+ def do_show
21
+ if credential.mode_username_password_and_otp?
22
+ return Tron.success :current_otp, step: :show,
23
+ otp_provisioning_svg: credential.otp_provisioning_svg,
24
+ otp_provisioning_url: credential.otp_provisioning_url
25
+ end
26
+
27
+ Tron.success :otp_addable, step: :add
28
+ end
29
+
30
+ def credential
31
+ @credential ||= ::Booth::Models::Credential.find(request.authentication.credential_id)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,51 @@
1
+ module Booth
2
+ module Userland
3
+ module Otps
4
+ class Sudo
5
+ include ::Booth::Concerns::Action
6
+
7
+ def call
8
+ request.must_be_post!
9
+ request.must_be_html!
10
+ request.must_be_logged_in!
11
+
12
+ do_find_credential
13
+ .on_success { do_require_credential_has_otp }
14
+ .on_success { do_check_code }
15
+ .on_success { do_grant_sudo }
16
+ end
17
+
18
+ private
19
+
20
+ def do_find_credential
21
+ @credential = ::Booth::Models::Credential.find(request.authentication.credential_id)
22
+ return Tron.success :found_credential if @credential
23
+
24
+ Tron.failure :missing_credential
25
+ end
26
+
27
+ def do_require_credential_has_otp
28
+ return Tron.success :credential_has_otp if @credential.mode_username_password_and_otp?
29
+
30
+ Tron.failure :credential_has_no_otp, public_message: I18n.t('booth.otp_unavailable')
31
+ end
32
+
33
+ def do_check_code
34
+ ::Booth::Credentials::OtpAuthentication.call credential: @credential,
35
+ code: code_param,
36
+ ip: request.ip,
37
+ agent: request.agent
38
+ end
39
+
40
+ def do_grant_sudo
41
+ request.sudo.otp!
42
+ Tron.success :otp_sudo_granted
43
+ end
44
+
45
+ def code_param
46
+ params.require(:otp).permit(:otp)[:otp]
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,84 @@
1
+ module Booth
2
+ module Userland
3
+ module Otps
4
+ module Transitions
5
+ module Update
6
+ class Confirm
7
+ include ::Booth::Concerns::Transition
8
+
9
+ def self.applicable?(params:)
10
+ params.key?(:otp) && params[:otp].key?(:otp_confirmation)
11
+ end
12
+
13
+ def call
14
+ do_check_secret_key
15
+ .on_success { do_find_credential }
16
+ .on_success { do_require_eligible_credential }
17
+ .on_success { do_compare_code }
18
+ .on_success { do_update_credential }
19
+ .on_success { do_audit }
20
+ end
21
+
22
+ private
23
+
24
+ def do_check_secret_key
25
+ return Tron.success :secret_key_exists if storage.secret_key.present?
26
+
27
+ Tron.failure :missing_secret_key
28
+ end
29
+
30
+ def do_find_credential
31
+ @credential = ::Booth::Models::Credential.find(request.authentication.credential_id)
32
+ return Tron.success :found_credential if @credential
33
+
34
+ Tron.failure :missing_credential
35
+ end
36
+
37
+ def do_require_eligible_credential
38
+ return Tron.success :can_add_otp if ::Booth::Credentials::Modes::OtpAddable.call(@credential)
39
+ return Tron.success :can_change_otp if ::Booth::Credentials::Modes::OtpChangeable.call(@credential)
40
+
41
+ Tron.failure :credential_not_otpable, public_message: I18n.t('booth.otp_not_eligible')
42
+ end
43
+
44
+ def do_compare_code
45
+ @credential.otp_secret_key = storage.secret_key
46
+ return Tron.success :correct_code if @credential.authenticate_otp(code_param)
47
+
48
+ debug { "Provided code #{code_param} did not match expected code #{@credential.otp_code}" }
49
+ Tron.failure :wrong_code, public_message: I18n.t('booth.wrong_otp')
50
+ end
51
+
52
+ def do_update_credential
53
+ @credential.mode = :username_password_and_otp
54
+
55
+ if @credential.save
56
+ request.sudo.otp!
57
+ debug { 'You confirmed your newly changed otp' }
58
+ return Tron.success :otp_confirmed
59
+ end
60
+
61
+ debug { "The confirmation was correct but the update failed: #{@credential.errors.to_a.to_sentence}" }
62
+ Tron.failure :correct_otp_confirmation_failed
63
+ end
64
+
65
+ def do_audit
66
+ ::Booth::Audits::Register::ChangedOtp.call credential: @credential, ip: request.ip, agent: request.agent
67
+ storage.secret_key_recently_changed!
68
+
69
+ Tron.success :audit_created
70
+ end
71
+
72
+ def code_param
73
+ params.require(:otp).permit(:otp_confirmation)[:otp_confirmation]
74
+ end
75
+
76
+ def storage
77
+ request.storage.otp
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,40 @@
1
+ module Booth
2
+ module Userland
3
+ module Otps
4
+ module Transitions
5
+ module Update
6
+ class Register
7
+ include ::Booth::Concerns::Transition
8
+
9
+ def self.applicable?(params:)
10
+ params&.key?(:register)
11
+ end
12
+
13
+ def call
14
+ do_check_secret_key
15
+ .on_success { do_register_otp }
16
+ end
17
+
18
+ private
19
+
20
+ def do_check_secret_key
21
+ return Tron.success :secret_key_exists if storage.secret_key.present?
22
+
23
+ Tron.failure :missing_secret_key
24
+ end
25
+
26
+ def do_register_otp
27
+ storage.secret_key_has_been_seen!
28
+
29
+ Tron.success :otp_secret_registered
30
+ end
31
+
32
+ def storage
33
+ request.storage.otp
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,31 @@
1
+ module Booth
2
+ module Userland
3
+ module Otps
4
+ module Transitions
5
+ module Update
6
+ class Reset
7
+ include ::Booth::Concerns::Transition
8
+
9
+ def self.applicable?(params:)
10
+ params.key?(:reset)
11
+ end
12
+
13
+ def call
14
+ do_reset
15
+ end
16
+
17
+ private
18
+
19
+ def do_reset
20
+ storage.reset
21
+ end
22
+
23
+ def storage
24
+ request.storage.otp
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,34 @@
1
+ module Booth
2
+ module Userland
3
+ module Otps
4
+ class Update
5
+ include ::Booth::Concerns::Action
6
+
7
+ def call
8
+ request.must_be_patch!
9
+ request.must_be_html!
10
+ request.must_be_logged_in!
11
+
12
+ ::Booth::Userland::Otps::Guards::Manageable.call(credential:) { return _1 }
13
+ ::Booth::Userland::Otps::Guards::Sudo.call(request:, credential:) { return _1 }
14
+
15
+ do_transition
16
+ end
17
+
18
+ private
19
+
20
+ def transitions
21
+ [
22
+ ::Booth::Userland::Otps::Transitions::Update::Confirm,
23
+ ::Booth::Userland::Otps::Transitions::Update::Register,
24
+ ::Booth::Userland::Otps::Transitions::Update::Reset,
25
+ ]
26
+ end
27
+
28
+ def credential
29
+ @credential ||= ::Booth::Models::Credential.find(request.authentication.credential_id)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,73 @@
1
+ module Booth
2
+ module Userland
3
+ module PasswordResets
4
+ class Create
5
+ include ::Booth::Concerns::Action
6
+
7
+ def call
8
+ request.must_be_post!
9
+
10
+ do_check_logged_out
11
+ .on_success { do_find_credential }
12
+ .on_success { do_remember_email }
13
+ .on_success { do_create_password_reset }
14
+ end
15
+
16
+ private
17
+
18
+ delegate :username, to: :storage, private: true
19
+
20
+ def do_check_logged_out
21
+ unless request.authentication.logged_in?
22
+ debug { "Good, nobody happens to be already logged in in scope #{request.scope}" }
23
+ return Tron.success :not_logged_in
24
+ end
25
+
26
+ debug do
27
+ "Logged in as user #{request.authentication.username.inspect} but trying to request password reset"
28
+ end
29
+ Tron.failure :logged_in_user_cannot_reset_password, public_message: I18n.t('booth.logged_in_user_cannot_reset_password')
30
+ end
31
+
32
+ def do_find_credential
33
+ return Tron.success :known_credential if login_storage.credential_for_username
34
+
35
+ Tron.failure :unknown_credential, public_message: I18n.t('booth.password_reset_needs_username')
36
+ end
37
+
38
+ def do_remember_email
39
+ password_reset_storage.email = ::Booth::Syntaxes::Email.call(email_param).normalized_invalid_email
40
+
41
+ Tron.success :remembered_email
42
+ end
43
+
44
+ def do_create_password_reset
45
+ creation = ::Booth::PasswordResets::Create.call(
46
+ credential: login_storage.credential_for_username,
47
+ email: email_param,
48
+ ip: request.ip,
49
+ agent: request.agent
50
+ )
51
+
52
+ creation.on_success do
53
+ password_reset_storage.email = nil
54
+ end
55
+
56
+ creation
57
+ end
58
+
59
+ def login_storage
60
+ request.storage.login
61
+ end
62
+
63
+ def password_reset_storage
64
+ request.storage.password_reset
65
+ end
66
+
67
+ def email_param
68
+ params.require(:password_reset).permit(:email)[:email]
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,21 @@
1
+ module Booth
2
+ module Userland
3
+ module PasswordResets
4
+ module Guards
5
+ class LoggedOut
6
+ include ::Booth::MethodObject
7
+ include ::Booth::Logging
8
+
9
+ option :request
10
+
11
+ def call
12
+ return Tron.success :not_logged_in unless request.authentication.logged_in?
13
+
14
+ debug { "Logged in as user #{request.authentication.username.inspect} but trying to password reset" }
15
+ yield Tron.success :logged_in_user_cannot_reset_password, step: :logout_first
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,57 @@
1
+ module Booth
2
+ module Userland
3
+ module PasswordResets
4
+ class New
5
+ include ::Booth::Concerns::Action
6
+
7
+ def call
8
+ request.must_be_get!
9
+ request.must_be_html!
10
+
11
+ ::Booth::Userland::PasswordResets::Guards::LoggedOut.call(request:) { return _1 }
12
+
13
+ do_find_credential
14
+ .on_success { do_prepare_reset_form }
15
+ end
16
+
17
+ private
18
+
19
+ delegate :username, to: :storage, private: true
20
+
21
+ def do_find_credential
22
+ return Tron.success :known_credential if login_storage.credential_for_username
23
+
24
+ debug { "I don't know the username for a password reset" }
25
+ Tron.failure :unknown_credential, public_message: I18n.t('booth.password_reset_needs_username')
26
+ end
27
+
28
+ def do_prepare_reset_form
29
+ creation = ::Booth::PasswordResets::Create.call(
30
+ credential: login_storage.credential_for_username,
31
+ email: nil,
32
+ ip: request.ip,
33
+ agent: request.agent
34
+ )
35
+
36
+ if creation.failure == :blank_email
37
+ debug { 'Password reset is possible, once email was provided' }
38
+ return Tron.success :email_address_needed, username: login_storage.username,
39
+ email: password_reset_storage.email,
40
+ step: :new
41
+ end
42
+
43
+ # Because we did not pass in an email, this will never be successful.
44
+ creation
45
+ end
46
+
47
+ def login_storage
48
+ request.storage.login
49
+ end
50
+
51
+ def password_reset_storage
52
+ request.storage.password_reset
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,77 @@
1
+ module Booth
2
+ module Userland
3
+ module PasswordResets
4
+ class Show
5
+ include ::Booth::Concerns::Action
6
+
7
+ def call
8
+ request.must_be_get!
9
+ request.must_be_html!
10
+
11
+ do_find_password_reset
12
+ .on_success { do_check_scope }
13
+ .on_success { do_check_logged_out }
14
+ .on_success { do_access_password_reset }
15
+ end
16
+
17
+ def do_find_password_reset
18
+ finding = ::Booth::PasswordResets::Find.call(secret_key: secret_key_param)
19
+ return finding if finding.failure?
20
+
21
+ @password_reset = finding.password_reset
22
+ finding
23
+ end
24
+
25
+ def do_check_scope
26
+ ::Booth::Syntaxes::ScopeComparison.call this: request.scope, that: @password_reset.credential.scope
27
+ end
28
+
29
+ def do_check_logged_out
30
+ unless request.authentication.logged_in?
31
+ debug { "Good, nobody happens to be already logged in in scope #{request.scope}" }
32
+ return Tron.success :not_logged_in
33
+ end
34
+
35
+ if request.authentication.logged_in_as?(credential: @password_reset.credential)
36
+ debug { "#{@password_reset.credential.username} is already logged in in scope #{request.scope}" }
37
+ return Tron.success :logged_in_as_same_credential
38
+ end
39
+
40
+ # This is not a security issue (the reset link status is checked elsewhere),
41
+ # but we should not ask the user to logout knowing that password reset is unavailble.
42
+ if %i[completed timed_out].include?(@password_reset.step)
43
+ return Tron.failure :wrong_user_and_reset_unavailable, step: @password_reset.step
44
+ end
45
+
46
+ debug do
47
+ "Logged in as user #{request.authentication.username.inspect} but trying to reset password as #{@password_reset.credential.username.inspect}"
48
+ end
49
+ Tron.failure :wrong_user_logged_in, step: :wrong_user_logged_in,
50
+ secret_key: @password_reset.secret_key,
51
+ username: @password_reset.credential.username
52
+ end
53
+
54
+ def do_access_password_reset
55
+ # Storing the consumer IP for now, since we already have it.
56
+ if @password_reset.accessed_at.blank?
57
+ @password_reset.update!(accessed_at: Time.current, consumer_ip: request.ip)
58
+ end
59
+
60
+ debug do
61
+ "Accessed PasswordReset with ID #{@password_reset.id.inspect} and Credential ID #{@password_reset.credential_id.inspect}"
62
+ end
63
+ Tron.success :password_reset_accessed, credential_id: @password_reset.credential.id,
64
+ step: @password_reset.step,
65
+ username: @password_reset.credential.username,
66
+ secret_key: @password_reset.secret_key,
67
+ minlength: @password_reset.class.password_minlength,
68
+ passwordrules: @password_reset.class.passwordrules
69
+ end
70
+
71
+ def secret_key_param
72
+ params[:id]
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end