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,19 @@
1
+ module Booth
2
+ module Authenticators
3
+ class Step
4
+ include ::Booth::MethodObject
5
+
6
+ param :authenticator
7
+
8
+ def call
9
+ return :register if authenticator.device_id.blank? ||
10
+ authenticator.public_key.blank? ||
11
+ authenticator.sign_count.blank?
12
+ return :choose_nickname if authenticator.nickname.blank?
13
+ return :confirm if authenticator.confirmed_at.blank?
14
+
15
+ :completed
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,58 @@
1
+ module Booth
2
+ module Concerns
3
+ # An "Action" is something that is called from a Rails controller.#
4
+ # It contains all the logic that the controller action is supposed to execute.
5
+ # By convention the "authentication scope" and the "request object" are passed in.
6
+ module Action
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ include ::Booth::Logging
11
+ include ::Booth::MethodObject
12
+
13
+ option :scope, ->(scope) { ::Booth::Syntaxes::Scope.call(scope).normalized_scope }
14
+ option :request, ::Booth::Request
15
+
16
+ delegate :params, to: :request, private: true
17
+ end
18
+
19
+ private
20
+
21
+ # ----------------
22
+ # May be overriden
23
+ # ----------------
24
+
25
+ def initialize_transition
26
+ transition.call(request:)
27
+ end
28
+
29
+ def after_transition; end
30
+
31
+ def transitions
32
+ raise "Implement `#transitions` in #{self}"
33
+ end
34
+
35
+ # ---------------
36
+ # Never overriden
37
+ # ---------------
38
+
39
+ # I found this to be a repetitive pattern, so I added this method in this concern.
40
+ # It makes the code a little harder to read but probably still more robust.
41
+ def do_transition
42
+ if transition
43
+ # debug { "Calling Transition #{transition}" }
44
+ result = initialize_transition
45
+ after_transition
46
+ return result
47
+ end
48
+
49
+ debug { 'No transition applies to these params' }
50
+ Tron.failure :unknown_transition
51
+ end
52
+
53
+ def transition
54
+ @transition ||= transitions.detect { _1.applicable?(params:) }
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,17 @@
1
+ module Booth
2
+ module Concerns
3
+ # A `Booth::Action` usually consists of several `Booth::Transition`s.
4
+ module Transition
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ include ::Booth::Logging
9
+ include ::Booth::MethodObject
10
+
11
+ option :request, ::Booth::Request
12
+
13
+ delegate :params, to: :request, private: true
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,116 @@
1
+ module Booth
2
+ # Holds global configuration parameters.
3
+ class Configuration
4
+ def otp_issuer(scope: :default)
5
+ @otp_issuers ||= {}
6
+
7
+ @otp_issuers[scope.to_sym].to_s.presence || 'Login'
8
+ end
9
+
10
+ def set_otp_issuer(new_issuer, scope: :default)
11
+ @otp_issuers ||= {}
12
+
13
+ @otp_issuers[scope.to_sym] = new_issuer
14
+ end
15
+
16
+ def otp_issuer=(new_issuer)
17
+ set_otp_issuer(new_issuer)
18
+ end
19
+
20
+ def logger
21
+ return if @no_logger
22
+
23
+ @logger || rails_logger
24
+ end
25
+
26
+ def logger=(new_logger)
27
+ @no_logger = new_logger.nil?
28
+ @logger = new_logger
29
+ end
30
+
31
+ def log_to_rails_and_stdout!
32
+ self.logger = Class.new do
33
+ def debug(...)
34
+ ::Booth.config.send(:rails_logger).debug(...)
35
+ ::Booth.config.send(:stdout_logger).debug(...)
36
+ end
37
+
38
+ def warn(...)
39
+ ::Booth.config.send(:rails_logger).warn(...)
40
+ ::Booth.config.send(:stdout_logger).warn(...)
41
+ end
42
+
43
+ def error(...)
44
+ ::Booth.config.send(:rails_logger).error(...)
45
+ ::Booth.config.send(:stdout_logger).error(...)
46
+ end
47
+ end.new
48
+ end
49
+
50
+ def interaction_timeout
51
+ 20.minutes
52
+ end
53
+
54
+ def session_inactivity_lifetime
55
+ @session_inactivity_lifetime ||= 3.months
56
+ end
57
+
58
+ def session_inactivity_lifetime=(new_lifetime)
59
+ @session_inactivity_lifetime = new_lifetime
60
+ end
61
+
62
+ def password_reset_window
63
+ 2.hours
64
+ end
65
+
66
+ def onboarding_window
67
+ 1.week
68
+ end
69
+
70
+ def otp_digits
71
+ 6
72
+ end
73
+
74
+ def contest_digits
75
+ 6
76
+ end
77
+
78
+ private
79
+
80
+ # The standard Rails logger does not show `progname`.
81
+ # But it's a helpful feature, so we add it manually to the message.
82
+ def rails_logger
83
+ @rails_logger ||= Class.new do
84
+ def debug(progname, &block)
85
+ ::Rails.logger.debug { "#{ActiveSupport::LogSubscriber.new.send(:color, progname, :blue)} - #{block.call}" }
86
+ end
87
+
88
+ def info(progname, &block)
89
+ ::Rails.logger.info { "#{ActiveSupport::LogSubscriber.new.send(:color, progname, :blue)} - #{block.call}" }
90
+ end
91
+
92
+ def warn(progname, &block)
93
+ ::Rails.logger.warn { "#{ActiveSupport::LogSubscriber.new.send(:color, progname, :blue)} - #{block.call}" }
94
+ end
95
+
96
+ def error(progname, &block)
97
+ ::Rails.logger.error { "#{ActiveSupport::LogSubscriber.new.send(:color, progname, :blue)} - #{block.call}" }
98
+ end
99
+ end.new
100
+ end
101
+
102
+ def stdout_logger
103
+ return @stdout_logger if defined?(@stdout_logger)
104
+
105
+ @stdout_logger = ::Logger.new($stdout)
106
+ @stdout_logger.formatter = stdout_logger_formatter
107
+ @stdout_logger
108
+ end
109
+
110
+ def stdout_logger_formatter
111
+ proc do |severity, _, progname, message|
112
+ [severity.rjust(5), progname, '-', message, "\n"].join(' ')
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,37 @@
1
+ module Booth
2
+ # Lazy-loads and returns the global configuration instance.
3
+ #
4
+ # @example
5
+ # Booth.config.logger = MyLogger.new
6
+ #
7
+ # @return [Booth::Configuration]
8
+ # @see .configure
9
+ #
10
+ def self.config
11
+ @config ||= ::Booth::Configuration.new
12
+ end
13
+
14
+ # Yields the configuration instance.
15
+ #
16
+ # @example
17
+ # Booth.configure do |config|
18
+ # config.logger = MyLogger.new
19
+ # end
20
+ #
21
+ # @yieldparam [Booth::Configuration] config global configuration instance.
22
+ # @see .config
23
+ #
24
+ def self.configure
25
+ yield config
26
+ end
27
+
28
+ # Resets the configuration.
29
+ #
30
+ # @note This is useful for testing, since the configuration is global
31
+ # and persists across tests.
32
+ # @api private
33
+ #
34
+ def self.reset!
35
+ @configs = nil
36
+ end
37
+ end
@@ -0,0 +1,36 @@
1
+ module Booth
2
+ module Contests
3
+ class Get
4
+ include ::Booth::Logging
5
+ include ::Booth::MethodObject
6
+
7
+ option :credential_id
8
+
9
+ def call
10
+ return Tron.failure :contest_not_found unless contest
11
+
12
+ Tron.success :found_recent_contest,
13
+ formatted_code: contest.formatted_code,
14
+ normalized_code: contest.code,
15
+ reason: contest.reason.to_sym,
16
+ ip: contest.ip,
17
+ agent: contest.agent.presence,
18
+ location: contest.location.presence,
19
+ recently_responded: contest.recently_responded?,
20
+ browser_name: contest.browser_name,
21
+ platform_name: contest.platform_name,
22
+ browser_image_path: contest.browser_image_path,
23
+ platform_image_path: contest.platform_image_path
24
+ end
25
+
26
+ private
27
+
28
+ def contest
29
+ return @contest if defined?(@contest)
30
+
31
+ @contest = ::Booth::Models::Contest.recently_created_scope
32
+ .find_by(credential_id:)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,78 @@
1
+ module Booth
2
+ module Contests
3
+ class Respond
4
+ include ::Booth::Logging
5
+ include ::Booth::MethodObject
6
+
7
+ option :scope
8
+ option :contest
9
+ option :request
10
+
11
+ def call
12
+ do_find_contest
13
+ .on_success { do_check_timeout }
14
+ .on_success { do_check_scope }
15
+ .on_success { do_check_already_responded }
16
+ .on_success { do_check_code_syntax }
17
+ .on_success { do_respond }
18
+ end
19
+
20
+ private
21
+
22
+ delegate :credential, to: :contest, private: true
23
+
24
+ def do_find_contest
25
+ return Tron.success :contest_exists if contest
26
+
27
+ Tron.failure :missing_contest
28
+ end
29
+
30
+ def do_check_timeout
31
+ return Tron.success :contested_recently if contest.recently_created?
32
+
33
+ debug { 'This contest timed out' }
34
+ Tron.failure :contest_timed_out,
35
+ lifespan: contest.lifespan,
36
+ public_message: I18n.t('booth.contest_timed_out', lifespan_minutes: contest.lifespan.seconds / 60)
37
+ end
38
+
39
+ def do_check_scope
40
+ ::Booth::Syntaxes::ScopeComparison.call this: scope, that: credential.scope
41
+ end
42
+
43
+ def do_check_already_responded
44
+ return Tron.success :ok_waiting_for_response if contest.responded_at.blank?
45
+
46
+ debug { "This contest has already been responded to #{contest.responded_at.inspect}" }
47
+ Tron.failure :already_responded, public_message: I18n.t('booth.already_responded_to_contest')
48
+ end
49
+
50
+ def do_check_code_syntax
51
+ check = ::Booth::Syntaxes::ContestCode.call(code_param)
52
+
53
+ check.on_success do
54
+ @normalized_code = check.normalized_contest_code
55
+ end
56
+
57
+ check
58
+ end
59
+
60
+ def do_respond
61
+ return Tron.failure :no_response_needed unless contest.reason.to_sym == :login
62
+
63
+ if @normalized_code == contest.code
64
+ debug { "The code #{@normalized_code} was accepted, persisting positive response..." }
65
+ contest.update!(responded_at: Time.current)
66
+ return Tron.success :response_code_accepted,
67
+ public_message: I18n.t('booth.contest_response_accepted')
68
+ end
69
+
70
+ Tron.failure :wrong_code, public_message: I18n.t('booth.wrong_response_code')
71
+ end
72
+
73
+ def code_param
74
+ request.params.require(:response).permit(:code)[:code]
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,28 @@
1
+ module Booth
2
+ module Contests
3
+ class SetForLogin
4
+ include ::Booth::Logging
5
+ include ::Booth::MethodObject
6
+
7
+ option :credential_id
8
+ option :request
9
+
10
+ def call
11
+ contest = nil
12
+
13
+ ::Booth::Models::ApplicationRecord.transaction do
14
+ ::Booth::Models::Contest.where(credential_id:).delete_all
15
+
16
+ contest = ::Booth::Models::Contest.create!(
17
+ credential_id:,
18
+ reason: :login,
19
+ ip: request.ip,
20
+ agent: request.agent
21
+ )
22
+ end
23
+
24
+ Tron.success :contest_created, contest:
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,46 @@
1
+ module Booth
2
+ module Cooldowns
3
+ class DistanceOfTime
4
+ include ::Booth::MethodObject
5
+
6
+ option :from
7
+ option :till
8
+
9
+ def call
10
+ result = []
11
+ result.push("#{distance_in_hours} h") if show_hours?
12
+ result.push("#{distance_in_minutes} min") if show_minutes?
13
+ result.push("#{distance_in_seconds} s") if show_seconds?
14
+ result.join(' ')
15
+ end
16
+
17
+ def show_hours?
18
+ distance_in_hours.positive?
19
+ end
20
+
21
+ def show_minutes?
22
+ return false if (((till - from).abs % 3600) / 60) < 1
23
+
24
+ distance_in_minutes.nonzero?
25
+ end
26
+
27
+ def show_seconds?
28
+ return true if distance_in_seconds < 60
29
+
30
+ distance_in_hours.zero? && distance_in_minutes.zero?
31
+ end
32
+
33
+ def distance_in_hours
34
+ ((till - from).abs / 3600).floor
35
+ end
36
+
37
+ def distance_in_minutes
38
+ (((till - from).abs % 3600) / 60).round
39
+ end
40
+
41
+ def distance_in_seconds
42
+ (till - from).abs.round
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,22 @@
1
+ module Booth
2
+ module Cooldowns
3
+ class Otp
4
+ include ::Booth::MethodObject
5
+
6
+ option :credential
7
+
8
+ def call
9
+ ::Booth::Cooldowns::Strategies::Global.call scope:,
10
+ max_attempts: 10
11
+ end
12
+
13
+ private
14
+
15
+ def scope
16
+ ::Booth::Models::Audit.visible_scope
17
+ .event_entered_wrong_otp
18
+ .where(credential:)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,44 @@
1
+ module Booth
2
+ module Cooldowns
3
+ class Password
4
+ include ::Booth::MethodObject
5
+
6
+ option :ip
7
+ option :credential
8
+
9
+ def call
10
+ # No limit for logins where hardware tokens are required.
11
+ return Tron.success :cool_for_webauth if credential.mode_username_and_webauth?
12
+ return Tron.success :cool_for_password_and_webauth if credential.mode_username_password_and_webauth?
13
+
14
+ if credential.mode_username_password_and_otp?
15
+ do_check_exponentially
16
+ else
17
+ do_check_globally
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def do_check_exponentially
24
+ ::Booth::Cooldowns::Strategies::Exponential.call scope: base_scope
25
+ end
26
+
27
+ def do_check_globally
28
+ ::Booth::Cooldowns::Strategies::Global.call scope: ip_range_scope, max_attempts: 10
29
+ end
30
+
31
+ # Scopes
32
+
33
+ def ip_range_scope
34
+ base_scope.where('ip << ?', "#{ip}/24")
35
+ end
36
+
37
+ def base_scope
38
+ ::Booth::Models::Audit.visible_scope
39
+ .event_entered_wrong_password
40
+ .where(credential:)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,24 @@
1
+ module Booth
2
+ module Cooldowns
3
+ # Throttles how often you can generate a password reset link.
4
+ # Note that this does not reveal whether we know the email address or not,
5
+ # because it throttles the attempts per Credential (i.e. username).
6
+ class PasswordReset
7
+ include ::Booth::MethodObject
8
+
9
+ option :credential
10
+
11
+ def call
12
+ ::Booth::Cooldowns::Strategies::Exponential.call scope:
13
+ end
14
+
15
+ private
16
+
17
+ def scope
18
+ ::Booth::Models::Audit.visible_scope
19
+ .event_requested_password_reset
20
+ .where(credential:)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,82 @@
1
+ module Booth
2
+ module Cooldowns
3
+ module Strategies
4
+ class Exponential
5
+ include ::Booth::MethodObject
6
+ include ::Booth::Logging
7
+
8
+ option :scope
9
+
10
+ def call
11
+ return limit_not_yet_reached! if seconds_to_wait.zero?
12
+
13
+ limit_reached!
14
+ end
15
+
16
+ private
17
+
18
+ def limit_reached!
19
+ debug { "Wait #{seconds_to_wait}/#{waiting_period} sec for #{number_of_incidents} incidents" }
20
+ public_message = I18n.t('booth.try_again_cooldown', distance_of_time_until_cooldown:)
21
+
22
+ ::Booth::Cooldowns::Strategies::Result.failure(
23
+ public_message:,
24
+ attempts_left: 999_999,
25
+ cooldown_at:,
26
+ number_of_incidents:
27
+ )
28
+ end
29
+
30
+ def limit_not_yet_reached!
31
+ debug { 'No need to wait' }
32
+
33
+ ::Booth::Cooldowns::Strategies::Result.success(
34
+ public_message: nil,
35
+ attempts_left: 999_999,
36
+ number_of_incidents:
37
+ )
38
+ end
39
+
40
+ # Calculation Helpers
41
+
42
+ def cooldown_at
43
+ seconds_to_wait.from_now
44
+ end
45
+
46
+ def seconds_to_wait
47
+ return 0 unless newest_timestamp
48
+
49
+ candidate = newest_timestamp.to_i + waiting_period - Time.current.to_i
50
+ return 0 if candidate.negative?
51
+
52
+ candidate
53
+ end
54
+
55
+ def waiting_period
56
+ return 2.years if number_of_incidents > 9
57
+
58
+ # This effectively implies less than 10 attempts.
59
+ (5**number_of_incidents).seconds
60
+ end
61
+
62
+ # Queries
63
+
64
+ def number_of_incidents
65
+ @number_of_incidents ||= scope.count
66
+ end
67
+
68
+ def newest_timestamp
69
+ return @newest_timestamp if defined? @newest_timestamp
70
+
71
+ @newest_timestamp = scope.maximum(:created_at)
72
+ end
73
+
74
+ # Helpers
75
+
76
+ def distance_of_time_until_cooldown
77
+ ::Booth::Cooldowns::DistanceOfTime.call(from: Time.current, till: cooldown_at)
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,62 @@
1
+ module Booth
2
+ module Cooldowns
3
+ module Strategies
4
+ class Global
5
+ include ::Booth::MethodObject
6
+ include ::Booth::Logging
7
+
8
+ option :scope
9
+ option :max_attempts
10
+
11
+ def call
12
+ if number_of_incidents >= max_attempts
13
+ limit_reached!
14
+ else
15
+ limit_not_yet_reached!
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def limit_reached!
22
+ debug { "Limited globally #{number_of_incidents}/#{max_attempts}" }
23
+
24
+ ::Booth::Cooldowns::Strategies::Result.failure(
25
+ public_message: I18n.t('booth.permanently_blocked'),
26
+ attempts_left:,
27
+ cooldown_at: nil,
28
+ number_of_incidents:
29
+ )
30
+ end
31
+
32
+ def limit_not_yet_reached!
33
+ debug { "Not yet globally limited #{number_of_incidents}/#{max_attempts}" }
34
+
35
+ ::Booth::Cooldowns::Strategies::Result.success(
36
+ public_message:,
37
+ attempts_left:,
38
+ number_of_incidents:
39
+ )
40
+ end
41
+
42
+ def public_message
43
+ return if number_of_incidents.zero?
44
+
45
+ if attempts_left == 1
46
+ I18n.t 'booth.last_attempt'
47
+ else
48
+ I18n.t 'booth.attempts_left', attempts_left:
49
+ end
50
+ end
51
+
52
+ def attempts_left
53
+ max_attempts - number_of_incidents
54
+ end
55
+
56
+ def number_of_incidents
57
+ @number_of_incidents ||= scope.count
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,22 @@
1
+ module Booth
2
+ module Cooldowns
3
+ module Strategies
4
+ # All strategies quack the same way.
5
+ module Result
6
+ def self.failure(number_of_incidents:, public_message:, cooldown_at:, attempts_left:)
7
+ Tron.failure :hot, public_message:,
8
+ cooldown_at:,
9
+ attempts_left:,
10
+ number_of_incidents:
11
+ end
12
+
13
+ def self.success(public_message:, number_of_incidents:, attempts_left:)
14
+ Tron.success :cool, number_of_incidents:,
15
+ cooldown_at: nil,
16
+ public_message:,
17
+ attempts_left:
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end