authnz_eleven 0.1.0

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 (237) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +384 -0
  5. data/lib/authnz_eleven/identity.rb +210 -0
  6. data/lib/authnz_eleven/principal.rb +201 -0
  7. data/lib/authnz_eleven/principals.rb +111 -0
  8. data/lib/authnz_eleven/version.rb +5 -0
  9. data/lib/authnz_eleven/wizard.rb +570 -0
  10. data/lib/authnz_eleven.rb +15 -0
  11. data/lib/generators/authnz_eleven/USAGE +26 -0
  12. data/lib/generators/authnz_eleven/authnz_eleven_generator.rb +3040 -0
  13. data/lib/generators/authnz_eleven/templates/config/forbidden_usernames.LICENSE +22 -0
  14. data/lib/generators/authnz_eleven/templates/config/forbidden_usernames.txt +558 -0
  15. data/lib/generators/authnz_eleven/templates/config/initializers/authentication.rb.tt +275 -0
  16. data/lib/generators/authnz_eleven/templates/config/initializers/omniauth.rb.tt +9 -0
  17. data/lib/generators/authnz_eleven/templates/config/initializers/team_slug.rb.tt +24 -0
  18. data/lib/generators/authnz_eleven/templates/config/initializers/webauthn.rb.tt +14 -0
  19. data/lib/generators/authnz_eleven/templates/config/locales/authentication.en.yml.tt +11 -0
  20. data/lib/generators/authnz_eleven/templates/controllers/active_teams_controller.rb.tt +6 -0
  21. data/lib/generators/authnz_eleven/templates/controllers/admin/dashboard_controller.rb.tt +4 -0
  22. data/lib/generators/authnz_eleven/templates/controllers/admin/sessions_controller.rb.tt +17 -0
  23. data/lib/generators/authnz_eleven/templates/controllers/admin/users_controller.rb.tt +91 -0
  24. data/lib/generators/authnz_eleven/templates/controllers/admin_controller.rb.tt +6 -0
  25. data/lib/generators/authnz_eleven/templates/controllers/base_controller.rb.tt +11 -0
  26. data/lib/generators/authnz_eleven/templates/controllers/concerns/api_token_authentication.rb.tt +21 -0
  27. data/lib/generators/authnz_eleven/templates/controllers/concerns/authentication/easy_dev_login.rb.tt +47 -0
  28. data/lib/generators/authnz_eleven/templates/controllers/concerns/authentication/registering.rb.tt +25 -0
  29. data/lib/generators/authnz_eleven/templates/controllers/concerns/authentication/sudoable.rb.tt +117 -0
  30. data/lib/generators/authnz_eleven/templates/controllers/concerns/authentication/webauthn_ceremony.rb.tt +51 -0
  31. data/lib/generators/authnz_eleven/templates/controllers/concerns/authentication.rb.tt +421 -0
  32. data/lib/generators/authnz_eleven/templates/controllers/concerns/authorization.rb.tt +48 -0
  33. data/lib/generators/authnz_eleven/templates/controllers/concerns/captchable.rb.tt +11 -0
  34. data/lib/generators/authnz_eleven/templates/controllers/impersonations_controller.rb.tt +8 -0
  35. data/lib/generators/authnz_eleven/templates/controllers/invitations_controller.rb.tt +65 -0
  36. data/lib/generators/authnz_eleven/templates/controllers/settings/api_tokens_controller.rb.tt +54 -0
  37. data/lib/generators/authnz_eleven/templates/controllers/settings/authentications/events_controller.rb.tt +5 -0
  38. data/lib/generators/authnz_eleven/templates/controllers/settings/dashboard_controller.rb.tt +4 -0
  39. data/lib/generators/authnz_eleven/templates/controllers/settings/email_verifications_controller.rb.tt +15 -0
  40. data/lib/generators/authnz_eleven/templates/controllers/settings/emails_controller.rb.tt +93 -0
  41. data/lib/generators/authnz_eleven/templates/controllers/settings/invitations_controller.rb.tt +61 -0
  42. data/lib/generators/authnz_eleven/templates/controllers/settings/multi_factor_authentication/authenticators_controller.rb.tt +87 -0
  43. data/lib/generators/authnz_eleven/templates/controllers/settings/multi_factor_authentication/recovery_codes_controller.rb.tt +27 -0
  44. data/lib/generators/authnz_eleven/templates/controllers/settings/multi_factor_authentication/security_keys_controller.rb.tt +86 -0
  45. data/lib/generators/authnz_eleven/templates/controllers/settings/multi_factor_authentication/sms_controller.rb.tt +84 -0
  46. data/lib/generators/authnz_eleven/templates/controllers/settings/omniauth_identities_controller.rb.tt +27 -0
  47. data/lib/generators/authnz_eleven/templates/controllers/settings/passkeys_controller.rb.tt +70 -0
  48. data/lib/generators/authnz_eleven/templates/controllers/settings/passwords_controller.rb.tt +74 -0
  49. data/lib/generators/authnz_eleven/templates/controllers/settings/phone_verifications_controller.rb.tt +39 -0
  50. data/lib/generators/authnz_eleven/templates/controllers/settings/phones_controller.rb.tt +88 -0
  51. data/lib/generators/authnz_eleven/templates/controllers/settings/sessions_controller.rb.tt +29 -0
  52. data/lib/generators/authnz_eleven/templates/controllers/settings/users_controller.rb.tt +42 -0
  53. data/lib/generators/authnz_eleven/templates/controllers/settings_controller.rb.tt +5 -0
  54. data/lib/generators/authnz_eleven/templates/controllers/teams_controller.rb.tt +38 -0
  55. data/lib/generators/authnz_eleven/templates/controllers/users/credential_enrollments/passkeys_controller.rb.tt +36 -0
  56. data/lib/generators/authnz_eleven/templates/controllers/users/credential_enrollments/passwords_controller.rb.tt +28 -0
  57. data/lib/generators/authnz_eleven/templates/controllers/users/credential_enrollments_controller.rb.tt +14 -0
  58. data/lib/generators/authnz_eleven/templates/controllers/users/easy_dev_logins_controller.rb.tt +19 -0
  59. data/lib/generators/authnz_eleven/templates/controllers/users/email_verifications_controller.rb.tt +36 -0
  60. data/lib/generators/authnz_eleven/templates/controllers/users/magic_link_controller.rb.tt +53 -0
  61. data/lib/generators/authnz_eleven/templates/controllers/users/multi_factor_authentication/challenge/recovery_codes_controller.rb.tt +30 -0
  62. data/lib/generators/authnz_eleven/templates/controllers/users/multi_factor_authentication/challenge/security_keys_controller.rb.tt +34 -0
  63. data/lib/generators/authnz_eleven/templates/controllers/users/multi_factor_authentication/challenge/sms_controller.rb.tt +45 -0
  64. data/lib/generators/authnz_eleven/templates/controllers/users/multi_factor_authentication/challenge/totps_controller.rb.tt +42 -0
  65. data/lib/generators/authnz_eleven/templates/controllers/users/omniauth/registrations_controller.rb.tt +26 -0
  66. data/lib/generators/authnz_eleven/templates/controllers/users/omniauth_controller.rb.tt +139 -0
  67. data/lib/generators/authnz_eleven/templates/controllers/users/passkeys/sessions_controller.rb.tt +18 -0
  68. data/lib/generators/authnz_eleven/templates/controllers/users/password_resets_controller.rb.tt +64 -0
  69. data/lib/generators/authnz_eleven/templates/controllers/users/registrations/email_verifications_controller.rb.tt +53 -0
  70. data/lib/generators/authnz_eleven/templates/controllers/users/registrations/phone_verifications_controller.rb.tt +37 -0
  71. data/lib/generators/authnz_eleven/templates/controllers/users/registrations_controller.rb.tt +112 -0
  72. data/lib/generators/authnz_eleven/templates/controllers/users/sessions_controller.rb.tt +52 -0
  73. data/lib/generators/authnz_eleven/templates/controllers/users/sms_sessions_controller.rb.tt +53 -0
  74. data/lib/generators/authnz_eleven/templates/controllers/users/sudos_controller.rb.tt +42 -0
  75. data/lib/generators/authnz_eleven/templates/erb/admin/dashboard/index.html.erb.tt +9 -0
  76. data/lib/generators/authnz_eleven/templates/erb/admin/sessions/index.html.erb.tt +27 -0
  77. data/lib/generators/authnz_eleven/templates/erb/admin/users/index.html.erb.tt +32 -0
  78. data/lib/generators/authnz_eleven/templates/erb/admin/users/show.html.erb.tt +70 -0
  79. data/lib/generators/authnz_eleven/templates/erb/invitations/show.html.erb.tt +9 -0
  80. data/lib/generators/authnz_eleven/templates/erb/settings/api_tokens/create.html.erb.tt +7 -0
  81. data/lib/generators/authnz_eleven/templates/erb/settings/api_tokens/index.html.erb.tt +29 -0
  82. data/lib/generators/authnz_eleven/templates/erb/settings/authentications/events/index.html.erb.tt +18 -0
  83. data/lib/generators/authnz_eleven/templates/erb/settings/dashboard/index.html.erb.tt +75 -0
  84. data/lib/generators/authnz_eleven/templates/erb/settings/emails/edit.html.erb.tt +68 -0
  85. data/lib/generators/authnz_eleven/templates/erb/settings/invitations/new.html.erb.tt +31 -0
  86. data/lib/generators/authnz_eleven/templates/erb/settings/multi_factor_authentication/authenticators/new.html.erb.tt +57 -0
  87. data/lib/generators/authnz_eleven/templates/erb/settings/multi_factor_authentication/recovery_codes/create.html.erb.tt +14 -0
  88. data/lib/generators/authnz_eleven/templates/erb/settings/multi_factor_authentication/recovery_codes/index.html.erb.tt +13 -0
  89. data/lib/generators/authnz_eleven/templates/erb/settings/multi_factor_authentication/security_keys/edit.html.erb.tt +14 -0
  90. data/lib/generators/authnz_eleven/templates/erb/settings/multi_factor_authentication/security_keys/index.html.erb.tt +20 -0
  91. data/lib/generators/authnz_eleven/templates/erb/settings/multi_factor_authentication/security_keys/new.html.erb.tt +17 -0
  92. data/lib/generators/authnz_eleven/templates/erb/settings/multi_factor_authentication/sms/new.html.erb.tt +35 -0
  93. data/lib/generators/authnz_eleven/templates/erb/settings/omniauth_identities/index.html.erb.tt +28 -0
  94. data/lib/generators/authnz_eleven/templates/erb/settings/passkeys/edit.html.erb.tt +14 -0
  95. data/lib/generators/authnz_eleven/templates/erb/settings/passkeys/index.html.erb.tt +21 -0
  96. data/lib/generators/authnz_eleven/templates/erb/settings/passkeys/new.html.erb.tt +13 -0
  97. data/lib/generators/authnz_eleven/templates/erb/settings/passwords/edit.html.erb.tt +68 -0
  98. data/lib/generators/authnz_eleven/templates/erb/settings/phone_verifications/new.html.erb.tt +22 -0
  99. data/lib/generators/authnz_eleven/templates/erb/settings/phones/edit.html.erb.tt +58 -0
  100. data/lib/generators/authnz_eleven/templates/erb/settings/sessions/index.html.erb.tt +29 -0
  101. data/lib/generators/authnz_eleven/templates/erb/settings/users/show.html.erb.tt +28 -0
  102. data/lib/generators/authnz_eleven/templates/erb/shared/_authnz_eleven_nav.html.erb.tt +24 -0
  103. data/lib/generators/authnz_eleven/templates/erb/shared/_authnz_eleven_sudo_challenge.html.erb.tt +43 -0
  104. data/lib/generators/authnz_eleven/templates/erb/shared/_captcha.html.erb.tt +8 -0
  105. data/lib/generators/authnz_eleven/templates/erb/teams/index.html.erb.tt +34 -0
  106. data/lib/generators/authnz_eleven/templates/erb/teams/new.html.erb.tt +22 -0
  107. data/lib/generators/authnz_eleven/templates/erb/teams/show.html.erb.tt +5 -0
  108. data/lib/generators/authnz_eleven/templates/erb/user_mailer/email_change_verification.html.erb.tt +7 -0
  109. data/lib/generators/authnz_eleven/templates/erb/user_mailer/invitation.html.erb.tt +11 -0
  110. data/lib/generators/authnz_eleven/templates/erb/user_mailer/magic_link.html.erb.tt +5 -0
  111. data/lib/generators/authnz_eleven/templates/erb/user_mailer/password_reset.html.erb.tt +6 -0
  112. data/lib/generators/authnz_eleven/templates/erb/user_mailer/registration_email_verification.html.erb.tt +7 -0
  113. data/lib/generators/authnz_eleven/templates/erb/user_mailer/security_notification.html.erb.tt +15 -0
  114. data/lib/generators/authnz_eleven/templates/erb/users/credential_enrollments/passkeys/new.html.erb.tt +24 -0
  115. data/lib/generators/authnz_eleven/templates/erb/users/credential_enrollments/passwords/new.html.erb.tt +21 -0
  116. data/lib/generators/authnz_eleven/templates/erb/users/credential_enrollments/show.html.erb.tt +21 -0
  117. data/lib/generators/authnz_eleven/templates/erb/users/email_verifications/show.html.erb.tt +9 -0
  118. data/lib/generators/authnz_eleven/templates/erb/users/magic_link/edit.html.erb.tt +9 -0
  119. data/lib/generators/authnz_eleven/templates/erb/users/magic_link/new.html.erb.tt +22 -0
  120. data/lib/generators/authnz_eleven/templates/erb/users/multi_factor_authentication/challenge/recovery_codes/new.html.erb.tt +20 -0
  121. data/lib/generators/authnz_eleven/templates/erb/users/multi_factor_authentication/challenge/security_keys/new.html.erb.tt +17 -0
  122. data/lib/generators/authnz_eleven/templates/erb/users/multi_factor_authentication/challenge/sms/new.html.erb.tt +25 -0
  123. data/lib/generators/authnz_eleven/templates/erb/users/multi_factor_authentication/challenge/totps/new.html.erb.tt +21 -0
  124. data/lib/generators/authnz_eleven/templates/erb/users/omniauth/registrations/new.html.erb.tt +28 -0
  125. data/lib/generators/authnz_eleven/templates/erb/users/password_resets/edit.html.erb.tt +26 -0
  126. data/lib/generators/authnz_eleven/templates/erb/users/password_resets/new.html.erb.tt +20 -0
  127. data/lib/generators/authnz_eleven/templates/erb/users/registrations/email_verifications/new.html.erb.tt +11 -0
  128. data/lib/generators/authnz_eleven/templates/erb/users/registrations/email_verifications/show.html.erb.tt +34 -0
  129. data/lib/generators/authnz_eleven/templates/erb/users/registrations/new.html.erb.tt +87 -0
  130. data/lib/generators/authnz_eleven/templates/erb/users/registrations/phone_verifications/new.html.erb.tt +46 -0
  131. data/lib/generators/authnz_eleven/templates/erb/users/sessions/new.html.erb.tt +88 -0
  132. data/lib/generators/authnz_eleven/templates/erb/users/sms_sessions/edit.html.erb.tt +30 -0
  133. data/lib/generators/authnz_eleven/templates/erb/users/sms_sessions/new.html.erb.tt +22 -0
  134. data/lib/generators/authnz_eleven/templates/erb/users/sudos/new.html.erb.tt +9 -0
  135. data/lib/generators/authnz_eleven/templates/javascript/controllers/webauthn_controller.js.tt +142 -0
  136. data/lib/generators/authnz_eleven/templates/jobs/authnz_eleven_deferred_job.rb.tt +9 -0
  137. data/lib/generators/authnz_eleven/templates/jobs/authnz_eleven_mail_delivery_job.rb.tt +3 -0
  138. data/lib/generators/authnz_eleven/templates/jobs/sms_delivery_job.rb.tt +11 -0
  139. data/lib/generators/authnz_eleven/templates/lib/captcha/null.rb.tt +9 -0
  140. data/lib/generators/authnz_eleven/templates/lib/captcha/turnstile.rb.tt +39 -0
  141. data/lib/generators/authnz_eleven/templates/lib/captcha.rb.tt +15 -0
  142. data/lib/generators/authnz_eleven/templates/lib/sms/log.rb.tt +8 -0
  143. data/lib/generators/authnz_eleven/templates/lib/sms/twilio.rb.tt +70 -0
  144. data/lib/generators/authnz_eleven/templates/lib/sms.rb.tt +15 -0
  145. data/lib/generators/authnz_eleven/templates/mailers/user_mailer.rb.tt +98 -0
  146. data/lib/generators/authnz_eleven/templates/migrations/add_guest_to_users_migration.rb.tt +6 -0
  147. data/lib/generators/authnz_eleven/templates/migrations/create_api_tokens_migration.rb.tt +14 -0
  148. data/lib/generators/authnz_eleven/templates/migrations/create_events_migration.rb.tt +26 -0
  149. data/lib/generators/authnz_eleven/templates/migrations/create_invitations_migration.rb.tt +20 -0
  150. data/lib/generators/authnz_eleven/templates/migrations/create_memberships_migration.rb.tt +12 -0
  151. data/lib/generators/authnz_eleven/templates/migrations/create_omniauth_identities_migration.rb.tt +16 -0
  152. data/lib/generators/authnz_eleven/templates/migrations/create_password_histories_migration.rb.tt +11 -0
  153. data/lib/generators/authnz_eleven/templates/migrations/create_pending_registrations_migration.rb.tt +31 -0
  154. data/lib/generators/authnz_eleven/templates/migrations/create_recovery_codes_migration.rb.tt +11 -0
  155. data/lib/generators/authnz_eleven/templates/migrations/create_sessions_migration.rb.tt +19 -0
  156. data/lib/generators/authnz_eleven/templates/migrations/create_teams_migration.rb.tt +9 -0
  157. data/lib/generators/authnz_eleven/templates/migrations/create_users_migration.rb.tt +70 -0
  158. data/lib/generators/authnz_eleven/templates/migrations/create_webauthn_credentials_migration.rb.tt +16 -0
  159. data/lib/generators/authnz_eleven/templates/models/api_token.rb.tt +24 -0
  160. data/lib/generators/authnz_eleven/templates/models/concerns/adminable.rb.tt +15 -0
  161. data/lib/generators/authnz_eleven/templates/models/concerns/authenticatable.rb.tt +151 -0
  162. data/lib/generators/authnz_eleven/templates/models/concerns/bannable.rb.tt +24 -0
  163. data/lib/generators/authnz_eleven/templates/models/concerns/deadboltable.rb.tt +39 -0
  164. data/lib/generators/authnz_eleven/templates/models/concerns/email_verifiable.rb.tt +52 -0
  165. data/lib/generators/authnz_eleven/templates/models/concerns/eventable.rb.tt +22 -0
  166. data/lib/generators/authnz_eleven/templates/models/concerns/guestable.rb.tt +30 -0
  167. data/lib/generators/authnz_eleven/templates/models/concerns/invitable.rb.tt +12 -0
  168. data/lib/generators/authnz_eleven/templates/models/concerns/magic_linkable.rb.tt +34 -0
  169. data/lib/generators/authnz_eleven/templates/models/concerns/password_historical.rb.tt +28 -0
  170. data/lib/generators/authnz_eleven/templates/models/concerns/password_policy.rb.tt +38 -0
  171. data/lib/generators/authnz_eleven/templates/models/concerns/password_rotatable.rb.tt +15 -0
  172. data/lib/generators/authnz_eleven/templates/models/concerns/phone_verifiable.rb.tt +50 -0
  173. data/lib/generators/authnz_eleven/templates/models/concerns/principals.rb.tt +67 -0
  174. data/lib/generators/authnz_eleven/templates/models/concerns/recoverable.rb.tt +19 -0
  175. data/lib/generators/authnz_eleven/templates/models/concerns/second_factor.rb.tt +52 -0
  176. data/lib/generators/authnz_eleven/templates/models/concerns/sms_challengeable.rb.tt +38 -0
  177. data/lib/generators/authnz_eleven/templates/models/concerns/team_member.rb.tt +20 -0
  178. data/lib/generators/authnz_eleven/templates/models/current.rb.tt +28 -0
  179. data/lib/generators/authnz_eleven/templates/models/event.rb.tt +32 -0
  180. data/lib/generators/authnz_eleven/templates/models/invitation.rb.tt +122 -0
  181. data/lib/generators/authnz_eleven/templates/models/membership.rb.tt +16 -0
  182. data/lib/generators/authnz_eleven/templates/models/omniauth_identity.rb.tt +12 -0
  183. data/lib/generators/authnz_eleven/templates/models/password_history.rb.tt +3 -0
  184. data/lib/generators/authnz_eleven/templates/models/pending_registration.rb.tt +216 -0
  185. data/lib/generators/authnz_eleven/templates/models/recovery_code.rb.tt +41 -0
  186. data/lib/generators/authnz_eleven/templates/models/session.rb.tt +46 -0
  187. data/lib/generators/authnz_eleven/templates/models/team.rb.tt +12 -0
  188. data/lib/generators/authnz_eleven/templates/models/user.rb.tt +59 -0
  189. data/lib/generators/authnz_eleven/templates/models/webauthn_credential.rb.tt +12 -0
  190. data/lib/generators/authnz_eleven/templates/tasks/authnz_eleven.rake.tt +41 -0
  191. data/lib/generators/authnz_eleven/templates/test_unit/controllers/account_existence_parity_test.rb.tt +158 -0
  192. data/lib/generators/authnz_eleven/templates/test_unit/controllers/admin/access_test.rb.tt +139 -0
  193. data/lib/generators/authnz_eleven/templates/test_unit/controllers/admin/multi_factor_authentication_test.rb.tt +65 -0
  194. data/lib/generators/authnz_eleven/templates/test_unit/controllers/api_token_authentication_test.rb.tt +45 -0
  195. data/lib/generators/authnz_eleven/templates/test_unit/controllers/credential_enrollment_test.rb.tt +84 -0
  196. data/lib/generators/authnz_eleven/templates/test_unit/controllers/guest_access_test.rb.tt +113 -0
  197. data/lib/generators/authnz_eleven/templates/test_unit/controllers/invitations_controller_test.rb.tt +230 -0
  198. data/lib/generators/authnz_eleven/templates/test_unit/controllers/passkey_ceremony_test.rb.tt +126 -0
  199. data/lib/generators/authnz_eleven/templates/test_unit/controllers/password_rotation_test.rb.tt +67 -0
  200. data/lib/generators/authnz_eleven/templates/test_unit/controllers/security_key_ceremony_test.rb.tt +218 -0
  201. data/lib/generators/authnz_eleven/templates/test_unit/controllers/security_notifications_test.rb.tt +39 -0
  202. data/lib/generators/authnz_eleven/templates/test_unit/controllers/session_lifecycle_test.rb.tt +102 -0
  203. data/lib/generators/authnz_eleven/templates/test_unit/controllers/settings/api_tokens_controller_test.rb.tt +61 -0
  204. data/lib/generators/authnz_eleven/templates/test_unit/controllers/settings/authentications/events_controller_test.rb.tt +17 -0
  205. data/lib/generators/authnz_eleven/templates/test_unit/controllers/settings/email_verifications_controller_test.rb.tt +39 -0
  206. data/lib/generators/authnz_eleven/templates/test_unit/controllers/settings/invitations_controller_test.rb.tt +157 -0
  207. data/lib/generators/authnz_eleven/templates/test_unit/controllers/settings/management_test.rb.tt +256 -0
  208. data/lib/generators/authnz_eleven/templates/test_unit/controllers/settings/multi_factor_authentication/management_test.rb.tt +222 -0
  209. data/lib/generators/authnz_eleven/templates/test_unit/controllers/settings/omniauth_identities_controller_test.rb.tt +57 -0
  210. data/lib/generators/authnz_eleven/templates/test_unit/controllers/settings/phone_verifications_controller_test.rb.tt +146 -0
  211. data/lib/generators/authnz_eleven/templates/test_unit/controllers/teams_controller_test.rb.tt +83 -0
  212. data/lib/generators/authnz_eleven/templates/test_unit/controllers/users/email_verifications_controller_test.rb.tt +74 -0
  213. data/lib/generators/authnz_eleven/templates/test_unit/controllers/users/magic_link_controller_test.rb.tt +90 -0
  214. data/lib/generators/authnz_eleven/templates/test_unit/controllers/users/multi_factor_authentication/challenge_test.rb.tt +143 -0
  215. data/lib/generators/authnz_eleven/templates/test_unit/controllers/users/omniauth/registrations_controller_test.rb.tt +138 -0
  216. data/lib/generators/authnz_eleven/templates/test_unit/controllers/users/omniauth_controller_test.rb.tt +97 -0
  217. data/lib/generators/authnz_eleven/templates/test_unit/controllers/users/password_resets_controller_test.rb.tt +58 -0
  218. data/lib/generators/authnz_eleven/templates/test_unit/controllers/users/registrations/email_verifications_controller_test.rb.tt +102 -0
  219. data/lib/generators/authnz_eleven/templates/test_unit/controllers/users/registrations/phone_verifications_controller_test.rb.tt +69 -0
  220. data/lib/generators/authnz_eleven/templates/test_unit/controllers/users/registrations_controller_test.rb.tt +180 -0
  221. data/lib/generators/authnz_eleven/templates/test_unit/controllers/users/sessions_controller_test.rb.tt +124 -0
  222. data/lib/generators/authnz_eleven/templates/test_unit/controllers/users/sms_sessions_controller_test.rb.tt +111 -0
  223. data/lib/generators/authnz_eleven/templates/test_unit/controllers/users/sudos_controller_test.rb.tt +110 -0
  224. data/lib/generators/authnz_eleven/templates/test_unit/fixtures/memberships.yml.tt +13 -0
  225. data/lib/generators/authnz_eleven/templates/test_unit/fixtures/teams.yml.tt +2 -0
  226. data/lib/generators/authnz_eleven/templates/test_unit/fixtures/users.yml.tt +68 -0
  227. data/lib/generators/authnz_eleven/templates/test_unit/mailers/user_mailer_test.rb.tt +88 -0
  228. data/lib/generators/authnz_eleven/templates/test_unit/models/api_token_test.rb.tt +38 -0
  229. data/lib/generators/authnz_eleven/templates/test_unit/models/password_history_test.rb.tt +51 -0
  230. data/lib/generators/authnz_eleven/templates/test_unit/models/session_test.rb.tt +16 -0
  231. data/lib/generators/authnz_eleven/templates/test_unit/models/sms_challengeable_test.rb.tt +108 -0
  232. data/lib/generators/authnz_eleven/templates/test_unit/models/user/guestable_test.rb.tt +78 -0
  233. data/lib/generators/authnz_eleven/templates/test_unit/models/user/recovery_codes_test.rb.tt +32 -0
  234. data/lib/generators/authnz_eleven/templates/test_unit/models/user/sign_in_methods_test.rb.tt +56 -0
  235. data/lib/generators/authnz_eleven/templates/test_unit/models/user_test.rb.tt +145 -0
  236. data/lib/generators/authnz_eleven/wizard_generator.rb +52 -0
  237. metadata +309 -0
