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,55 @@
1
+ module Booth
2
+ module Models
3
+ class Contest < ::Booth::Models::ApplicationRecord
4
+ self.table_name = 'booth_contests'
5
+
6
+ belongs_to :credential
7
+
8
+ before_validation :ensure_code
9
+ before_validation :update_location
10
+
11
+ validates :credential_id, :code, :ip, presence: true
12
+ validates :credential_id, uniqueness: true
13
+ validates :reason, presence: true, inclusion: %w[login support]
14
+
15
+ scope :recently_created_scope, -> { ::Booth::Models::Contests::Scopes::RecentlyCreated.scope(self) }
16
+ scope :recently_responded_scope, -> { ::Booth::Models::Contests::Scopes::RecentlyResponded.scope(self) }
17
+
18
+ delegate :browser_name, :platform_name, :browser_image_path, :platform_image_path, to: :user_agent
19
+
20
+ def self.lifespan
21
+ ::Booth.config.interaction_timeout
22
+ end
23
+
24
+ def formatted_code
25
+ code.to_s.scan(/.{1,3}/).join(' ').presence
26
+ end
27
+
28
+ def recently_created?
29
+ ::Booth::Models::Contests::Scopes::RecentlyCreated.call(self)
30
+ end
31
+
32
+ def recently_responded?
33
+ ::Booth::Models::Contests::Scopes::RecentlyResponded.call(self)
34
+ end
35
+
36
+ def lifespan
37
+ self.class.lifespan
38
+ end
39
+
40
+ private
41
+
42
+ def ensure_code
43
+ self.code ||= 6.times.map { rand(0..9) }.join
44
+ end
45
+
46
+ def update_location
47
+ self.location = ::Booth::Geolocation.lookup(ip)
48
+ end
49
+
50
+ def user_agent
51
+ @user_agent = ::Booth::Models::UserAgent.new(agent)
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,23 @@
1
+ module Booth
2
+ module Models
3
+ module Contests
4
+ module Scopes
5
+ class RecentlyCreated
6
+ include ::Booth::MethodObject
7
+
8
+ param :contest
9
+
10
+ def self.scope(base)
11
+ base.where.not(created_at: nil)
12
+ .where('created_at > ?', ::Booth::Models::Contest.lifespan.ago)
13
+ end
14
+
15
+ def call
16
+ contest.created_at.present? &&
17
+ contest.created_at > ::Booth::Models::Contest.lifespan.ago
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,32 @@
1
+ module Booth
2
+ module Models
3
+ module Contests
4
+ module Scopes
5
+ class RecentlyResponded
6
+ include ::Booth::MethodObject
7
+
8
+ param :contest
9
+
10
+ def self.scope(base)
11
+ base.where.not(created_at: nil, responded_at: nil)
12
+ .where('created_at > ?', lifespan.ago)
13
+ .where('responded_at > ?', lifespan.ago)
14
+ end
15
+
16
+ def call
17
+ contest.created_at.present? &&
18
+ contest.responded_at.present? &&
19
+ contest.created_at > lifespan.ago &&
20
+ contest.responded_at > lifespan.ago
21
+ end
22
+
23
+ private
24
+
25
+ def lifespan
26
+ contest.class.lifespan
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,61 @@
1
+ module Booth
2
+ module Models
3
+ class Credential < ::Booth::Models::ApplicationRecord
4
+ include ::Booth::Models::Concerns::Modeable
5
+ include ::Booth::Models::Concerns::Passwordable
6
+ include ::Booth::Models::Concerns::Otpable
7
+
8
+ self.table_name = 'booth_credentials'
9
+
10
+ has_one :contest, dependent: :destroy
11
+ has_one :onboarding, dependent: :destroy
12
+
13
+ has_many :audits, dependent: :destroy
14
+ has_many :authenticators, dependent: :destroy
15
+ has_many :password_resets, dependent: :destroy
16
+ has_many :sessions, dependent: :destroy
17
+
18
+ before_validation :normalize_username
19
+ before_validation :normalize_scope
20
+ before_validation :stringify_allowed_modes
21
+
22
+ validates :username, :scope, :allowed_modes, :mode, presence: true
23
+ validates :username, uniqueness: { scope: :scope }
24
+
25
+ validates_each :allowed_modes do |record, attr, value|
26
+ record.errors.add(attr, 'is invalid') unless value.all? { modes.keys.include?(_1) }
27
+ end
28
+
29
+ def remote_session_available?
30
+ sessions.active_scope.any?
31
+ end
32
+
33
+ def applicable_for_password_reset?
34
+ mode_username_and_password? ||
35
+ mode_username_password_and_otp? ||
36
+ mode_username_password_and_webauth?
37
+ end
38
+ alias passworded? applicable_for_password_reset?
39
+
40
+ def registered_authenticator_ids
41
+ authenticators.registered_scope
42
+ .sorted_scope
43
+ .pluck(:device_id)
44
+ end
45
+
46
+ private
47
+
48
+ def normalize_username
49
+ self.username = ::Booth::Syntaxes::Username.call(username).normalized_username
50
+ end
51
+
52
+ def normalize_scope
53
+ self.scope = ::Booth::Syntaxes::Scope.call(scope).normalized_scope
54
+ end
55
+
56
+ def stringify_allowed_modes
57
+ self.allowed_modes = Array(allowed_modes).map(&:to_s)
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,61 @@
1
+ module Booth
2
+ module Models
3
+ class Onboarding < ::Booth::Models::ApplicationRecord
4
+ include ::Booth::Logging
5
+ include ::Booth::Models::Concerns::Modeable
6
+ include ::Booth::Models::Concerns::Passwordable
7
+ include ::Booth::Models::Concerns::Otpable
8
+
9
+ self.table_name = 'booth_onboardings'
10
+
11
+ belongs_to :credential, class_name: '::Booth::Models::Credential'
12
+
13
+ validates :credential_id, uniqueness: true
14
+ validates :webauthn_id, presence: true
15
+ validates :authenticator_nickname, length: { minimum: 3, maximum: 40 }, allow_blank: true
16
+
17
+ before_validation :ensure_webauthn_id
18
+
19
+ # See https://github.com/rails/rails/blob/main/activerecord/lib/active_record/secure_token.rb
20
+ has_secure_token :secret_key, length: 30
21
+ delegate :allowed_modes, :scope, :username, to: :credential
22
+
23
+ attr_accessor :otp_confirmation
24
+
25
+ scope :includes_scope, -> { includes(:credential) }
26
+ scope :sorted_scope, -> { order(:created_at) }
27
+
28
+ def step
29
+ ::Booth::Onboardings::Step.call(self)
30
+ end
31
+
32
+ def lifetime
33
+ ::Booth.config.onboarding_window
34
+ end
35
+
36
+ def completed?
37
+ step == :completed
38
+ end
39
+
40
+ def authenticator?
41
+ authenticator_public_key.present?
42
+ end
43
+
44
+ def propagated?
45
+ propagated_at.present?
46
+ end
47
+
48
+ def recently_created?
49
+ created_at > lifetime.ago
50
+ end
51
+
52
+ private
53
+
54
+ def ensure_webauthn_id
55
+ return if webauthn_id.present?
56
+
57
+ self.webauthn_id = ::WebAuthn.generate_user_id
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,41 @@
1
+ module Booth
2
+ module Models
3
+ class PasswordReset < ::Booth::Models::ApplicationRecord
4
+ include ::Booth::Logging
5
+ include ::Booth::Models::Concerns::Passwordable
6
+
7
+ self.table_name = 'booth_password_resets'
8
+
9
+ def self.lifetime
10
+ ::Booth.config.password_reset_window
11
+ end
12
+
13
+ belongs_to :credential, class_name: '::Booth::Models::Credential'
14
+
15
+ validates :creator_ip, presence: true
16
+
17
+ # See https://github.com/rails/rails/blob/main/activerecord/lib/active_record/secure_token.rb
18
+ has_secure_token :secret_key, length: 30
19
+
20
+ def step
21
+ ::Booth::PasswordResets::Step.call(self)
22
+ end
23
+
24
+ def completed?
25
+ password_chosen_at.present? && password_confirmed_at.present?
26
+ end
27
+
28
+ def revoked?
29
+ revoked_at.present?
30
+ end
31
+
32
+ def recently_created?
33
+ created_at > self.class.lifetime.ago
34
+ end
35
+
36
+ def other_password_resets_of_this_credential
37
+ credential.password_resets.where.not(id:)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,32 @@
1
+ module Booth
2
+ module Models
3
+ class Recovery < ::Booth::Models::ApplicationRecord
4
+ include ::Booth::Logging
5
+
6
+ self.table_name = 'booth_recoveries'
7
+
8
+ before_validation :normalize_email
9
+
10
+ def consumed?
11
+ consumed_at.present?
12
+ end
13
+
14
+ def revoked?
15
+ revoked_at.present?
16
+ end
17
+
18
+ def other_recoveries_with_this_scope_and_email
19
+ self.class
20
+ .where(scope:)
21
+ .where(email:)
22
+ .where.not(id:)
23
+ end
24
+
25
+ private
26
+
27
+ def normalize_email
28
+ self.email = ::Booth::Syntaxes::Email.call(email).normalized_email
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,10 @@
1
+ module Booth
2
+ module Models
3
+ class Registration < ::Booth::Models::ApplicationRecord
4
+ include ::Booth::Logging
5
+ include ::Booth::Models::Concerns::ModeEnumerable
6
+
7
+ self.table_name = 'booth_registrations'
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,47 @@
1
+ module Booth
2
+ module Models
3
+ class Session < ::Booth::Models::ApplicationRecord
4
+ self.table_name = :booth_sessions
5
+
6
+ def self.lifetime
7
+ ::Booth.config.session_inactivity_lifetime
8
+ end
9
+
10
+ belongs_to :credential, class_name: '::Booth::Models::Credential'
11
+
12
+ before_create :ensure_activity_at
13
+ before_create :denormalize_and_geolocate_ip
14
+
15
+ # First of all it has to be not deleted, but also it must not be way too old.
16
+ scope :active_scope, -> { where(revoked_at: nil).where('activity_at > ?', lifetime.ago) }
17
+ scope :owned_by_scope, lambda { |credential_id:|
18
+ active_scope.where('credential_id = ? OR incognito_credential_id = ?', credential_id, credential_id)
19
+ }
20
+ scope :sorted_scope, -> { order(activity_at: :desc) }
21
+
22
+ delegate :browser_name, :platform_name, :browser_image_path, :platform_image_path, to: :user_agent
23
+
24
+ def historical_location_names
25
+ ::Booth::Sessions::HistoricalLocations.call(self)
26
+ end
27
+
28
+ private
29
+
30
+ def ensure_activity_at
31
+ self.activity_at = Time.current
32
+ end
33
+
34
+ # These attributes are later updated via SQL.
35
+ # This method here only sets the initial state.
36
+ def denormalize_and_geolocate_ip
37
+ self.location = ::Booth::Geolocation.lookup(most_recent_ip)
38
+ self.historical_locations = { most_recent_ip => location }
39
+ self.historical_ips = { most_recent_ip => Time.current.to_i }
40
+ end
41
+
42
+ def user_agent
43
+ @user_agent = ::Booth::Models::UserAgent.new(agent)
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,50 @@
1
+ module Booth
2
+ module Models
3
+ class UserAgent
4
+ def initialize(agent_string)
5
+ @agent_string = agent_string
6
+ end
7
+
8
+ delegate :name, to: :browser, prefix: true
9
+
10
+ def platform_name
11
+ browser.platform.name
12
+ end
13
+
14
+ def browser_image_path
15
+ "booth/browsers/#{browser_id}.svg"
16
+ end
17
+
18
+ def platform_image_path
19
+ "booth/platforms/#{platform_id}.svg"
20
+ end
21
+
22
+ private
23
+
24
+ # Not sure if `https://github.com/podigee/device_detector` is better. Hard to tell.
25
+ def browser
26
+ @browser ||= ::Browser.new(@agent_string)
27
+ end
28
+
29
+ def browser_id
30
+ return :chrome if browser.chrome?
31
+ return :edge if browser.edge?
32
+ return :firefox if browser.firefox?
33
+ return :internet_explorer if browser.ie?
34
+ return :opera if browser.opera?
35
+ return :safari if browser.safari?
36
+
37
+ :unknown
38
+ end
39
+
40
+ def platform_id
41
+ return :android if browser.platform.android?
42
+ return :apple if browser.platform.ios? || browser.platform.mac?
43
+ return :linux if browser.platform.linux?
44
+ return :windows if browser.platform.windows?
45
+
46
+ :unknown
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,25 @@
1
+ module Booth
2
+ module Modes
3
+ module Base
4
+ extend ActiveSupport::Concern
5
+
6
+ class_methods do
7
+ def id
8
+ self.to_s.demodulize.underscore
9
+ end
10
+
11
+ def title
12
+ I18n.t "booth.mode_#{id}_title"
13
+ end
14
+
15
+ def description
16
+ I18n.t "booth.mode_#{id}_description"
17
+ end
18
+
19
+ def <=>(other)
20
+ ::Booth::Mode.all.index(self) <=> ::Booth::Mode.all.index(other)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,7 @@
1
+ module Booth
2
+ module Modes
3
+ module UsernameAndPassword
4
+ include ::Booth::Modes::Base
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module Booth
2
+ module Modes
3
+ module UsernameAndWebauth
4
+ include ::Booth::Modes::Base
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module Booth
2
+ module Modes
3
+ module UsernamePasswordAndOtp
4
+ include ::Booth::Modes::Base
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module Booth
2
+ module Modes
3
+ module UsernamePasswordAndWebauth
4
+ include ::Booth::Modes::Base
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,35 @@
1
+ module Booth
2
+ module Onboardings
3
+ class Find
4
+ include ::Booth::Logging
5
+ include ::Booth::MethodObject
6
+
7
+ option :secret_key
8
+
9
+ def call
10
+ check_secret_key_syntax_action
11
+ .on_success { find_onboarding_action }
12
+ end
13
+
14
+ private
15
+
16
+ def check_secret_key_syntax_action
17
+ ::Booth::Syntaxes::SecretKey.call(secret_key)
18
+ end
19
+
20
+ def find_onboarding_action
21
+ debug { "Looking for Onboarding with secret key #{secret_key.inspect}" }
22
+ onboarding = ::Booth::Models::Onboarding.find_by(secret_key:)
23
+
24
+ if onboarding
25
+ debug { "Found Onboarding with ID #{onboarding.id.inspect}" }
26
+ Tron.success(:found_onboarding, onboarding:)
27
+ else
28
+ message = "Could not find userland Onboarding with secret key #{secret_key.inspect}"
29
+ debug { message }
30
+ Tron.failure :onboarding_not_found, public_message: I18n.t('booth.unknown_secret_key')
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,63 @@
1
+ module Booth
2
+ module Onboardings
3
+ class PropagateToCredential
4
+ include ::Booth::Logging
5
+ include ::Booth::MethodObject
6
+
7
+ param :onboarding
8
+ option :ip
9
+ option :agent
10
+
11
+ def call
12
+ debug { 'Propagating Onboarding to Credential...' }
13
+ raise "Expected Onboarding to be valid: #{onboarding.errors.full_messages.to_sentence}" if onboarding.invalid?
14
+
15
+ onboarding.transaction do
16
+ update_credential!
17
+ remove_existing_authenticators!
18
+ create_authenticator!
19
+ register_audit!
20
+ finalize_onboarding!
21
+ end
22
+ debug { 'Propagation of Onboarding completed' }
23
+ end
24
+
25
+ private
26
+
27
+ def update_credential!
28
+ onboarding.credential.update! mode: onboarding.mode,
29
+ password_digest: onboarding.password_digest,
30
+ otp_secret_key: onboarding.otp_secret_key
31
+ end
32
+
33
+ def remove_existing_authenticators!
34
+ onboarding.credential.authenticators.destroy_all
35
+ end
36
+
37
+ def create_authenticator!
38
+ return unless onboarding.authenticator?
39
+
40
+ onboarding.credential.authenticators.create! webauthn_id: onboarding.webauthn_id,
41
+ device_id: onboarding.authenticator_id,
42
+ nickname: onboarding.authenticator_nickname,
43
+ public_key: onboarding.authenticator_public_key,
44
+ sign_count: onboarding.authenticator_sign_count,
45
+ challenge: onboarding.authenticator_challenge,
46
+ supports_user_verification: onboarding.requires_user_verification?,
47
+ confirmed_at: Time.current
48
+ end
49
+
50
+ def register_audit!
51
+ ::Booth::Audits::Register::CompletedOnboarding.call(
52
+ credential: onboarding.credential,
53
+ ip:,
54
+ agent:
55
+ )
56
+ end
57
+
58
+ def finalize_onboarding!
59
+ onboarding.update! propagated_at: Time.current
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,68 @@
1
+ module Booth
2
+ module Onboardings
3
+ class Step
4
+ include ::Booth::MethodObject
5
+
6
+ param :onboarding
7
+
8
+ def call
9
+ return :timed_out unless onboarding.recently_created?
10
+
11
+ if onboarding.mode_first_time?
12
+ mode_first_time
13
+ elsif onboarding.mode_username_and_password?
14
+ mode_username_and_password
15
+ elsif onboarding.mode_username_password_and_otp?
16
+ mode_username_password_and_otp
17
+ elsif onboarding.mode_username_password_and_webauth?
18
+ mode_username_password_and_webauth
19
+ elsif onboarding.mode_username_and_webauth?
20
+ mode_username_and_webauth
21
+ else
22
+ raise 'Invalid Onboarding State'
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def mode_first_time
29
+ :choose_mode
30
+ end
31
+
32
+ def mode_username_and_password
33
+ return :choose_password if onboarding.password_chosen_at.blank?
34
+ return :completed if onboarding.password_confirmed_at.present?
35
+
36
+ :confirm_password
37
+ end
38
+
39
+ def mode_username_password_and_otp
40
+ return mode_username_and_password unless mode_username_and_password == :completed
41
+ return :register_otp if onboarding.otp_registered_at.blank?
42
+ return :confirm_otp if onboarding.otp_confirmed_at.blank?
43
+
44
+ :completed
45
+ end
46
+
47
+ def mode_username_password_and_webauth
48
+ return mode_username_and_password unless mode_username_and_password == :completed
49
+
50
+ mode_username_and_webauth
51
+ end
52
+
53
+ def mode_username_and_webauth
54
+ return :register_webauth if onboarding.authenticator_id.blank? ||
55
+ onboarding.authenticator_public_key.blank? ||
56
+ onboarding.authenticator_sign_count.blank?
57
+ return :choose_webauth_nickname if onboarding.authenticator_nickname.blank?
58
+ return :confirm_webauth if onboarding.authenticator_confirmed_at.blank?
59
+
60
+ :completed
61
+ end
62
+
63
+ def mode_unknown
64
+ :unknown
65
+ end
66
+ end
67
+ end
68
+ end