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,86 @@
1
+ module Booth
2
+ module Errors
3
+ class Error < ::StandardError
4
+ end
5
+
6
+ # --------------------
7
+ # Categories of Errors
8
+ # --------------------
9
+
10
+ # Developer integrated Booth in a wrong way.
11
+ class Integration < ::Booth::Errors::Error
12
+ end
13
+
14
+ # Browser submits invalid data.
15
+ class BadRequest < ::Booth::Errors::Error
16
+ end
17
+
18
+ # Something inside of Booth went wrong.
19
+ class Internal < ::Booth::Errors::Error
20
+ end
21
+
22
+ # ------------------------------
23
+ # Developer integration mistakes
24
+ # ------------------------------
25
+
26
+ class InvalidScopeSyntax < ::Booth::Errors::Integration
27
+ def initialize(scope)
28
+ super("Invalid scope name: #{scope.inspect}")
29
+ end
30
+ end
31
+
32
+ class NotAuthenticated < ::Booth::Errors::Integration
33
+ def initialize(message = 'This controller action must only be reachable for authenticated users. ' \
34
+ 'Protect it for example with `before_action :require_authentication`')
35
+ super
36
+ end
37
+ end
38
+
39
+ class MissingRelyingParty < ::Booth::Errors::Integration
40
+ def initialize(message = 'Please configure a name for the relying party in the `webauth` gem. ' \
41
+ "For example `WebAuthn.configuration.rp_name = 'My Homepage'`")
42
+ super
43
+ end
44
+ end
45
+
46
+ # -----------
47
+ # Bad Request
48
+ # -----------
49
+
50
+ class MustBeHtml < ::Booth::Errors::BadRequest
51
+ def initialize(message = 'This feature is only reachable in the format HTML')
52
+ super
53
+ end
54
+ end
55
+
56
+ class MustBeJson < ::Booth::Errors::BadRequest
57
+ def initialize(message = 'This feature is only reachable in the format JSON')
58
+ super
59
+ end
60
+ end
61
+
62
+ class MustBeGet < ::Booth::Errors::BadRequest
63
+ def initialize(message = 'This feature is only reachable via GET')
64
+ super
65
+ end
66
+ end
67
+
68
+ class MustBePost < ::Booth::Errors::BadRequest
69
+ def initialize(message = 'This feature is only reachable via POST')
70
+ super
71
+ end
72
+ end
73
+
74
+ class MustBePatch < ::Booth::Errors::BadRequest
75
+ def initialize(message = 'This feature is only reachable via PATCH')
76
+ super
77
+ end
78
+ end
79
+
80
+ class MustBeDelete < ::Booth::Errors::BadRequest
81
+ def initialize(message = 'This feature is only reachable via DELETE')
82
+ super
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,20 @@
1
+ module Booth
2
+ module Geolocation
3
+ def self.lookup(ip)
4
+ return 'localhost' if ip.to_s == '127.0.0.1' || ip.to_s == '::1'
5
+
6
+ record = reader&.city(ip.to_s)
7
+ (record&.city || record&.subdivision || record&.country || record&.continent)&.name
8
+ end
9
+
10
+ def self.reader
11
+ return unless defined?(MaxMind::GeoIP2::Reader)
12
+
13
+ @reader ||= MaxMind::GeoIP2::Reader.new(database: database_path.to_s)
14
+ end
15
+
16
+ def self.database_path
17
+ Rails.root.join('db/GeoLite2-City.mmdb')
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,54 @@
1
+ module Booth
2
+ module Hooks
3
+ class AfterFetch
4
+ include ::Booth::MethodObject
5
+ include ::Booth::Logging
6
+
7
+ option :passport
8
+ option :warden
9
+ option :options
10
+
11
+ def call
12
+ if passport.revoked_at
13
+ debug { "Your session in scope #{scope.inspect} was revoked at #{passport.revoked_at}. Logging you out." }
14
+ request.authentication.logout
15
+ else
16
+ debug { "Registering activity of legitimate session in scope #{scope.inspect} with IP #{request.ip}..." }
17
+ register_activity
18
+ end
19
+
20
+ nil
21
+ end
22
+
23
+ private
24
+
25
+ def register_activity
26
+ ::Booth::Models::Session.where(id: passport.id).update_all(
27
+ ['activity_at = ?, ' \
28
+ 'agent = ?, ' \
29
+ 'location = ?, ' \
30
+ 'most_recent_ip = ?, ' \
31
+ 'historical_locations = historical_locations || hstore(?, ?), ' \
32
+ 'historical_ips = historical_ips || hstore(?, ?)',
33
+ Time.current,
34
+ request.agent,
35
+ request.location,
36
+ request.ip,
37
+ request.ip,
38
+ request.location,
39
+ request.ip,
40
+ Time.current.to_i.to_s]
41
+ )
42
+ end
43
+
44
+ def request
45
+ @request ||= ::Booth::Request.new(request: warden.request,
46
+ scope:)
47
+ end
48
+
49
+ def scope
50
+ options[:scope]
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,29 @@
1
+ module Booth
2
+ module Hooks
3
+ class BeforeLogout
4
+ include ::Booth::MethodObject
5
+ include ::Booth::Logging
6
+
7
+ option :passport
8
+ option :warden
9
+ option :options
10
+
11
+ def call
12
+ return unless passport # Attempting to logout when already logged out.
13
+
14
+ debug { "Revoking Session with ID #{passport.id} in scope #{scope.inspect} because of logout" }
15
+
16
+ ::Booth::Models::Session.where(id: passport.id)
17
+ .update_all(revoked_at: Time.current, revoke_reason: :logout)
18
+
19
+ nil
20
+ end
21
+
22
+ private
23
+
24
+ def scope
25
+ options[:scope]
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,24 @@
1
+ module Booth
2
+ module Hooks
3
+ class SerializeFromSession
4
+ include ::Booth::MethodObject
5
+ include ::Booth::Logging
6
+
7
+ param :session_id
8
+
9
+ def call
10
+ ::Booth::Syntaxes::Uuid.call(session_id, raise_if_invalid: true)
11
+
12
+ session = ::Booth::Models::Session.active_scope.find_by(id: session_id)
13
+
14
+ unless session
15
+ debug { "Session ID #{session_id.inspect} stored in the cookie, but doesn't exist in the database" }
16
+ return
17
+ end
18
+
19
+ debug { "Deserializing Session #{session_id.inspect} information into Warden..." }
20
+ ::Booth::Sessions::ToPassport.call(session)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,14 @@
1
+ module Booth
2
+ module Hooks
3
+ class SerializeIntoSession
4
+ include ::Booth::MethodObject
5
+
6
+ param :passport
7
+
8
+ def call
9
+ # This is the ID of the `Booth::Models::Session`.
10
+ passport.id
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,41 @@
1
+ module Booth
2
+ # TODO: Not sure we need this. Do we need this, or can `Logging` do the delegation
3
+ # to the upstream logger by itself?
4
+ class Logger
5
+ def initialize(thing)
6
+ @name = if thing == Class || thing.is_a?(Module) || thing.is_a?(String)
7
+ thing.to_s
8
+ else
9
+ thing.class.to_s
10
+ end
11
+ end
12
+
13
+ def debug(&)
14
+ logger&.debug(name, &)
15
+ end
16
+
17
+ def info(&)
18
+ logger&.info(name, &)
19
+ end
20
+
21
+ def warn(&)
22
+ logger&.warn(name, &)
23
+ end
24
+
25
+ def error(&)
26
+ logger&.error(name, &)
27
+ end
28
+
29
+ def fatal(&)
30
+ logger&.fatal(name, &)
31
+ end
32
+
33
+ private
34
+
35
+ attr_reader :name
36
+
37
+ def logger
38
+ ::Booth.config.logger
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,59 @@
1
+ module Booth
2
+ module Logging
3
+ extend ActiveSupport::Concern
4
+
5
+ # TODO: Implement only generic `log` method to avoid cluttering the Object where this was included.
6
+ # We only use `debug` anyway.
7
+ class_methods do
8
+ def debug(&)
9
+ _logger.debug(&)
10
+ end
11
+
12
+ def info(&)
13
+ _logger.info(&)
14
+ end
15
+
16
+ def warn(&)
17
+ _logger.warn(&)
18
+ end
19
+
20
+ def error(&)
21
+ _logger.error(&)
22
+ end
23
+
24
+ def fatal(&)
25
+ _logger.fatal(&)
26
+ end
27
+
28
+ def _logger
29
+ ::Booth::Logger.new(self)
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def debug(&)
36
+ _logger.debug(&)
37
+ end
38
+
39
+ def info(&)
40
+ _logger.info(&)
41
+ end
42
+
43
+ def warn(&)
44
+ _logger.warn(&)
45
+ end
46
+
47
+ def error(&)
48
+ _logger.error(&)
49
+ end
50
+
51
+ def fatal(&)
52
+ _logger.fatal(&)
53
+ end
54
+
55
+ def _logger
56
+ @_logger ||= ::Booth::Logger.new(self)
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,73 @@
1
+ # Copyright 2018 Bukowskis https://github.com/bukowskis/method_object
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining
4
+ # a copy of this software and associated documentation files (the
5
+ # "Software"), to deal in the Software without restriction, including
6
+ # without limitation the rights to use, copy, modify, merge, publish,
7
+ # distribute, sublicense, and/or sell copies of the Software, and to
8
+ # permit persons to whom the Software is furnished to do so, subject to
9
+ # the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be
12
+ # included in all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
22
+ module Booth
23
+ module MethodObject
24
+ def self.included(base)
25
+ base.extend Dry::Initializer
26
+ base.extend ClassMethods
27
+ base.send(:private_class_method, :new)
28
+ end
29
+
30
+ module ClassMethods
31
+ def call(*args, **kwargs, &)
32
+ __check_for_unknown_options(*args, **kwargs)
33
+
34
+ if kwargs.empty?
35
+ # Preventing `Passing the keyword argument as the last hash parameter is deprecated`
36
+ new(*args).call(&)
37
+ else
38
+ new(*args, **kwargs).call(&)
39
+ end
40
+ end
41
+
42
+ # Overriding the implementation of `#param` in the `dry-initializer` gem.
43
+ # Because of the positioning of multiple params, params can never be omitted in a method object.
44
+ def param(name, type = nil, **opts, &)
45
+ raise ArgumentError, "Default value for param not allowed - #{name}" if opts.key? :default
46
+ raise ArgumentError, "Optional params not supported - #{name}" if opts.fetch(:optional, false)
47
+
48
+ super
49
+ end
50
+
51
+ def __check_for_unknown_options(*args, **kwargs)
52
+ return if __defined_options.empty?
53
+
54
+ # Checking params
55
+ opts = args.drop(__defined_params.length).first || kwargs
56
+ raise ArgumentError, "Unexpected argument #{opts}" unless opts.is_a? Hash
57
+
58
+ # Checking options
59
+ unknown_options = opts.keys - __defined_options
60
+ message = "Key(s) #{unknown_options} not found in #{__defined_options}"
61
+ raise KeyError, message if unknown_options.any?
62
+ end
63
+
64
+ def __defined_options
65
+ dry_initializer.options.map(&:source)
66
+ end
67
+
68
+ def __defined_params
69
+ dry_initializer.params.map(&:source)
70
+ end
71
+ end
72
+ end
73
+ end
data/lib/booth/mode.rb ADDED
@@ -0,0 +1,22 @@
1
+ module Booth
2
+ module Mode
3
+ def self.find(symbol)
4
+ return if symbol.to_s == 'first_time'
5
+
6
+ all.detect { _1.id.to_s == symbol.to_s } || raise("Unknown Booth Mode: #{symbol.inspect}")
7
+ end
8
+
9
+ def self.wrap(input)
10
+ Array(input).map { find(_1) }.sort
11
+ end
12
+
13
+ def self.all
14
+ [
15
+ ::Booth::Modes::UsernameAndWebauth, # Recommended for convenience
16
+ ::Booth::Modes::UsernamePasswordAndWebauth, # Most secure
17
+ ::Booth::Modes::UsernamePasswordAndOtp, # If you don't have a hardware key
18
+ ::Booth::Modes::UsernameAndPassword, # Discouraged
19
+ ]
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,7 @@
1
+ module Booth
2
+ module Models
3
+ class ApplicationRecord < ::ActiveRecord::Base
4
+ self.abstract_class = true
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,24 @@
1
+ module Booth
2
+ module Models
3
+ class Audit < ::Booth::Models::ApplicationRecord
4
+ self.table_name = 'booth_audits'
5
+
6
+ enum event: { added_otp: 'added_otp',
7
+ changed_otp: 'changed_otp',
8
+ completed_onboarding: 'completed_onboarding',
9
+ entered_correct_password: 'entered_correct_password',
10
+ entered_wrong_otp: 'entered_wrong_otp',
11
+ entered_wrong_password: 'entered_wrong_password',
12
+ logout: 'logout',
13
+ queried_unknown_username: 'queried_unknown_username',
14
+ requested_password_reset: 'requested_password_reset' },
15
+ _prefix: true
16
+
17
+ belongs_to :credential, class_name: '::Booth::Models::Credential', optional: true
18
+
19
+ validates :ip, :event, presence: true
20
+
21
+ scope :visible_scope, -> { where(deleted_at: nil) }
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,45 @@
1
+ module Booth
2
+ module Models
3
+ class Authenticator < ::Booth::Models::ApplicationRecord
4
+ include ::Booth::Logging
5
+
6
+ self.table_name = 'booth_authenticators'
7
+
8
+ belongs_to :credential, class_name: '::Booth::Models::Credential'
9
+
10
+ validates :credential_id, uniqueness: { scope: :webauthn_id }
11
+ validates :webauthn_id, presence: true, uniqueness: true
12
+ validates :nickname, length: { minimum: 3, maximum: 40 }, allow_blank: true
13
+
14
+ before_validation :ensure_webauthn_id
15
+ # before_validation :derive_registration_challenge
16
+ # before_destroy :keep_at_least_one_authenticator
17
+
18
+ scope :registered_scope, -> { where.not(confirmed_at: nil) }
19
+ scope :supports_user_verification_scope, -> { where(supports_user_verification: true) }
20
+ scope :sorted_scope, -> { order(:nickname) }
21
+
22
+ def generate_webauth_id
23
+ self.webauthn_id = ::WebAuthn.generate_user_id
24
+ end
25
+
26
+ def confirmed?
27
+ confirmed_at.present?
28
+ end
29
+
30
+ def step
31
+ ::Booth::Authenticators::Step.call(self)
32
+ end
33
+
34
+ private
35
+
36
+ # delegate :recommended_user_verification, to: :credential
37
+
38
+ def ensure_webauthn_id
39
+ return if webauthn_id.present?
40
+
41
+ generate_webauth_id
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,50 @@
1
+ module Booth
2
+ module Models
3
+ module Concerns
4
+ module Modeable
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ enum mode: { first_time: 'first_time',
9
+ username_and_password: 'username_and_password',
10
+ username_password_and_otp: 'username_password_and_otp',
11
+ username_password_and_webauth: 'username_password_and_webauth',
12
+ username_and_webauth: 'username_and_webauth' },
13
+ _prefix: true
14
+
15
+ validates :mode, presence: true, inclusion: { in: modes.keys }
16
+ validates :mode, inclusion: { in: -> { ['first_time'] + _1.allowed_modes } }
17
+
18
+ before_validation :ensure_mode
19
+ end
20
+
21
+ def requires_user_verification?
22
+ # Passwordless login should always require user presence
23
+ !mode_username_password_and_webauth?
24
+ end
25
+
26
+ def allows_mode_username_and_password?
27
+ allowed_modes.include?('username_and_password')
28
+ end
29
+
30
+ def allows_mode_username_password_and_otp?
31
+ allowed_modes.include?('username_password_and_otp')
32
+ end
33
+
34
+ def allows_mode_username_password_and_webauth?
35
+ allowed_modes.include?('username_password_and_webauth')
36
+ end
37
+
38
+ def allows_mode_username_and_webauth?
39
+ allowed_modes.include?('username_and_webauth')
40
+ end
41
+
42
+ private
43
+
44
+ def ensure_mode
45
+ self.mode ||= 'first_time'
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,37 @@
1
+ module Booth
2
+ module Models
3
+ module Concerns
4
+ module Otpable
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ # See https://github.com/heapsource/active_model_otp/blob/master/lib/active_model/one_time_password.rb
9
+ has_one_time_password length: otp_digits
10
+ end
11
+
12
+ class_methods do
13
+ def otp_digits
14
+ 6
15
+ end
16
+ end
17
+
18
+ def authenticate_otp(code)
19
+ super(code, drift: 30.seconds)
20
+ end
21
+
22
+ def otp_provisioning_url
23
+ raise "Expected #{self} to respond to #scope" unless respond_to?(:scope)
24
+
25
+ provisioning_uri(username, issuer: ::Booth.config.otp_issuer(scope:), digits: otp_digits)
26
+ end
27
+
28
+ def otp_provisioning_svg
29
+ qr = ::RQRCode::QRCode.new(otp_provisioning_url, level: :l)
30
+ # See https://whomwah.github.io/rqrcode/
31
+ qr.as_svg use_path: false,
32
+ viewbox: true, fill: 'fff', offset: 5, module_size: 8, svg_attributes: { 'data-booth' => :otpqr }
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,58 @@
1
+ module Booth
2
+ module Models
3
+ module Concerns
4
+ module Passwordable
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ # See https://github.com/rails/rails/blob/master/activemodel/lib/active_model/secure_password.rb
9
+ has_secure_password
10
+
11
+ define_method('password=') do |unencrypted_password|
12
+ # We auto-assign a random password before validation.
13
+ # If a non-empty password was explicitly assigned, then we should not generate a random password.
14
+ @apparently_wants_no_password = unencrypted_password.blank?
15
+
16
+ # See https://github.com/rails/rails/issues/34348#issuecomment-615856794
17
+ super(unencrypted_password.presence)
18
+ end
19
+
20
+ validates :password, length: { minimum: password_minlength }, allow_blank: true
21
+ # Don't say anything about breaches if the password was already too short to be accepted.
22
+ validates :password, not_pwned: {
23
+ on_error: :valid,
24
+ request_options: {
25
+ read_timeout: 3,
26
+ open_timeout: 1
27
+ },
28
+ },
29
+ allow_blank: true,
30
+ if: proc { _1.errors.empty? }
31
+
32
+ before_validation :ensure_initial_password
33
+ end
34
+
35
+ class_methods do
36
+ def password_minlength
37
+ 8
38
+ end
39
+
40
+ # See https://support.1password.com/compatible-website-design
41
+ # See https://developer.apple.com/password-rules
42
+ def passwordrules
43
+ "minlength: #{password_minlength}; maxlength: #{::ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED};"
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ def ensure_initial_password
50
+ return if password_digest.present?
51
+ return if @apparently_wants_no_password
52
+
53
+ self.password = ::SecureRandom.alphanumeric(::ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED)
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end