@@ -0,0 +1,570 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../authnz_eleven"
4
+
5
+ module AuthnzEleven
6
+ # An interactive flag picker for the main generator.
7
+ #
8
+ # The generator has forty-odd flags and a dozen validators explaining which
9
+ # combinations don't work. Those validators exist because a command line
10
+ # arrives all at once. A wizard asks in dependency order instead, so a
11
+ # combination the generator would refuse is simply never offered: the answers
12
+ # to one screen decide what the next screen contains.
13
+ #
14
+ # That ordering is the whole design. Principals before doors, because a door
15
+ # needs something to deliver to. Verification before the password mode,
16
+ # because `deferred` waits on a proved channel. Teams and admin last, because
17
+ # nothing upstream depends on them.
18
+ class Wizard
19
+ # Flags that turn another one on by themselves, so naming both in the
20
+ # emitted command would be noise.
21
+ IMPLIED = {
22
+ "admin_dashboard" => "adminable",
23
+ "impersonatable" => "adminable"
24
+ }.freeze
25
+
26
+ # The answers are seeded with the recommended explicit principal and the
27
+ # generator's behavioral defaults.
28
+ class Answers
29
+ attr_accessor :user_class, :namespaced,
30
+ :email, :phone, :username,
31
+ :contactable, :verify, :coy, :encrypted,
32
+ :doors, :registration, :permanent, :password_mode, :password_extras,
33
+ :second_factor, :protections,
34
+ :sessions, :max_sessions, :api_tokens, :teams, :admin
35
+
36
+ def initialize
37
+ @user_class = "User"
38
+ @namespaced = false
39
+ @email = "required"
40
+ @phone = "none"
41
+ @username = false
42
+ @contactable = true
43
+ @verify = true
44
+ @coy = false
45
+ @encrypted = false
46
+ @doors = []
47
+ @registration = "open"
48
+ @permanent = []
49
+ @password_mode = "required"
50
+ @password_extras = []
51
+ @second_factor = []
52
+ @protections = []
53
+ @sessions = []
54
+ @max_sessions = "none"
55
+ @api_tokens = false
56
+ @teams = "none"
57
+ @admin = []
58
+ end
59
+
60
+ def email? = email != "none"
61
+ def phone? = phone != "none"
62
+
63
+ # The requiredness words of the channels this build actually has, so the
64
+ # channel rules can be asked of the set rather than of each column.
65
+ def channels = [email, phone].reject { |mode| mode == "none" }
66
+ def channels? = channels.any?
67
+ def optional_channel? = channels.include?("optional")
68
+
69
+ # One channel, and it may be absent: nothing for "optional" to be optional
70
+ # against, so an account could hold no way to be reached at all.
71
+ def sole_optional_channel? = channels == ["optional"]
72
+
73
+ def password? = doors.include?("password")
74
+ def sms_code? = doors.include?("sms_code")
75
+ def other_doors? = doors.any? { |d| d != "password" }
76
+
77
+ # Mirrors the generator's validate_password_deferred!.
78
+ def password_deferrable? = doors.include?("passkey") || (contactable && verify && channels?)
79
+
80
+ # Mirrors the generator's validate_sudoable!.
81
+ def sudo_answerable? = password? || doors.include?("passkey") || second_factor.intersect?(%w[totp webauthn])
82
+
83
+ def email_verifiable? = verify && email?
84
+ def phone_verifiable? = verify && phone?
85
+
86
+ # Mirrors the generator's validate_coy!.
87
+ def coyable? = verify && channels?
88
+
89
+ # "Permanent" removes the settings form for a value, which only reads as a
90
+ # rule when the value was there from the start: every sign-up path collects
91
+ # a required phone before the account exists.
92
+ def phone_permanent? = phone == "required"
93
+ end
94
+
95
+ def initialize
96
+ @answers = Answers.new
97
+ end
98
+
99
+ attr_reader :answers
100
+
101
+ def run
102
+ require "huh"
103
+
104
+ errors = form.run
105
+ raise Error, errors.map(&:message).join("; ") if errors.any?
106
+
107
+ to_flags
108
+ end
109
+
110
+ def to_flags
111
+ [
112
+ *identity_flags,
113
+ *principal_flags,
114
+ *channel_flags,
115
+ *door_flags,
116
+ *feature_flags
117
+ ]
118
+ end
119
+
120
+ def command = ["bin/rails generate authnz_eleven", *to_flags].join(" ")
121
+
122
+ private
123
+
124
+ # Every screen's options are plain [label, value] pairs, so which options a
125
+ # build offers can be asked without a terminal — or the form library — in
126
+ # the room. `menu` is the only place they become Huh objects.
127
+ def menu(pairs) = pairs.map { |label, value| Huh.option(label, value) }
128
+
129
+ EMAIL_MODES = [
130
+ %w[Required required],
131
+ %w[Optional optional],
132
+ %w[None none]
133
+ ].freeze
134
+
135
+ PHONE_MODES = [
136
+ %w[Required required],
137
+ %w[Optional optional],
138
+ %w[None none]
139
+ ].freeze
140
+
141
+ SECOND_FACTORS = [
142
+ ["Authenticator app (TOTP)", "totp"],
143
+ ["Hardware security key (WebAuthn)", "webauthn"],
144
+ ["Texted code (SMS)", "sms"]
145
+ ].freeze
146
+
147
+ PROTECTIONS = [
148
+ ["Captcha on signed-out forms (Cloudflare Turnstile)", "captchable"],
149
+ ["Ask users to authenticate again before dangerous actions (sudo)", "sudoable"],
150
+ ["Email users when security settings change", "security_notifications"]
151
+ ].freeze
152
+
153
+ SESSIONS = [
154
+ ["'Remember me' checkbox", "rememberable"],
155
+ ["Sign out inactive users", "timeoutable"],
156
+ ["Record when users were last seen", "last_seenable"],
157
+ ["Audit trail of authentication activity", "trackable"],
158
+ ["Anonymous guest users", "guestable"],
159
+ ["Sign in as anyone (development only)", "easy_dev_login"]
160
+ ].freeze
161
+
162
+ MAX_SESSIONS = [
163
+ ["No limit", "none"],
164
+ ["Sign out the oldest", "evict"],
165
+ ["Ask the user to sign one out", "prompt"]
166
+ ].freeze
167
+
168
+ TEAMS = [
169
+ ["No teams", "none"],
170
+ ["Team stored in the session", "session"],
171
+ ["Team in the URL (/:team_id)", "scope"],
172
+ ["Team in the URL, via middleware (37signals-style)", "middleware"]
173
+ ].freeze
174
+
175
+ ADMIN = [
176
+ ["Admin users", "adminable"],
177
+ ["An /admin area", "admin_dashboard"],
178
+ ["Admins can impersonate users", "impersonatable"],
179
+ ["Admins can ban users", "bannable"]
180
+ ].freeze
181
+
182
+ def form
183
+ Huh.form(
184
+ identity_group,
185
+ namespaced_group,
186
+ principals_group,
187
+ contactable_group,
188
+ channels_group,
189
+ coy_group,
190
+ doors_group,
191
+ registration_group,
192
+ permanence_group,
193
+ password_group,
194
+ protections_group,
195
+ sessions_group,
196
+ api_tokens_group,
197
+ teams_group,
198
+ admin_group
199
+ ).with_theme(Huh::Themes.base16)
200
+ end
201
+
202
+ # ---- screens -------------------------------------------------------
203
+
204
+ def identity_group
205
+ Huh.group(
206
+ Huh.input
207
+ .key("user_class")
208
+ .title("What is the signed-in model called?")
209
+ .description("The table, routes and controllers are named after it.")
210
+ .placeholder("User")
211
+ .value(answers, :user_class)
212
+ ).title("Identity")
213
+ end
214
+
215
+ # Only a second identity needs a URL prefix, and a second identity is the
216
+ # only reason to name the model something other than User.
217
+ def namespaced_group
218
+ Huh.group(
219
+ Huh.confirm
220
+ .key("namespaced")
221
+ .title("Give this identity its own URL prefix and namespace?")
222
+ .description("Required if your app already has one.")
223
+ .value(answers, :namespaced)
224
+ ).title("Namespace").with_hide_func { answers.user_class == "User" }
225
+ end
226
+
227
+ def principals_group
228
+ Huh.group(
229
+ Huh.select
230
+ .key("email")
231
+ .title("Email")
232
+ .options(*menu(EMAIL_MODES))
233
+ .value(answers, :email),
234
+ Huh.select
235
+ .key("phone")
236
+ .title("Phone")
237
+ .options(*menu(PHONE_MODES))
238
+ .value(answers, :phone),
239
+ Huh.confirm
240
+ .key("username")
241
+ .title("Username?")
242
+ .value(answers, :username)
243
+ .validate { |username| validate_identifier(username) }
244
+ ).title("Identifiers")
245
+ .description("What users sign in with.")
246
+ .with_validate_on_submit(true)
247
+ end
248
+
249
+ # Drop the email column and something else has to name the account at the
250
+ # sign-in form. Asked here rather than left to the generator because the
251
+ # answer is two fields up on the screen the user is already looking at.
252
+ def validate_identifier(username)
253
+ return if answers.email? || username || answers.phone.start_with?("required")
254
+
255
+ raise Huh::ValidationError,
256
+ "without email, add a username or make phone required"
257
+ end
258
+
259
+ # "Optional" only means something when another channel covers the gap.
260
+ # With every channel required the promise is already kept, so don't ask.
261
+ def contactable_group
262
+ Huh.group(
263
+ Huh.confirm
264
+ .key("contactable")
265
+ .title("Must every account have an email or phone?")
266
+ .description("Say no for old-school accounts you can't contact or recover.")
267
+ .value(answers, :contactable)
268
+ .validate { |contactable| validate_contactable(contactable) }
269
+ ).title("Contact")
270
+ .with_hide_func { !answers.optional_channel? }
271
+ .with_validate_on_submit(true)
272
+ end
273
+
274
+ # With a second channel to fall back on, "optional" and "must hold one" agree.
275
+ # With only one, they contradict each other and the flag would be a lie.
276
+ def validate_contactable(contactable)
277
+ return unless contactable && answers.sole_optional_channel?
278
+
279
+ raise Huh::ValidationError,
280
+ "your only email/phone is optional, so answer no or make it required"
281
+ end
282
+
283
+ def channels_group
284
+ Huh.group(
285
+ Huh.confirm
286
+ .key("verify")
287
+ .title("Verify emails and phone numbers?")
288
+ .description("By emailed link or texted code.")
289
+ .value(answers, :verify),
290
+ Huh.confirm
291
+ .key("encrypted")
292
+ .title("Encrypt emails and phone numbers at rest?")
293
+ .value(answers, :encrypted)
294
+ ).title("Email and phone").with_hide_func { !answers.channels? }
295
+ end
296
+
297
+ def coy_group
298
+ Huh.group(
299
+ Huh.confirm
300
+ .key("coy")
301
+ .title("Hide whether an account exists?")
302
+ .description("Forms respond the same whether or not an email or phone is registered.")
303
+ .value(answers, :coy)
304
+ ).title("Account privacy").with_hide_func { !answers.coyable? }
305
+ end
306
+
307
+ def doors_group
308
+ Huh.group(
309
+ Huh.multi_select
310
+ .key("doors")
311
+ .title("How do users sign in?")
312
+ .options_func { menu(door_options) }
313
+ .value(answers, :doors)
314
+ .validate { |chosen| validate_doors(chosen) }
315
+ ).title("Sign-in").with_validate_on_submit(true)
316
+ end
317
+
318
+ # A door needs somewhere to deliver to: magic links ride email, codes ride SMS.
319
+ def door_options
320
+ [
321
+ %w[Password password],
322
+ %w[Passkey passkey],
323
+ ["Social / SSO (OmniAuth)", "omniauth"],
324
+ (["Emailed link", "magic_link"] if answers.email?),
325
+ (["Texted code", "sms_code"] if answers.phone?)
326
+ ].compact
327
+ end
328
+
329
+ # A texted second factor needs a number to text, and is pointless beside the
330
+ # texted door: same proof of the same phone, so whoever holds the number has
331
+ # already opened the door.
332
+ def second_factor_options
333
+ return SECOND_FACTORS if answers.phone? && !answers.sms_code?
334
+
335
+ SECOND_FACTORS.reject { |_, value| value == "sms" }
336
+ end
337
+
338
+ def protection_options
339
+ PROTECTIONS.reject do |_, value|
340
+ value == "sudoable" && !answers.sudo_answerable? ||
341
+ value == "security_notifications" && !answers.email?
342
+ end
343
+ end
344
+
345
+ def validate_doors(chosen)
346
+ raise Huh::ValidationError, "pick at least one way to sign in" if chosen.empty?
347
+
348
+ return unless chosen == ["sms_code"] && answers.phone == "optional"
349
+
350
+ raise Huh::ValidationError,
351
+ "texted codes are the only sign-in method, so make phone required or add another method"
352
+ end
353
+
354
+ # Registration is one choice with four possible account-creation policies.
355
+ # An invitation has to be sent somewhere, so the two invite answers need a
356
+ # channel.
357
+ def registration_group
358
+ Huh.group(
359
+ Huh.select
360
+ .key("registration")
361
+ .title("Who can create an account?")
362
+ .options_func { menu(registration_options) }
363
+ .value(answers, :registration)
364
+ ).title("Registration")
365
+ end
366
+
367
+ def registration_options
368
+ [
369
+ ["Nobody. You create accounts yourself", "closed"],
370
+ %w[Anyone open],
371
+ (["Anyone, and users can invite others", "open-and-invites"] if answers.channels?),
372
+ (["Invited users only", "invite-only"] if answers.channels?)
373
+ ].compact
374
+ end
375
+
376
+ # Asked here, well after the principals themselves, because whether a number
377
+ # CAN be permanent depends on the doors and the registration mode: a build
378
+ # that mints the account before the number arrives has to keep the settings
379
+ # form that permanence removes.
380
+ def permanence_group
381
+ Huh.group(
382
+ Huh.multi_select
383
+ .key("permanent")
384
+ .title("Which can users never change?")
385
+ .description("Admins can still change them.")
386
+ .options_func { menu(permanent_options) }
387
+ .value(answers, :permanent)
388
+ ).title("Permanent").with_hide_func { permanent_options.empty? }
389
+ end
390
+
391
+ def permanent_options
392
+ [
393
+ (%w[Email email] if answers.email == "required"),
394
+ (%w[Phone phone] if answers.phone_permanent?)
395
+ ].compact
396
+ end
397
+
398
+ def password_group
399
+ Huh.group(
400
+ Huh.select
401
+ .key("password_mode")
402
+ .title("Password on the sign-up form")
403
+ .options_func { menu(password_mode_options) }
404
+ .value(answers, :password_mode),
405
+ Huh.multi_select
406
+ .key("password_extras")
407
+ .title("Password features")
408
+ .options_func { menu(password_extra_options) }
409
+ .value(answers, :password_extras)
410
+ ).title("Password").with_hide_func { !answers.password? }
411
+ end
412
+
413
+ # A password reset is an emailed link, so it rides the email channel.
414
+ def password_extra_options
415
+ [
416
+ (["Password reset by email", "recoverable"] if answers.email?),
417
+ ["Reject breached passwords", "pwned"],
418
+ ["Reject weak passwords", "strong_passwords"],
419
+ ["Change passwords every 90 days", "password_rotatable"],
420
+ ["Reject reused passwords", "password_historical"],
421
+ ["Shut password sign-in for a while after too many failures", "deadboltable"]
422
+ ].compact
423
+ end
424
+
425
+ def password_mode_options
426
+ [
427
+ %w[Required required],
428
+ (%w[Optional optional] if answers.other_doors?),
429
+ (["Asked for on a second page", "deferred"] if answers.password_deferrable?)
430
+ ].compact
431
+ end
432
+
433
+ def protections_group
434
+ Huh.group(
435
+ Huh.multi_select
436
+ .key("second_factor")
437
+ .title("Two-factor authentication")
438
+ .options_func { menu(second_factor_options) }
439
+ .value(answers, :second_factor),
440
+ Huh.multi_select
441
+ .key("protections")
442
+ .title("Other protections")
443
+ .options_func { menu(protection_options) }
444
+ .value(answers, :protections)
445
+ ).title("Protections")
446
+ end
447
+
448
+ def sessions_group
449
+ Huh.group(
450
+ Huh.multi_select
451
+ .key("sessions")
452
+ .title("Session features")
453
+ .options(*menu(SESSIONS))
454
+ .value(answers, :sessions),
455
+ Huh.select
456
+ .key("max_sessions")
457
+ .title("Limit sessions per user?")
458
+ .options(*menu(MAX_SESSIONS))
459
+ .value(answers, :max_sessions)
460
+ ).title("Sessions")
461
+ end
462
+
463
+ def api_tokens_group
464
+ Huh.group(
465
+ Huh.confirm
466
+ .key("api_tokens")
467
+ .title("Issue personal access tokens for your application's API?")
468
+ .description("Adds browser settings to manage tokens and a separate concern for API controllers.")
469
+ .value(answers, :api_tokens)
470
+ ).title("API tokens")
471
+ end
472
+
473
+ def teams_group
474
+ Huh.group(
475
+ Huh.select
476
+ .key("teams")
477
+ .title("Teams")
478
+ .options(*menu(TEAMS))
479
+ .value(answers, :teams)
480
+ ).title("Teams")
481
+ end
482
+
483
+ def admin_group
484
+ Huh.group(
485
+ Huh.multi_select
486
+ .key("admin")
487
+ .title("Administration")
488
+ .options(*menu(ADMIN))
489
+ .value(answers, :admin)
490
+ ).title("Administration")
491
+ end
492
+
493
+ # ---- emitting ------------------------------------------------------
494
+
495
+ def identity_flags
496
+ [
497
+ ("--user-class=#{answers.user_class}" unless answers.user_class == "User"),
498
+ ("--namespaced" if answers.namespaced)
499
+ ].compact
500
+ end
501
+
502
+ def principal_flags
503
+ [
504
+ ("--email=#{mode(:email)}" unless mode(:email) == "none"),
505
+ ("--phone=#{mode(:phone)}" unless mode(:phone) == "none"),
506
+ ("--username" if answers.username)
507
+ ].compact
508
+ end
509
+
510
+ def mode(channel)
511
+ value = answers.public_send(channel)
512
+ answers.permanent.include?(channel.to_s) ? "#{value},permanent" : value
513
+ end
514
+
515
+ def channel_flags
516
+ [
517
+ ("--no-verifiable" unless answers.verify),
518
+ ("--coy" if answers.coy && answers.coyable?),
519
+ ("--no-contactable" unless answers.contactable),
520
+ ("--encrypted-pii" if answers.encrypted)
521
+ ].compact
522
+ end
523
+
524
+ def door_flags
525
+ answers.doors.map { |door| door == "password" ? password_flag : flag(door) }
526
+ end
527
+
528
+ def password_flag
529
+ answers.password_mode == "required" ? "--password" : "--password=#{answers.password_mode}"
530
+ end
531
+
532
+ REGISTRATION = {
533
+ "closed" => ["--registration=closed"],
534
+ "open" => [],
535
+ "open-and-invites" => ["--registration=open-and-invites"],
536
+ "invite-only" => ["--registration=invite-only"]
537
+ }.freeze
538
+
539
+ def feature_flags
540
+ [
541
+ *REGISTRATION.fetch(answers.registration),
542
+ *(answers.password? ? answers.password_extras.map { |e| flag(e) } : []),
543
+ ("--second-factor=#{answers.second_factor.join(",")}" if answers.second_factor.any?),
544
+ *answers.protections.map { |p| flag(p) },
545
+ *answers.sessions.map { |s| flag(s) },
546
+ *max_sessions_flag,
547
+ ("--api-tokens" if answers.api_tokens),
548
+ *teams_flag,
549
+ *admin_flags
550
+ ].compact
551
+ end
552
+
553
+ def max_sessions_flag
554
+ return [] if answers.max_sessions == "none"
555
+
556
+ [answers.max_sessions == "evict" ? "--max-sessionable" : "--max-sessionable=prompt"]
557
+ end
558
+
559
+ def teams_flag
560
+ { "none" => [], "session" => ["--teams=session"], "scope" => ["--teams"],
561
+ "middleware" => ["--teams=middleware"] }.fetch(answers.teams)
562
+ end
563
+
564
+ def admin_flags
565
+ (answers.admin - answers.admin.filter_map { |a| IMPLIED[a] }).map { |a| flag(a) }
566
+ end
567
+
568
+ def flag(name) = "--#{name.tr("_", "-")}"
569
+ end
570
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "authnz_eleven/version"
4
+ require_relative "authnz_eleven/principal"
5
+ require_relative "authnz_eleven/principals"
6
+
7
+ module AuthnzEleven
8
+ class Error < StandardError; end
9
+
10
+ # authnz_eleven ships no runtime code: it is simply a Rails generator. The
11
+ # generator is discovered automatically from lib/generators when the gem
12
+ # is in your Gemfile. See:
13
+ #
14
+ # bin/rails generate authnz_eleven --help
15
+ end
@@ -0,0 +1,26 @@
1
+ Description:
2
+ Generates authentication into your app. Only the features you pick are
3
+ generated, and the code is yours to change.
4
+
5
+ Pick at least one identifier (--email, --phone or --username) and one
6
+ sign-in method (--password, --magic-link, --sms-code, --passkey or
7
+ --omniauth).
8
+
9
+ Not sure which flags you want? The wizard will ask you:
10
+
11
+ bin/rails generate authnz_eleven:wizard
12
+
13
+ Settings live in config/initializers/authentication.rb.
14
+
15
+ Examples:
16
+ bin/rails generate authnz_eleven --email --password
17
+
18
+ Passwordless:
19
+ bin/rails generate authnz_eleven --email --magic-link --passkey
20
+
21
+ GitHub-style:
22
+ bin/rails generate authnz_eleven --username --email --password \
23
+ --recoverable --passkey --second-factor=totp,webauthn --sudoable
24
+
25
+ A second, separate identity:
26
+ bin/rails generate authnz_eleven --user-class=Realtor --namespaced --email --password