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,47 @@
1
+ module Booth
2
+ module Test
3
+ module Userland
4
+ module Onboardings
5
+ class Timeout
6
+ include ::Booth::MethodObject
7
+ include ::Booth::Test::Helpers
8
+ include ActiveSupport::Testing::TimeHelpers
9
+
10
+ option :page
11
+ option :scope
12
+ option :show_onboarding_path
13
+
14
+ def call
15
+ # Setup
16
+ freeze_time
17
+
18
+ credential = ::Booth::Models::Credential.create!(
19
+ username: 'alice',
20
+ password: 'qwrasfyxv',
21
+ scope:,
22
+ mode: :username_and_password,
23
+ allowed_modes: %i[username_and_password username_and_webauth]
24
+ )
25
+
26
+ onboarding = ::Booth::Models::Onboarding.create!(
27
+ credential_id: credential.id,
28
+ mode: :first_time
29
+ )
30
+
31
+ # Onboarding
32
+
33
+ travel 7.days - 1.minute
34
+
35
+ page.visit show_onboarding_path.sub('ID', onboarding.secret_key)
36
+ assert_userland_partial controller: :onboarding, step: :choose_mode
37
+
38
+ travel 2.minutes
39
+
40
+ page.visit show_onboarding_path.sub('ID', onboarding.secret_key)
41
+ assert_userland_partial controller: :onboarding, step: :timed_out
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,86 @@
1
+ module Booth
2
+ module Test
3
+ module Userland
4
+ module Otps
5
+ class Manage
6
+ include ::Booth::MethodObject
7
+ include ::Booth::Test::Helpers
8
+
9
+ option :page
10
+ option :scope
11
+ option :new_login_path
12
+ option :show_otp_path
13
+ option :after_credential, default: -> {}
14
+
15
+ def call
16
+ # Setup
17
+
18
+ credential = ::Booth::Models::Credential.create!(
19
+ username: 'alice',
20
+ password: 'qwrasfyxv',
21
+ scope:,
22
+ mode: :username_and_password,
23
+ allowed_modes: %i[username_and_password username_password_and_otp]
24
+ )
25
+ after_credential&.call(credential.id)
26
+
27
+ # Login
28
+
29
+ page.visit new_login_path
30
+
31
+ assert_userland_partial controller: :login, step: :enter_username
32
+ page.fill_in :username, with: 'alice'
33
+ page.click_on :submit
34
+
35
+ assert_userland_partial controller: :login, step: :enter_password
36
+ page.fill_in :password, with: 'qwrasfyxv'
37
+ page.click_on :submit
38
+
39
+ # Add OTP
40
+
41
+ assert_logged_in credential: credential
42
+ page.visit show_otp_path
43
+
44
+ assert_userland_partial controller: :otp, step: :add
45
+ page.click_on :add
46
+
47
+ assert_userland_partial controller: :otp, step: :register
48
+ page.assert_selector '[data-booth=otpqr]'
49
+ page.assert_text 'otpauth://totp/'
50
+ page.click_on :registered
51
+
52
+ # Back one step
53
+
54
+ assert_userland_partial controller: :otp, step: :confirm
55
+ page.click_on :change
56
+
57
+ # Continue adding OTP
58
+
59
+ assert_userland_partial controller: :otp, step: :register
60
+ code = extract_otp_secret_key_and_generate_code
61
+ page.click_on :registered
62
+
63
+ assert_userland_partial controller: :otp, step: :confirm
64
+ page.fill_in :code, with: code
65
+ page.click_on :submit
66
+
67
+ assert_userland_partial controller: :otp, step: :successfully_changed
68
+
69
+ page.visit show_otp_path
70
+ assert_userland_partial controller: :otp, step: :show
71
+ page.assert_selector '[data-booth=otpqr]'
72
+ page.assert_text 'otpauth://totp/'
73
+
74
+ travel 19.minutes
75
+ page.visit show_otp_path
76
+ assert_userland_partial controller: :otp, step: :show
77
+
78
+ travel 2.minutes
79
+ page.visit show_otp_path
80
+ assert_userland_partial controller: :otp, step: :sudo
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,102 @@
1
+ module Booth
2
+ module Test
3
+ module Userland
4
+ module PasswordResets
5
+ class Reset
6
+ include ::Booth::MethodObject
7
+ include ::Booth::Test::Helpers
8
+
9
+ option :page
10
+ option :scope
11
+ option :new_login_path
12
+ option :new_password_reset_path
13
+ option :show_password_reset_path
14
+
15
+ def call
16
+ # Setup
17
+ alice = ::Booth::Models::Credential.create!(
18
+ username: 'alice',
19
+ password: 'qwrasfyxv',
20
+ scope:,
21
+ mode: :username_and_password,
22
+ allowed_modes: %i[username_and_password]
23
+ )
24
+
25
+ bobby = ::Booth::Models::Credential.create!(
26
+ username: 'bobby',
27
+ password: 'qwrasfyxv',
28
+ scope:,
29
+ mode: :username_and_password,
30
+ allowed_modes: %i[username_and_password]
31
+ )
32
+
33
+ bobbys_password_reset = ::Booth::Models::PasswordReset.create!(
34
+ credential: bobby,
35
+ creator_ip: '198.51.100.50'
36
+ )
37
+
38
+ bobbys_other_password_reset = ::Booth::Models::PasswordReset.create!(
39
+ credential: bobby,
40
+ creator_ip: '198.51.100.51'
41
+ )
42
+
43
+ # Request Password Reset
44
+ page.visit new_login_path
45
+ assert_userland_partial controller: :login, step: :enter_username
46
+ page.fill_in :username, with: 'alice'
47
+ page.click_on :submit
48
+ assert_userland_partial controller: :login, step: :enter_password
49
+ page.click_on :forgot
50
+ assert_userland_partial controller: :password_reset, step: :new
51
+ page.fill_in :email, with: 'alice@example.com'
52
+ page.click_on :submit
53
+ assert_userland_partial controller: :password_reset, step: :check_your_mail
54
+
55
+ # Forgot Login password
56
+ page.visit new_login_path
57
+ assert_userland_partial controller: :login, step: :enter_password
58
+ page.fill_in :password, with: 'qwrasfyxv'
59
+ page.click_on :submit
60
+ assert_logged_in credential: alice
61
+ page.visit new_password_reset_path
62
+ assert_userland_partial controller: :password_reset, step: :logout_first
63
+
64
+ # Reset password as wrong user
65
+ page.visit show_password_reset_path.sub('ID', bobbys_password_reset.secret_key)
66
+ assert_userland_partial controller: :password_reset, step: :wrong_user_logged_in
67
+ page.click_on :logout
68
+ assert_logged_out
69
+
70
+ # Timed out
71
+ travel 2.hours + 1.second
72
+ page.visit show_password_reset_path.sub('ID', bobbys_password_reset.secret_key)
73
+ assert_userland_partial controller: :password_reset, step: :timed_out
74
+ travel_back
75
+
76
+ # Reset password
77
+ page.visit show_password_reset_path.sub('ID', bobbys_password_reset.secret_key)
78
+ assert_userland_partial controller: :password_reset, step: :choose_password
79
+ page.fill_in :password, with: 'wetsdgxcb'
80
+ page.click_on :submit
81
+ page.click_on :change
82
+ assert_userland_partial controller: :password_reset, step: :choose_password
83
+ page.fill_in :password, with: 'rtufgjvbm'
84
+ page.click_on :submit
85
+ assert_userland_partial controller: :password_reset, step: :confirm_password
86
+ page.fill_in :password, with: 'rtufgjvbm'
87
+ page.click_on :submit
88
+ assert_userland_partial controller: :password_reset, step: :completed
89
+
90
+ # Refresh page
91
+ page.visit show_password_reset_path.sub('ID', bobbys_password_reset.secret_key)
92
+ assert_userland_partial controller: :password_reset, step: :completed
93
+
94
+ # Try a revoked token
95
+ page.visit show_password_reset_path.sub('ID', bobbys_other_password_reset.secret_key)
96
+ assert_userland_partial controller: :password_reset, step: :revoked
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,38 @@
1
+ module Booth
2
+ module Test
3
+ module Userland
4
+ class Scenario
5
+ attr_accessor :page, :scope, :new_login_path, :show_onboarding_path, :show_otp_path, :new_password_reset_path,
6
+ :show_password_reset_path, :after_credential
7
+
8
+ def initialize(klass)
9
+ @klass = klass
10
+ end
11
+
12
+ def name
13
+ @klass.to_s.underscore.parameterize(separator: '_')
14
+ end
15
+
16
+ def call
17
+ arguments = @klass.dry_initializer.public_attributes(self)
18
+ @klass.call(**arguments)
19
+ end
20
+ end
21
+
22
+ def self.scenarios(skip_password_resets: false) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
23
+ yield Scenario.new(Booth::Test::Userland::Logins::MissingAuthenticators)
24
+ yield Scenario.new(Booth::Test::Userland::Logins::MissingOnboarding)
25
+ yield Scenario.new(Booth::Test::Userland::Logins::UsernameAndPassword)
26
+ yield Scenario.new(Booth::Test::Userland::Logins::UsernameAndWebauth)
27
+ yield Scenario.new(Booth::Test::Userland::Logins::UsernamePasswordAndOtp)
28
+ yield Scenario.new(Booth::Test::Userland::Logins::UsernamePasswordAndWebauth)
29
+ yield Scenario.new(Booth::Test::Userland::Onboardings::AlreadyLoggedIn)
30
+ yield Scenario.new(Booth::Test::Userland::Onboardings::Otp)
31
+ yield Scenario.new(Booth::Test::Userland::Onboardings::Password)
32
+ yield Scenario.new(Booth::Test::Userland::Onboardings::Timeout)
33
+ yield Scenario.new(Booth::Test::Userland::Otps::Manage)
34
+ yield Scenario.new(Booth::Test::Userland::PasswordResets::Reset) unless skip_password_resets
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,17 @@
1
+ module Booth
2
+ module Test
3
+ module Webauthn
4
+ class Disable
5
+ include ::Booth::Logging
6
+ include ::Booth::MethodObject
7
+
8
+ option :devtools
9
+
10
+ def call
11
+ debug { 'Disabling Chrome Virtual Authenticator Environment...' }
12
+ devtools.send_cmd 'WebAuthn.disable'
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,19 @@
1
+ module Booth
2
+ module Test
3
+ module Webauthn
4
+ class Enable
5
+ include ::Booth::Logging
6
+ include ::Booth::MethodObject
7
+
8
+ option :devtools
9
+
10
+ def call
11
+ WebAuthn.configuration.origin = Capybara.current_session.server.base_url
12
+
13
+ debug { 'Ensuring enabled Chrome Virtual Authenticator Environment...' }
14
+ devtools.send_cmd 'WebAuthn.enable'
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,38 @@
1
+ module Booth
2
+ module Test
3
+ module Webauthn
4
+ module VirtualAuthenticators
5
+ class Create
6
+ include ::Booth::Logging
7
+ include ::Booth::MethodObject
8
+
9
+ option :page
10
+ option :has_user_verification
11
+
12
+ def call
13
+ options = ::Selenium::WebDriver::VirtualAuthenticatorOptions.new
14
+ options.user_verification = has_user_verification
15
+ options.user_verified = true
16
+
17
+ debug { "Registering Virtual Authenticator... #{options.as_json}" }
18
+ page.driver.browser.add_virtual_authenticator(options)
19
+
20
+ # debug { "Created #{authenticator.id}" }
21
+ end
22
+
23
+ # See https://chromedevtools.github.io/devtools-protocol/tot/WebAuthn/#type-VirtualAuthenticatorOptions
24
+ # def options
25
+ # {
26
+ # protocol: :ctap2,
27
+ # transport: :internal,
28
+ # hasResidentKey: false, # Chrome should not have to reveal a list of existing virtual authenticator IDs.
29
+ # # isUserConsenting: true, # Not sure, this option exists in selenium but not in chrome?
30
+ # hasUserVerification: has_user_verification,
31
+ # isUserVerified: true,
32
+ # }
33
+ # end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,20 @@
1
+ module Booth
2
+ module Test
3
+ module Webauthn
4
+ module VirtualAuthenticators
5
+ class Destroy
6
+ include ::Booth::Logging
7
+ include ::Booth::MethodObject
8
+
9
+ option :devtools
10
+ option :id
11
+
12
+ def call
13
+ debug { "Removing Virtual Authenticator with ID #{id}" }
14
+ devtools.send_cmd 'WebAuthn.removeVirtualAuthenticator', authenticatorId: id
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
data/lib/booth/test.rb ADDED
@@ -0,0 +1,53 @@
1
+ # Just in case
2
+ raise 'Requiring `booth/test` is only intended for testing your code' unless Rails.env.test?
3
+
4
+ def try_to_load_gem(gem_name, require_name = nil)
5
+ require_name = gem_name if require_name.nil?
6
+ require require_name
7
+ rescue LoadError => e
8
+ raise "Please add the `#{gem_name}` gem to your test group (NOT development or production) - #{e.message}"
9
+ end
10
+
11
+ # Gems
12
+ try_to_load_gem 'capybara-lockstep'
13
+ try_to_load_gem 'rack_session_access'
14
+ try_to_load_gem 'selenium-devtools', 'selenium/devtools'
15
+ try_to_load_gem 'selenium-webdriver'
16
+ require 'rack_session_access/capybara'
17
+
18
+ # Internal Helpers
19
+ require_relative 'test/helpers'
20
+ require_relative 'test/support/force_login'
21
+
22
+ # Tests
23
+ require_relative 'test/userland/logins/missing_authenticators'
24
+ require_relative 'test/userland/logins/missing_onboarding'
25
+ require_relative 'test/userland/logins/username_and_password'
26
+ require_relative 'test/userland/logins/username_and_webauth'
27
+ require_relative 'test/userland/logins/username_password_and_otp'
28
+ require_relative 'test/userland/logins/username_password_and_webauth'
29
+ require_relative 'test/userland/onboardings/already_logged_in'
30
+ require_relative 'test/userland/onboardings/otp'
31
+ require_relative 'test/userland/onboardings/password'
32
+ require_relative 'test/userland/onboardings/timeout'
33
+ require_relative 'test/userland/otps/manage'
34
+ require_relative 'test/userland/password_resets/reset'
35
+
36
+ # Public API
37
+ require_relative 'test/userland'
38
+
39
+ module Booth
40
+ module Test
41
+ def self.middleware
42
+ ::RackSessionAccess::Middleware
43
+ end
44
+
45
+ def self.force_login(...)
46
+ ::Booth::Test::Support::ForceLogin.call(...)
47
+ end
48
+ end
49
+ end
50
+
51
+ Capybara.configure do |config|
52
+ config.test_id = 'data-booth' # How Booth interacts with your HTML elements.
53
+ end
@@ -0,0 +1,11 @@
1
+ module Booth
2
+ class ToStruct
3
+ include ::Booth::MethodObject
4
+
5
+ param :attributes
6
+
7
+ def call
8
+ ::Struct.new(*attributes.keys, keyword_init: true).new(**attributes)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,35 @@
1
+ module Booth
2
+ module Userland
3
+ class ExtractFlashMessages
4
+ include ::Booth::Logging
5
+ include ::Booth::MethodObject
6
+
7
+ option :from
8
+ option :to
9
+
10
+ def call
11
+ check_arguments!
12
+ return unless from.respond_to?(:public_message)
13
+ return if from.public_message.blank?
14
+
15
+ if from.success?
16
+ debug { "Saving flash notice: #{from.public_message}" }
17
+ to[:notice] = from.public_message
18
+ else
19
+ debug { "Saving flash alert: #{from.public_message}" }
20
+ to[:alert] = from.public_message
21
+ end
22
+
23
+ nil
24
+ end
25
+
26
+ def check_arguments!
27
+ raise(ArgumentError, 'You can only extract flash messages from something that is not nil') if from.nil?
28
+
29
+ return if to.respond_to?(:notice=)
30
+
31
+ raise ArgumentError, "Please pass in `to: flash` for public flash messages not #{to.inspect}"
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,28 @@
1
+ module Booth
2
+ module Userland
3
+ module Logins
4
+ class Create
5
+ include ::Booth::Concerns::Action
6
+
7
+ def call
8
+ request.must_be_post!
9
+
10
+ do_transition
11
+ end
12
+
13
+ private
14
+
15
+ def transitions
16
+ [
17
+ ::Booth::Logins::Transitions::Create::ChooseUsername,
18
+ ::Booth::Logins::Transitions::Create::EnterOtp,
19
+ ::Booth::Logins::Transitions::Create::SkipRemotes,
20
+ ::Booth::Logins::Transitions::Create::VerifyPassword,
21
+ ::Booth::Logins::Transitions::Create::WebauthAuthenticationInitiation,
22
+ ::Booth::Logins::Transitions::Create::WebauthAuthenticationVerification,
23
+ ]
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,37 @@
1
+ module Booth
2
+ module Userland
3
+ module Logins
4
+ class Destroy
5
+ include ::Booth::Concerns::Action
6
+
7
+ def call
8
+ request.must_be_delete!
9
+ request.must_be_html!
10
+
11
+ if request.authentication.logged_in?
12
+ public_message = I18n.t('booth.successfully_logged_out')
13
+ ::Booth::Audits::Register::Logout.call credential:,
14
+ ip: request.ip,
15
+ agent: request.agent
16
+ end
17
+
18
+ reset!
19
+
20
+ Tron.success :logged_out, return_path: request.return_path,
21
+ public_message:
22
+ end
23
+
24
+ private
25
+
26
+ def credential
27
+ @credential ||= ::Booth::Models::Credential.find(request.authentication.credential_id)
28
+ end
29
+
30
+ def reset!
31
+ request.storage.login.reset
32
+ request.authentication.logout
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,70 @@
1
+ module Booth
2
+ module Userland
3
+ module Logins
4
+ class New
5
+ include ::Booth::Concerns::Action
6
+
7
+ def call
8
+ request.must_be_get!
9
+ request.must_be_html!
10
+
11
+ if already_logged_in?
12
+ # Bail out early if it would conflict with another session.
13
+ # Because this could lead to strong confusion for the end-user.
14
+ ::Booth::Userland::Logins::Transitions::New::AlreadyLoggedIn.call(request:)
15
+
16
+ elsif storage.timed_out?
17
+ # The browser cookie just destroyed itself.
18
+ ::Booth::Userland::Logins::Transitions::New::TimedOut.call(request:)
19
+
20
+ elsif storage.credential_for_username.blank?
21
+ # First of all, we need a username.
22
+ # Without it we wouldn't know which credential to look for.
23
+ ::Booth::Userland::Logins::Transitions::New::NoUsernameChosen.call(request:)
24
+
25
+ elsif storage.credential_for_username.mode_first_time?
26
+ # If that username is not initialized, bail out again.
27
+ # This should be an informative site with remedy information.
28
+ ::Booth::Userland::Logins::Transitions::New::ModeFirstTime.call(request:)
29
+
30
+ elsif remote_session_available?
31
+ # If the user is logged in on another device,
32
+ # suggest that that device is used as a remote to login here.
33
+ ::Booth::Userland::Logins::Transitions::New::RemoteSessionAvailable.call(request:)
34
+
35
+ elsif storage.credential_for_username.mode_username_and_password?
36
+ ::Booth::Userland::Logins::Transitions::New::ModeUsernameAndPassword.call(request:)
37
+
38
+ elsif storage.credential_for_username.mode_username_password_and_otp?
39
+ ::Booth::Userland::Logins::Transitions::New::ModeUsernamePasswordAndOtp.call(request:)
40
+
41
+ elsif storage.credential_for_username.mode_username_password_and_webauth?
42
+ ::Booth::Userland::Logins::Transitions::New::ModeUsernamePasswordAndWebauth.call(request:)
43
+
44
+ elsif storage.credential_for_username.mode_username_and_webauth?
45
+ ::Booth::Userland::Logins::Transitions::New::ModeUsernameAndWebauth.call(request:)
46
+
47
+ else
48
+ raise 'Invalid Login State'
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ def already_logged_in?
55
+ request.authentication.logged_in?
56
+ end
57
+
58
+ def remote_session_available?
59
+ return false if storage.skip_remotes?
60
+
61
+ storage.credential_for_username.remote_session_available?
62
+ end
63
+
64
+ def storage
65
+ request.storage.login
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,41 @@
1
+ module Booth
2
+ module Logins
3
+ module Transitions
4
+ module Create
5
+ class ChooseUsername
6
+ include ::Booth::Concerns::Transition
7
+
8
+ def self.applicable?(params:)
9
+ params.dig :login, :username
10
+ end
11
+
12
+ def call
13
+ finding = ::Booth::Credentials::FindByUsername.call(username: username_param)
14
+
15
+ finding.on_success do
16
+ # Each time a username was entered, we destroy and recreate the Contest for this Credential.
17
+ contest = ::Booth::Contests::SetForLogin.call(request:, credential_id: finding.credential.id).contest
18
+ storage.contest_for_username = contest
19
+ storage.credential_for_username = finding.credential
20
+ end
21
+
22
+ # Whether the username is valid or not, we persist it in the cookie,
23
+ # so that it can be shown again in the username text input.
24
+ storage.username = finding.normalized_invalid_username
25
+ finding
26
+ end
27
+
28
+ private
29
+
30
+ def username_param
31
+ params.require(:login).permit(:username)[:username]
32
+ end
33
+
34
+ def storage
35
+ request.storage.login
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end