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,54 @@
1
+ module Booth
2
+ module Requests
3
+ module Storages
4
+ class Otp
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 secret_key
18
+ session[:secret_key]
19
+ end
20
+
21
+ def secret_key_has_been_seen?
22
+ !!session[:secret_key_has_been_seen]
23
+ end
24
+
25
+ def secret_key_recently_changed?
26
+ !!session[:secret_key_recently_changed]
27
+ end
28
+
29
+ # -------
30
+ # Setters
31
+ # -------
32
+
33
+ def generate_secret_key!
34
+ debug { 'Generating secret OTP key for registration' }
35
+ session[:secret_key] = ::Booth::Models::Credential.otp_random_secret
36
+ end
37
+
38
+ def secret_key_has_been_seen!
39
+ debug { 'Remembering that the secret OTP key has been registered' }
40
+ session[:secret_key_has_been_seen] = true
41
+ end
42
+
43
+ def secret_key_recently_changed!
44
+ reset
45
+ session[:secret_key_recently_changed] = true
46
+ end
47
+
48
+ private
49
+
50
+ attr_reader :session
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,49 @@
1
+ module Booth
2
+ module Requests
3
+ module Storages
4
+ class Password
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 password_digest
18
+ session[:password_digest]
19
+ end
20
+
21
+ # To show a reminder for the user to logout other devices after password change.
22
+ # This value either times out or is deleted at read-confirmation by the user.
23
+ def password_recently_changed?
24
+ !!session[:password_recently_changed]
25
+ end
26
+
27
+ # -------
28
+ # Setters
29
+ # -------
30
+
31
+ def password_digest=(new_password_digest)
32
+ debug { 'Remembering new password digest' }
33
+ session[:password_digest] = new_password_digest
34
+ end
35
+
36
+ def password_recently_changed!
37
+ debug { 'Remembering that the password was recently changed' }
38
+ session[:password_recently_changed] = true
39
+ end
40
+
41
+ private
42
+
43
+ attr_reader :session
44
+
45
+ delegate :scope, to: :session, private: true
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,35 @@
1
+ module Booth
2
+ module Requests
3
+ module Storages
4
+ class PasswordReset
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 email
18
+ session[:email]
19
+ end
20
+
21
+ # -------
22
+ # Setters
23
+ # -------
24
+
25
+ def email=(new_email)
26
+ session[:email] = new_email
27
+ end
28
+
29
+ private
30
+
31
+ attr_reader :session
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,35 @@
1
+ module Booth
2
+ module Requests
3
+ module Storages
4
+ class Recovery
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 email
18
+ session[:email]
19
+ end
20
+
21
+ # -------
22
+ # Setters
23
+ # -------
24
+
25
+ def email=(new_email)
26
+ session[:email] = new_email
27
+ end
28
+
29
+ private
30
+
31
+ attr_reader :session
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,27 @@
1
+ module Booth
2
+ module Requests
3
+ module Storages
4
+ class Registration
5
+ include ::Booth::Logging
6
+
7
+ delegate :reset, to: :session
8
+
9
+ def initialize(session:)
10
+ @session = session
11
+ end
12
+
13
+ def username
14
+ session[:username]
15
+ end
16
+
17
+ def username=(new_username)
18
+ session[:username] = new_username
19
+ end
20
+
21
+ private
22
+
23
+ attr_reader :session
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,38 @@
1
+ module Booth
2
+ module Requests
3
+ module Storages
4
+ class Webauth
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 authenticator
18
+ return @authenticator if defined?(@authenticator)
19
+ return if session[:authenticator_id].blank?
20
+
21
+ @authenticator = ::Booth::Models::Authenticator.find_by(id: session[:authenticator_id])
22
+ end
23
+
24
+ # -------
25
+ # Setters
26
+ # -------
27
+
28
+ def authenticator_id=(new_authenticator_id)
29
+ session[:authenticator_id] = new_authenticator_id
30
+ end
31
+
32
+ private
33
+
34
+ attr_reader :session
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,110 @@
1
+ module Booth
2
+ module Requests
3
+ class Sudo
4
+ include ::Booth::Logging
5
+
6
+ def initialize(scope:, request:)
7
+ @scope = scope
8
+ @request = request
9
+ end
10
+
11
+ def mode
12
+ request.authentication.mode.to_sym
13
+ end
14
+
15
+ def lifespan
16
+ ::Booth.config.interaction_timeout
17
+ end
18
+
19
+ # Guards
20
+
21
+ def guard_with_password
22
+ raise unless block_given?
23
+ return if password?
24
+
25
+ debug { 'You need password sudo' }
26
+ public_message = I18n.t('booth.password_sudo_timeout', lifespan_minutes: (lifespan / 60))
27
+ yield Tron.success(:password_sudo_needed, step: :sudo, public_message:)
28
+ end
29
+
30
+ def guard_with_otp
31
+ raise unless block_given?
32
+ return if otp?
33
+
34
+ debug { 'You need OTP sudo' }
35
+ public_message = I18n.t('booth.otp_sudo_timeout', lifespan_minutes: (lifespan / 60))
36
+ yield Tron.success(:otp_sudo_needed, step: :sudo, public_message:)
37
+ end
38
+
39
+ def guard_with_webauth
40
+ raise unless block_given?
41
+ return if webauth?
42
+
43
+ debug { 'You need Webauth sudo' }
44
+ public_message = I18n.t('booth.webauth_sudo_timeout', lifespan_minutes: (lifespan / 60))
45
+ yield Tron.success(:webauth_sudo_needed, step: :sudo, public_message:)
46
+ end
47
+
48
+ # Getters
49
+
50
+ def password?
51
+ return true if session[:password].to_i > lifespan.ago.to_i
52
+
53
+ session[:password] = nil
54
+ false
55
+ end
56
+
57
+ def otp?
58
+ return true if session[:otp].to_i > lifespan.ago.to_i
59
+
60
+ session[:otp] = nil
61
+ false
62
+ end
63
+
64
+ def webauth?
65
+ return true if session[:webauth].to_i > lifespan.ago.to_i
66
+
67
+ session[:webauth] = nil
68
+ false
69
+ end
70
+
71
+ def webauthn_challenge
72
+ session[:webauthn_challenge].presence
73
+ end
74
+
75
+ # Setters
76
+
77
+ def password!
78
+ debug { "Remembering sudo via password has been granted in scope #{scope}" }
79
+ session[:password] = Time.current.to_i
80
+ end
81
+
82
+ def otp!
83
+ debug { "Remembering sudo via OTP has been granted in scope #{scope}" }
84
+ session[:otp] = Time.current.to_i
85
+ end
86
+
87
+ def webauth!
88
+ debug { "Remembering sudo via WebAuth has been granted in scope #{scope}" }
89
+ session[:webauth] = Time.current.to_i
90
+ end
91
+
92
+ def webauthn_challenge=(new_challenge)
93
+ if new_challenge
94
+ debug { "Persisting webauth challenge #{new_challenge.inspect} in sudo session for scope #{scope.inspect}" }
95
+ else
96
+ debug { "Removing webauth challenge from sudo session for scope #{scope.inspect}" }
97
+ end
98
+ session[:webauthn_challenge] = new_challenge.presence
99
+ end
100
+
101
+ private
102
+
103
+ attr_reader :scope, :request
104
+
105
+ def session
106
+ request.session(namespace: :sudo)
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,80 @@
1
+ module Booth
2
+ module Routes
3
+ class Userland
4
+ include ::Booth::MethodObject
5
+
6
+ option :response_path, default: -> {}
7
+ option :self_registration
8
+ option :self_recovery
9
+
10
+ def call
11
+ result = [mandatory_routes]
12
+ result.push self_registration_routes if self_registration
13
+ result.push self_recovery_routes if self_recovery
14
+ result.join
15
+ end
16
+
17
+ private
18
+
19
+ def response_path_with_fallback
20
+ return ':response' if response_path.blank?
21
+
22
+ # Just to avoid potential syntax errors, we sanitize the variable little bit.
23
+ response_path.to_sym.to_s.inspect
24
+ end
25
+
26
+ # ---------
27
+ # Templates
28
+ # ---------
29
+
30
+ # There is probably no reason to avoid any of these routes.
31
+ def mandatory_routes
32
+ <<-RUBY
33
+ # Registering credentials for the first time, such as Hardware keys.
34
+ resources :onboardings, only: %i[show update]
35
+
36
+ # Login
37
+ resource :login, only: %i[new create destroy]
38
+ resource :response, only: %i[show update], path: #{response_path_with_fallback}
39
+
40
+ # Self-service portal
41
+ resources :sessions, only: %i[index show destroy] do
42
+ delete :destroy_all_others, on: :collection
43
+ end
44
+ resource :password, only: %i[show edit update destroy] do
45
+ collection do
46
+ get :remove
47
+ post :sudo
48
+ end
49
+ end
50
+ resource :otp, only: %i[show edit update destroy] do
51
+ post :sudo, on: :collection
52
+ end
53
+ resources :webauths, only: %i[index new create destroy] do
54
+ post :sudo, on: :collection
55
+ end
56
+ RUBY
57
+ end
58
+
59
+ # If users create their own accounts, you will need these routes.
60
+ def self_registration_routes
61
+ <<-RUBY
62
+ resources :registrations, only: %i[new create]
63
+ RUBY
64
+ end
65
+
66
+ # If users can recover lost credentials (username/password) via email, you need these routes.
67
+ def self_recovery_routes
68
+ <<-RUBY
69
+ resources :recoveries, only: %i[new create] do
70
+ get :check_your_mail, on: :collection
71
+ end
72
+
73
+ resources :password_resets, only: %i[new create show update] do
74
+ get :check_your_mail, on: :collection
75
+ end
76
+ RUBY
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,46 @@
1
+ module Booth
2
+ module Sessions
3
+ class CreateAndLogin
4
+ include ::Booth::MethodObject
5
+ include ::Booth::Logging
6
+
7
+ option :credential
8
+ option :request, ::Booth::Request
9
+
10
+ def call
11
+ if request.scope != credential.scope.to_sym
12
+ debug { "Request has scope #{request.scope.inspect} but Credential has #{credential.scope.to_sym.inspect}" }
13
+ raise 'Not performing login because something in your setup is wrong.'
14
+ end
15
+
16
+ request.authentication
17
+ .login(session: create_session)
18
+
19
+ # Consume everything that led up to this authentication to avoid re-authentication.
20
+ # If we don't reset the cookie here, the "remote login flow" could log you right back in.
21
+ login_storage.reset
22
+ registration_storage.reset
23
+
24
+ Tron.success :session_created_and_logged_in, return_path: request.return_path
25
+ end
26
+
27
+ private
28
+
29
+ def create_session
30
+ debug { "Creating new Session for credential ID #{credential.id}" }
31
+
32
+ ::Booth::Models::Session.create! credential:,
33
+ agent: request.agent,
34
+ most_recent_ip: request.ip
35
+ end
36
+
37
+ def login_storage
38
+ request.storage.login
39
+ end
40
+
41
+ def registration_storage
42
+ request.storage.registration
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,18 @@
1
+ module Booth
2
+ module Sessions
3
+ class HistoricalLocations
4
+ include ::Booth::MethodObject
5
+
6
+ param :session
7
+
8
+ # TODO: Show IPs for cities that are different from the most recent IP.
9
+ def call
10
+ return if historical_locations.values.blank?
11
+
12
+ historical_locations.values.reject { _1 == location }.uniq.sort.join(', ').presence
13
+ end
14
+
15
+ delegate :location, :historical_locations, to: :session, private: true
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,59 @@
1
+ module Booth
2
+ module Sessions
3
+ class Index
4
+ include ::Booth::MethodObject
5
+
6
+ option :credential_id
7
+ option :current_session_id
8
+
9
+ def call
10
+ # Booth currently doesn't support pagination.
11
+ # To avoid an attacking vector that could bring our database down,
12
+ # we simply pull the handbrake when trying to fetch too many sessions.
13
+ # Nobody should have that many active concurrent sessions.
14
+ raise "Too many sessions for Credential ID #{credential_id}" if base_scope.count > max_allowed_sessions
15
+
16
+ base_scope.map do |session|
17
+ ::Booth::ToStruct.call(exposed_attributes(session))
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def exposed_attributes(session)
24
+ result = { is_current: current_session_id == session.id }
25
+
26
+ exposed_attribute_names.each do |attribute|
27
+ result[attribute] = session.public_send(attribute)
28
+ end
29
+
30
+ result
31
+ end
32
+
33
+ def exposed_attribute_names
34
+ %i[id
35
+ activity_at
36
+ created_at
37
+ agent
38
+ most_recent_ip
39
+ historical_ips
40
+ location
41
+ historical_location_names
42
+ browser_name
43
+ platform_name
44
+ browser_image_path
45
+ platform_image_path]
46
+ end
47
+
48
+ def base_scope
49
+ ::Booth::Models::Session.active_scope
50
+ .sorted_scope
51
+ .owned_by_scope(credential_id:)
52
+ end
53
+
54
+ def max_allowed_sessions
55
+ 500
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,51 @@
1
+ module Booth
2
+ module Sessions
3
+ class Revoke
4
+ include ::Booth::MethodObject
5
+ include ::Booth::Logging
6
+
7
+ option :credential_id
8
+ option :session_id
9
+
10
+ def call
11
+ do_check_id_syntax
12
+ .on_success { do_revoke_session }
13
+ end
14
+
15
+ private
16
+
17
+ def do_check_id_syntax
18
+ ::Booth::Syntaxes::Uuid.call(credential_id, raise_if_invalid: true)
19
+ ::Booth::Syntaxes::Uuid.call(session_id, raise_if_invalid: true)
20
+
21
+ Tron.success :valid_session_id_syntax
22
+ end
23
+
24
+ def do_revoke_session
25
+ unless session
26
+ debug { "Session #{session_id.inspect} not found for credential #{credential_id.inspect}" }
27
+ return Tron.success :session_not_found
28
+ end
29
+
30
+ if session.revoked_at.present?
31
+ debug { "Session #{session.id.inspect} is already revoked." }
32
+ return Tron.success :already_revoked,
33
+ public_message: I18n.t('booth.session_revoked', ip: session.most_recent_ip)
34
+ end
35
+
36
+ debug { "Revoking Session #{session.id.inspect}" }
37
+ session.update! revoked_at: Time.current,
38
+ revoke_reason: :manual
39
+
40
+ Tron.success :session_revoked, public_message: I18n.t('booth.session_revoked', ip: session.most_recent_ip)
41
+ end
42
+
43
+ def session
44
+ return @session if defined?(@session)
45
+
46
+ @session = ::Booth::Models::Session.where(credential_id:)
47
+ .find_by(id: session_id)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,43 @@
1
+ module Booth
2
+ module Sessions
3
+ class RevokeAllOthers
4
+ include ::Booth::MethodObject
5
+
6
+ option :credential_id
7
+ option :surviving_session_id
8
+
9
+ def call
10
+ do_check_id_syntax
11
+ .on_success { do_revoke_all_other_session }
12
+ end
13
+
14
+ private
15
+
16
+ def do_check_id_syntax
17
+ ::Booth::Syntaxes::Uuid.call(credential_id, raise_if_invalid: true)
18
+ ::Booth::Syntaxes::Uuid.call(surviving_session_id, raise_if_invalid: true)
19
+
20
+ Tron.success :valid_session_id_syntax
21
+ end
22
+
23
+ def do_revoke_all_other_session
24
+ if sessions.blank?
25
+ return Tron.success :nothing_to_revoke, public_message: I18n.t('booth.all_other_sessions_revoked')
26
+ end
27
+
28
+ # Preferring speed. Because if there are many we don't want the page to hang.
29
+ sessions.update_all revoked_at: Time.current,
30
+ revoke_reason: :manual_all_others
31
+
32
+ Tron.success :other_sessions_revoked, public_message: I18n.t('booth.all_other_sessions_revoked')
33
+ end
34
+
35
+ def sessions
36
+ return @sessions if defined?(@sessions)
37
+
38
+ @sessions = ::Booth::Models::Session.where(credential_id:)
39
+ .where.not(id: surviving_session_id)
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,51 @@
1
+ module Booth
2
+ module Sessions
3
+ # A Passport is an immutable representation of your current browser-session.
4
+ class ToPassport
5
+ include ::Booth::MethodObject
6
+ include ::Booth::Logging
7
+
8
+ param :session
9
+
10
+ def call
11
+ ::Booth::ToStruct.call(attributes)
12
+ end
13
+
14
+ private
15
+
16
+ delegate :credential, to: :session, private: true
17
+
18
+ def attributes
19
+ {
20
+ id: session.id,
21
+ username: credential.username,
22
+ credential_id: credential.id,
23
+ mode:,
24
+ incognito_credential_id: session.incognito_credential_id,
25
+ revoked_at: session.revoked_at,
26
+ onboarding_secret_key:,
27
+ is:,
28
+ }
29
+ end
30
+
31
+ def is
32
+ ::Booth::Credentials::Mode.new(credential)
33
+ end
34
+
35
+ def mode
36
+ credential.mode.to_sym
37
+ end
38
+
39
+ def onboarding_secret_key
40
+ return unless credential.mode_first_time?
41
+
42
+ # If a first-time credential is in a logged in state,
43
+ # that means this credential was just self-registered.
44
+
45
+ # In that case, reveal the onboarding secret key,
46
+ # so that the end-user may choose a login method for the first time.
47
+ credential.onboarding&.secret_key
48
+ end
49
+ end
50
+ end
51
+ end