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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 55b6fa11e7529b08d79c80eb75523f4d65a6f17365987e172a9dea6cb2fe90cd
4
+ data.tar.gz: fbc4882807a7027a124522082d76c74c932ff6b49581cdebf1d716476193129a
5
+ SHA512:
6
+ metadata.gz: be92aa5fe4fca9845d5bd0e154aade76ee5a1af6686d6b366ea7831705487efe580808e1db293c1bb39d55787a1349ec5381fa7f31611efc0c876fa1c6a83e5a
7
+ data.tar.gz: 1f79e1ea9d500ed56be7303ea1f619d874fb7c21c8a856fb1ca761a7c5135ed59c285311dce22e4865c9709a577f071d1c54212b80f7dae11f84737bfc62dd30
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ Initial public release. See the [README](README.md) for features and installation.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Patrick Ziller
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,384 @@
1
+ # Authnz Eleven
2
+
3
+ Authnz Eleven is an authentication **generator** for Rails. It ***is*** intended to be an [all-singing, all-dancing answer to most authentication concerns](https://github.com/rails/rails/pull/52328). Use it to build your own bespoke auth system or model one after an auth system used on a site you know. Most common authentication flows and features can be expressed with it.
4
+
5
+ You can think of this gem as a little bit
6
+ like a compiler. It will always try to emit the simplest code possible with the fewest
7
+ parts possible. Anything you don't use, you don't pay any complexity or overhead tax for.
8
+ Because it is generated into your project, customization is
9
+ infinite. If you want three-factor authentication, add it.
10
+
11
+ The generated architecture is opinionated and has been designed to be easy for humans to understand, easy to extend,
12
+ and hard to get wrong. It aims for strong account protection without making
13
+ ordinary account management needlessly hostile. In addition to standard account
14
+ protection, builds also resist account-squatting, pre-account hijacking, and other kinds of attacks.
15
+ The defaults deliberately balance security, recovery, and convenience.
16
+
17
+ Quick highlights:
18
+
19
+ - **Sign in with email, phone, or username.** Phone and email can be verified or optional.
20
+
21
+ - **Use passwords, passkeys, magic links, social login, SMS sign-in**, or any combination thereof.
22
+
23
+ - **Optionally, stay coy about the existence of user accounts**. Make flows respond the same whether or not the account exists and resist timing attacks.
24
+
25
+ - **Complete settings views**. You can leave it as-is or customize.
26
+
27
+ ## Why
28
+
29
+ You may have heard that you shouldn't roll your own crypto, but you can roll your own auth. This is true, and truer now than ever before. You don't need a library or team of PhDs to design an auth system. But are you prepared to let your agent one-shot it?
30
+
31
+ The basic case is easy enough, but depending on what you ask for, it can become quite tricky, and may take several iterations to get right. There are often a large number of edge cases to consider, and the consequences of flubbing one can be very serious. There is no one "spec" to hand off, because every app's authentication requirements are different. Conversely, agents can also be overzealous, adding needless complexity to prevent "bugs" that would in fact be harmless or moot.
32
+
33
+ Now you could call this under-specification, but that's just the thing: it's a case where the main bottleneck is *decisions* that must made about app behavior. So I went ahead and made them all for you. You can take this as a carefully considered, opinionated, and human-oriented starting point, so you can focus on only what actually needs changing.
34
+
35
+ ## Requirements
36
+
37
+ Ruby >= 3.4.2
38
+
39
+ Rails >= 8.0.0
40
+
41
+ SQLite, PostgreSQL, or MySQL >= 8.0.13
42
+
43
+ ## Install
44
+
45
+ ```ruby
46
+ # Add this to your Gemfile
47
+ gem "authnz_eleven", group: :development
48
+ ```
49
+
50
+ ```bash
51
+ # then
52
+ bundle install
53
+ bin/rails generate authnz_eleven --email --password
54
+ bin/rails db:migrate # and whatever other instructions that are described in the post-install message
55
+ ```
56
+
57
+ You must select at least one **identifier** (`--email`, `--phone`, or `--username`)
58
+ and one **sign-in method** (`--password`, `--magic-link`, `--sms-code`, `--omniauth`, or
59
+ `--passkey`).
60
+
61
+ ## Wizard
62
+
63
+ There are quite a few flags, and some combinations don't work together, so if you'd rather have your hand held there is a wizard:
64
+
65
+ ```ruby
66
+ # Add this to your Gemfile
67
+ gem "huh", github: "marcoroth/huh-ruby", group: :development
68
+ ```
69
+
70
+ ```bash
71
+ # then
72
+ bundle install
73
+ bin/rails generate authnz_eleven:wizard
74
+ ```
75
+
76
+ ## Sample builds
77
+
78
+ Each build lists only the flags that give it its shape. Add more to taste: `--trackable --rememberable --pwned --deadboltable --captchable`, and so on. Please note these are rough approximations.
79
+
80
+ ### Hacker News / Reddit (old)
81
+
82
+ ```bash
83
+ bin/rails generate authnz_eleven --username --password --email=optional --no-contactable --recoverable --no-verifiable
84
+ ```
85
+
86
+ ### Reddit (modern)
87
+
88
+ ```bash
89
+ bin/rails generate authnz_eleven --username --password --email=optional --phone=optional --recoverable --omniauth
90
+ ```
91
+
92
+ ### WhatsApp / Signal
93
+
94
+ ```bash
95
+ bin/rails generate authnz_eleven --phone --sms-code --registration=open-and-invites
96
+ ```
97
+
98
+ ### Slack
99
+
100
+ ```bash
101
+ bin/rails generate authnz_eleven --email --magic-link --omniauth --teams --registration=open-and-invites
102
+ ```
103
+
104
+ ### GitHub
105
+
106
+ ```bash
107
+ bin/rails generate authnz_eleven --username --email --password --recoverable --passkey --second-factor=totp,webauthn --sudoable
108
+ ```
109
+
110
+ ### Online store with guest checkout
111
+
112
+ ```bash
113
+ bin/rails generate authnz_eleven --email --password --recoverable --guestable --rememberable
114
+ ```
115
+
116
+ ### Internal tool
117
+
118
+ ```bash
119
+ bin/rails generate authnz_eleven --email --omniauth --registration=invite-only --admin-dashboard --impersonatable
120
+ ```
121
+
122
+ ### Shopify
123
+
124
+ ```bash
125
+ bin/rails generate authnz_eleven --email --magic-link --guestable
126
+ bin/rails generate authnz_eleven --user-class=Merchant --namespaced --email --password --second-factor --teams
127
+ ```
128
+
129
+ ## Choices
130
+
131
+ All sessions are backed by a `Session` model. `ActiveSupport::CurrentAttributes` is used to reference the current user/session. Views are ERB.
132
+
133
+ ## Generated code
134
+
135
+ Every build generates the identity model (`User` by default) and its `Authenticatable` concern,
136
+ `Session`, `Current`, the `Authentication` controller concern, sign in/sign
137
+ out controllers, a `/settings` area with a dashboard, signed-in device list, and delete account. Also a
138
+ starter test suite. A navigation partial is included and will be injected into the layout if it has not been modified yet.
139
+
140
+ ## Registration and identifiers
141
+
142
+ At least one identifier is needed to create an account, either an email, phone, or username. Having only a single optional email or phone will not work. However, having *both* an optional email *and* optional phone *will* (`--email=optional --phone=optional`), the user will just need to pick which one they want to supply, and this rule will prevent them from changing their email/phone later to remove both.
143
+
144
+ | Flag | Adds |
145
+ | --- | --- |
146
+ | `--registration[=open\|open-and-invites\|invite-only\|closed]` | Account creation policy: `open` (default) means registration is public. `open-and-invites` allows users to invite others via email or phone, or invite them to teams if using `--teams` too. `invite-only` prevents public registration without an invite. See [Invitations](#invitations). `closed` means accounts must be created some other way in the backend. |
147
+ | `--email[=required[,permanent]\|optional]` | Login with email address. If `required` (default), you may also set `permanent` to prevent users changing it. `optional` with `permanent` is not supported. Email addresses are validated with [valid_email2](https://github.com/micke/valid_email2) and by default also refuse disposable domains. |
148
+ | `--phone[=required[,permanent]\|optional]` | Login with phone number. If `required` (default), you may also set `permanent` to prevent users changing it. `optional` with `permanent` is not supported. Stored in E.164 and validated with [phonelib](https://github.com/daddyz/phonelib). By default all countries are allowed, but you can restrict this in the initializer. |
149
+ | `--username` | A required, unique, unchangeable username. Uniqueness is compared case-insensitively, but the original casing is stored for display. Comes with a customizable blacklist for names like `admin` or `404`, etc. |
150
+ | `--contactable` | **On by default.** Requires that every account hold at least one channel identifier (an email address or phone number) so that you may contact them. |
151
+ | `--verifiable` | **On by default.** Verify ownership of emails and phones by sending a verification email or texting a six-digit code. No-op if you only have usernames. |
152
+ | `--coy` | Never reveal whether an account exists for a given email or phone number in any of the flows. For example, when doing the reset password flow, respond the same way whether an account with that email exists or not. Usernames are considered public and flows concerning them will always respond candidly regardless. Being coy is generally worse for user experience, and although we'll attempt to stymie timing attacks by deferring most timeable work to an `AuthnzElevenDeferredJob`, dedicated analysis can still reveal whether an account exists due to lookup miss vs hit timings. Cannot be used with `--no-verifiable`. |
153
+ | `--encrypted-pii` | Encrypt the email and phone at rest with Active Record Encryption. Covers invitations and `pending_*` fields as well. Encryption is deterministic so `find_by`/`where`/`exists?` and unique indexes will still work, but `LIKE %search%` will not. |
154
+ | `--guestable` | Anonymous guest users. See [Guests](#guests). |
155
+
156
+ If `--verifiable` is on, sign-up will be staged in a `PendingRegistration` model until the email or phone is verified. If both are required, they both must be verified. Only upon satisfaction of verification and credential requirements will the account be created and added to the `users` table. Doing it like this prevents account-squatting, pre-account hijacking, and keeps the code from having to check if users are verified. One side effect though is that registrations that are abandoned will hang around. You can run the included rake task or schedule the cleanup job by uncommenting the entry in `config/recurring.yml`.
157
+
158
+ ```bash
159
+ bin/rails authnz_eleven:delete_expired_pending_registrations
160
+ ```
161
+
162
+ At this time there is no option to let users in a `--verifiable` build create an account with a grace period before requiring they verify their phone/email, but you can of course hack that in yourself.
163
+
164
+ ## Sign-in methods
165
+
166
+ Only the sign-in methods for the flags included will exist in the generated build. If you do not specify `--password`, then passwords will not be an available method for signing in.
167
+
168
+ | Flag | Adds |
169
+ | --- | --- |
170
+ | `--password[=required\|optional\|deferred]` | Password sign-in (`has_secure_password`). `required` (default) puts a mandatory password field on the sign-up form. `optional` lets you leave it blank. `deferred` puts password input after email/phone verification.|
171
+ | `--magic-link` | Sign in by emailed link. Requires `--email`. |
172
+ | `--sms-code` | Sign in by texted six-digit code. Requires `--phone`. |
173
+ | `--passkey` | Passkeys as a passwordless first-factor using the `webauthn` gem. |
174
+ | `--omniauth` | Social / SSO login via `omniauth` gem. You will need to set up a provider in the included initializer. |
175
+
176
+ ## Passwords
177
+
178
+ **Disclaimer**: I do not recommend using passwords if you can avoid it. They are phishable, easy to forget, often make up the majority of IT support calls, and engender a need for more mechanisms in order to keep accounts safe. The few advantages are that they are conceptually simple and familiar to users and easy to share. Many websites will require passwords and then bolt on passkeys as an alternative sign-in method. I say that this is requiring user accounts to have a phishing vector. Better to not include passwords at all. All that being said, if you must use passwords, I suggest including another sign-in method as well, and `--pwned` and `--deadboltable` at the very least. It is up to your wisdom which of the others to include, based on your cohort of users.
179
+
180
+ Any one of the following will imply the `--password` flag.
181
+
182
+ | Flag | Adds |
183
+ | --- | --- |
184
+ | `--recoverable` | Password reset by emailed link. |
185
+ | `--pwned` | `not_pwned` validation against Have I Been Pwned using `pwned` gem. |
186
+ | `--strong-passwords` | Reject weak passwords by their `zxcvbn` strength score, adjustable in initializer. |
187
+ | `--deadboltable` | Too many failed sign-ins deadbolts the password door shut for a duration. A deadbolted account may still use other sign in methods, like passkeys or magic links, because they are not guessable, and if an attacker has access to your email they can just reset your password anyway. Use `authenticate_with_password` instead of `authenticate`. |
188
+ | `--password-rotatable` | Require changing passwords every 90 days (by default). The user will be held at the change-password page until they set a new one. **Note**: NIST SP 800-63B advises *against* forced rotation because it encourages formulaic passwords. |
189
+ | `--password-historical` | Reject a password matching any of the last 5 (default).
190
+
191
+ ## Sessions
192
+
193
+ Control how long sessions may exist and under what conditions they be created.
194
+
195
+ | Flag | Adds |
196
+ | --- | --- |
197
+ | `--rememberable` | A "remember me" checkbox on password sign-in. Checked, the cookie gets a lifetime that survives the browser closing. Unchecked, it's a browser-session cookie. |
198
+ | `--timeoutable` | Expire sessions after a period of inactivity. |
199
+ | `--max-sessionable[=evict\|prompt]` | Cap concurrent sessions. `evict` (default) signs out the least-recently-active session. `prompt` holds the new sign-in at the session list until the user signs one out. |
200
+
201
+ ## API tokens
202
+
203
+ `--api-tokens` adds personal access tokens and a management page at `/settings/api_tokens`. Users can create and revoke tokens. Secrets are shown once and stored only as digests. Tokens expire after 90 days.
204
+
205
+ ### Using tokens in your API
206
+
207
+ The generator includes an `ApiTokenAuthentication` concern that provides bearer authentication. Include it in your own API controller:
208
+
209
+ ```ruby
210
+ class Api::ApplicationController < ActionController::API
211
+ include ApiTokenAuthentication
212
+ end
213
+
214
+ class Api::ProfileController < Api::ApplicationController
215
+ def show
216
+ render json: { id: current_api_user.id }
217
+ end
218
+ end
219
+ ```
220
+
221
+ Clients send `Authorization: Bearer <token>`. Your actions have access to `current_api_user` and `current_api_token`. Invalid, expired, or revoked tokens receive a JSON `401`.
222
+
223
+ Token authentication does not create browser sessions. Signing out or changing a password leaves tokens active. Revocation and expiry end access. Banning or deleting the account removes its tokens. With `--security-notifications`, creation and revocation send an email.
224
+
225
+ For namespaced identities, use `Realtors::ApiTokenAuthentication` and `Realtor::ApiToken`.
226
+
227
+ ## Activity
228
+
229
+ | Flag | Adds |
230
+ | --- | --- |
231
+ | `--last-seenable` | A `last_seen_at` column on users, touched at sign-in and refreshed periodically. |
232
+ | `--trackable` | An `Event` audit trail of authentication activity, browsable at `/settings/authentications/events`. Records the sign-in method used. |
233
+
234
+ ## Hardening
235
+
236
+ There are many hardening measures included in every build, like rate limits and disallowing reuse of tokens. These flags are about behavior observable to the user.
237
+
238
+ | Flag | Adds |
239
+ | --- | --- |
240
+ | `--second-factor[=totp\|webauth\|sms]` | Add two-factor authentication. You can adjust which sign-in methods require a 2nd factor in the initializer. `totp` ("time-based one-time password", the default) uses an authenticator app, `webauthn` is hardware security keys (like passkeys but for 2nd factor), and `sms` is a one-time code texted to the user's phone. You may use multiple, like so: `--second-factor=totp,webauthn,sms`. Enrolling in a 2FA method will create encrypted recovery codes and show them to the user only once, and they can be regenerated if lost. `sms` needs `--phone`, and will not work with `--sms-code` since the sign-in method and the 2FA method would be exactly the same. |
241
+ | `--sudoable` | Re-require user authentication before dangerous actions. See [Sudo](#sudo). A partial template and controller class methods are included to simplify setup. Will challenge the user with an auth method based on what methods the user has available. |
242
+ | `--captchable` | A captcha (Cloudflare Turnstile is the only provider included for now) partial template is included and used inline on the unauthenticated forms: sign-in, sign-up, password reset, magic link, texted-code sign-in. Reuse on any form you want. |
243
+ | `--security-notifications` | Email users when credentials, second factors, connected accounts, contact information, sessions, or the account itself change. Requires `--email`. |
244
+
245
+ ## Administration
246
+
247
+ | Flag | Adds |
248
+ | --- | --- |
249
+ | `--adminable` | Users can be site admins. Cannot be created from ActiveRecord, must be created directly in the DB. For example, `echo "UPDATE users SET admin = true …;" \| bin/rails dbconsole` |
250
+ | `--admin-dashboard` | An `/admin` area (dashboard, users, sessions). Responds with a `404` unless the user is an admin. If `--second-factor` is included, admins are held at 2FA enrollment until they enroll one. Implies `--adminable`. |
251
+ | `--bannable` | Admins can ban users temporarily or permanently. Immediately kicks the user out and locks login. |
252
+ | `--impersonatable` | Admins can impersonate other users, with the true user being accessible through `Current.true_user`. Impersonation automatically expires after 1 hour (default). Useful for support or bug investigation. Implies `--adminable`. |
253
+ | `--easy-dev-login` | **Development only.** Bypass the sign-in form and sign in as anyone in development. Use any email/phone/username. If using `--password` then the password is ignored. If not, there will be a dedicated endpoint at `POST /easy_dev_login`. Set `REQUIRE_DEV_PASSWORD` and `REQUIRE_DEV_MFA` ENV variables to use real flows. |
254
+
255
+ ## Teams
256
+
257
+ | Flag | Adds |
258
+ | --- | --- |
259
+ | `--teams[=scope\|middleware\|session]` | Add `Team`, `Membership`, and team creation/selection. `scope` (default) namespaces team pages under a `/:team_id` route scope (or `/<identity>/:team_id` for a namespaced identity), allowing separate tabs to be open with different teams on the same session, and for links to be easily shareable. `middleware` does the same thing but using middleware, as seen in 37signals apps: it peels a leading `/:team_id` onto `SCRIPT_NAME`, so every route in the app carries it without being written inside a scope (not available with `--namespaced`). `session` stores the team on the session instead, so the team identifier is not in the url, but you're limited to one active team per session. A namespaced identity gets its own teams (`Merchant::Team`, `Merchant::Membership`). |
260
+
261
+ ## Identity classes
262
+
263
+ You can customize the identity class used in generation.
264
+
265
+ | Flag | Adds |
266
+ | --- | --- |
267
+ | `--user-class=NAME` | Rename the generated identity (default `User`). The table, sessions, routes, controllers and config constant all derive from it. |
268
+ | `--namespaced` | Give the identity its own URL prefix and namespace. **This is required when generating a second identity** so it doesn't collide with the first. |
269
+ | `--primary-key-type=TYPE` | Primary key type for the generated migrations (e.g. `uuid`). |
270
+
271
+ Most apps will only ever need one user identity class and can utilize roles or admin flags to differentiate user types. However, if you need totally separate authentication methods or namespaces, you may run the generator again with `--namespaced` and a different `--user-class` to get a second, fully independent auth stack: its own model, table, sessions, controllers, routes, and flags.
272
+
273
+ ```bash
274
+ bin/rails generate authnz_eleven --email --password --guestable
275
+ bin/rails generate authnz_eleven --email --phone --passkey --timeoutable --user-class=Realtor --namespaced
276
+ ```
277
+
278
+ That gives you a `Realtor` model that signs in at `/realtor/sign_in`, `Realtors::SessionsController`,
279
+ `realtor_sign_in_path`, a `Realtor::Session` and a `Realtors::BaseController`.
280
+ It's purely additive. No file from the first run is touched.
281
+
282
+ A namespaced identity is its own portal, the way a Rails engine is. `Realtors::BaseController`
283
+ inherits `ActionController::Base`, not your `ApplicationController`, and renders
284
+ a `realtors` layout. Nothing you change in one identity's auth code reaches the other.
285
+
286
+ ## Invitations
287
+
288
+ When using an invitable registration mode, you can send invitations to both emails and phone numbers if the invitee doesn't already have an account. The `User` is not created until the invitee accepts the invitation.
289
+ If teams are enabled as well, the you can also invite existing users to a team using email or phone.
290
+
291
+ ## Guests
292
+
293
+ `--guestable` adds anonymous users. A guest is a **real `User` row** (`guest: true`) with a **real session**, so they get a real `Current.user` and your existing code should work unchanged. Guests are not considered `authenticated?` though.
294
+
295
+ Opt a controller in with `allow_guest_access`:
296
+
297
+ ```ruby
298
+ class CartsController < ApplicationController
299
+ allow_guest_access
300
+
301
+ def show
302
+ @cart = Current.user.cart # both authenticated users and guest users can have carts
303
+ end
304
+ end
305
+ ```
306
+
307
+ When a guest signs in or signs up, `absorb_guest` in `User::Guestable` is called from the `Authentication` concern and transfers the guest state onto the authenticated account, then destroys the guest row:
308
+
309
+ ```ruby
310
+ def absorb_guest(guest)
311
+ transaction do
312
+ guest.cart&.update!(user: self)
313
+ guest.destroy
314
+ end
315
+ end
316
+ ```
317
+
318
+ Guests sessions will pile up, so run the included rake task or schedule the cleanup job by uncommenting the entry in `config/recurring.yml`.
319
+
320
+ ```bash
321
+ bin/rails authnz_eleven:delete_expired_guests
322
+ ```
323
+
324
+ A guest's age is measured from creation, not last activity, and guests are exempt from `--timeoutable`. Their cookie persists for that lifetime, so returning visitors reuse their guest account even after closing the browser.
325
+
326
+ ## Sudo
327
+
328
+ `--sudoable` generates two macros, for two different use cases:
329
+
330
+ ```ruby
331
+ require_sudo only: :destroy # require the sudo challenge answer as a param in a single request
332
+ require_sudo_within 10.minutes # guard a collection of routes
333
+ ```
334
+
335
+ `require_sudo` is for a single dangerous action. Include the password (or webauthn credential, or
336
+ code) in a field on the form you submit. `require_sudo_within` guards a
337
+ whole section and keeps it unlocked for the window.
338
+
339
+ A successful sudo confirmation stamps the session's `sudo_at` column. `require_sudo_within` checks that that timestamp is within the window, otherwise the user is redirected to a separate confirmation page, then back to the originally requested protected page.
340
+
341
+ Use `require_sudo_within` on pages that can safely be requested again. It redirects back to the original URL; it does not replay a submitted form or preserve an OAuth callback's provider authentication data.
342
+
343
+ Out of the box, sudo guards actions that create persistent access to the account, or that can't be undone:
344
+
345
+ - changing the email address
346
+ - changing the phone number
347
+ - adding a passkey or security key
348
+ - creating an API token
349
+ - deleting the account
350
+
351
+ Sudo only works with a password, passkey, or a TOTP/webauthn 2nd factor. Accounts that only use magic links, SMS OTPs, or logins from an Omniauth provider cannot perform a sudo and will not be asked for one.
352
+
353
+ ## Configuration
354
+
355
+ `config/initializers/authentication.rb` holds every config, aliased to a top-level constant, `UserAuth` (default), so you can reference `UserAuth.session.idle_timeout`. I suggest you read that file to understand the different options. A namespaced identity gets its own config constant, for example `RealtorAuth`.
356
+
357
+ The flags you generated the build with are posted in a comment at the top of the file.
358
+
359
+ ## Upgrading or transmuting
360
+
361
+ Because authentication is generated into your app, there is no automated way to change to a new version or different set of flags in the future after you have added your own code in there, like you may be able to do in some runtime auth gems. However, you can generate a new separate build with the flags you want, and point an agent at it to fix your app to use the auth system of the new one. Don't forget to do data migrations and update your UI as well. Overall this seems to be effective because the chance of the agent messing up or building something awkward is greatly diminished by the fact that it has a reference implementation, a "spec", to look at.
362
+
363
+ ## Notes
364
+
365
+ - `/settings` uses the top-level `Settings` constant. If your app already
366
+ defines one (the [`config`](https://github.com/rubyconfig/config) gem does),
367
+ [rename it before generating](https://github.com/rubyconfig/config#configuration).
368
+
369
+
370
+ ## Contributing
371
+
372
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for setup, tests and style.
373
+
374
+ ## Acknowledgments
375
+
376
+ This gem was heavily inspired by [`authentication-zero`](https://github.com/lazaronixon/authentication-zero), [`devise`](https://github.com/heartcombo/devise), and [`rodauth`](https://github.com/jeremyevans/rodauth). Thank you.
377
+
378
+ ## Author
379
+
380
+ Patrick Ziller
381
+
382
+ ## License
383
+
384
+ MIT, see [LICENSE.txt](LICENSE.txt).
@@ -0,0 +1,210 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/string/inflections"
4
+
5
+ module AuthnzEleven
6
+ # Derives every name the generator needs from a single class name, using Rails'
7
+ # inflector so the generated code agrees with what Rails itself computes at
8
+ # runtime (route keys, table names, foreign keys).
9
+ #
10
+ # The default identity (class "User", not namespaced) is the plain case: User,
11
+ # /sign_in, Session, Authentication. Only a renamed (`--user-class`) and/or
12
+ # namespaced identity diverges, and every accessor below says how.
13
+ class Identity
14
+ DEFAULT_CLASS_NAME = "User"
15
+
16
+ attr_reader :class_name
17
+
18
+ def initialize(class_name: nil, namespaced: false)
19
+ name = class_name.to_s.strip
20
+ @class_name = (name.empty? ? DEFAULT_CLASS_NAME : name).camelize
21
+ @namespaced = namespaced
22
+ end
23
+
24
+ def namespaced? = @namespaced
25
+
26
+ # The canonical, unchanged build: plain "User", no namespace.
27
+ def default? = @class_name == DEFAULT_CLASS_NAME && !namespaced?
28
+
29
+ # Core inflections: "user"/"users" for the default, e.g. "realtor"/"realtors".
30
+ def singular = @class_name.underscore
31
+ def plural = singular.pluralize
32
+ def table = plural
33
+ def foreign_key = "#{singular}_id"
34
+
35
+ # Route + controller slugs. They track the class name (for the default that is
36
+ # the familiar user/users); the URL *path*, however, is only prefixed once
37
+ # namespaced (see #path_prefix), so the default keeps top-level /sign_in.
38
+ #
39
+ # route_scope -> route "as:" stem, e.g. user_sign_in_path
40
+ # controller_module -> module: :users / Users::SessionsController
41
+ def route_scope = singular
42
+ def controller_module = plural
43
+
44
+ # Camelized forms for class/module declarations in templates.
45
+ # module_name -> "Users" (Users::SessionsController)
46
+ # settings_module -> "Settings" (Settings::PasswordsController)
47
+ def module_name = controller_module.camelize
48
+ def settings_module = nested_module("Settings")
49
+
50
+ # An area (settings, admin, teams) sits at the top level for the default
51
+ # identity and inside the identity's own namespace once namespaced: its files,
52
+ # its module, and its route helpers.
53
+ def nested_path(name) = namespaced? ? "#{controller_module}/#{name}" : name
54
+ def nested_module(name) = namespaced? ? "#{module_name}::#{name}" : name
55
+ def nested_helper(name) = namespaced? ? "#{route_scope}_#{name}" : name
56
+
57
+ # URL path scope: bare for the default identity (so /sign_in stays /sign_in),
58
+ # the identity's own name once namespaced.
59
+ def path_prefix = namespaced? ? singular : nil
60
+
61
+ # Where the self-service area's controllers, views and tests are *filed*:
62
+ # a plain top-level "settings" directory for the default identity. A namespaced
63
+ # identity's settings controllers, by contrast, live *inside* the same outer
64
+ # route scope as its other controllers (see namespaced_route_block), which sets
65
+ # `module: identity.controller_module` once for everything nested in it — so
66
+ # the directory must nest under controller_module here too, or the settings
67
+ # controllers get filed (and named) under a namespace the routes never
68
+ # actually point at.
69
+ def settings_path_prefix = nested_path("settings")
70
+
71
+ # The settings area's own base controller, mirroring AdminController: every
72
+ # Settings:: controller inherits it, so an exemption that belongs to the
73
+ # whole area is declared once.
74
+ def settings_controller = "#{settings_module}Controller"
75
+ def settings_controller_file = "#{settings_path_prefix}_controller"
76
+
77
+ # The *route-helper* stem for that same area — not simply the directory with
78
+ # slashes swapped. settings_path_prefix is plural (controller_module) so the
79
+ # controllers file/name correctly under Admins::Settings::…, but the settings
80
+ # routes nest inside the outer scope's `as: route_scope` (singular), so their
81
+ # helpers are admin_settings_… — not admins_settings_…. Use this for
82
+ # `#{…}_*_path/url`; use settings_path_prefix for file paths, and
83
+ # settings_module for the module name.
84
+ def settings_helper_prefix = nested_helper("settings")
85
+
86
+ # A generic name (Session, Current, Team, ...) belongs to the default identity.
87
+ # A namespaced identity nests its own copy inside its model class, where Rails
88
+ # infers associations (Realtor's has_many :sessions finds Realtor::Session),
89
+ # prefixes the table (realtor_sessions) and demodulizes foreign keys
90
+ # (session_id), so no association needs a class_name.
91
+ def namespaced_class(name) = namespaced? ? "#{@class_name}::#{name}" : name
92
+ def table_for(klass) = klass.underscore.tr("/", "_").pluralize
93
+
94
+ # Two identities can be signed in at once (separate cookies), never
95
+ # clobbering each other.
96
+ def session_class = namespaced_class("Session")
97
+ def session_file = session_class.underscore
98
+ def sessions_table = table_for(session_class)
99
+ def cookie_name = namespaced? ? "#{singular}_session_token" : "session_token"
100
+
101
+ # Each identity owns an independent set of authentication tunables, reachable
102
+ # two ways (the initializer sets up both, pointing at the same object):
103
+ #
104
+ # config_storage -> Rails.configuration.x.user_auth (the Rails-native slot,
105
+ # source of truth; still works for anything expecting it)
106
+ # config_constant -> UserAuth (a short top-level alias
107
+ # the generated code actually reads — UserAuth.session.…)
108
+ #
109
+ # config_reference is the constant: it's what every read site resolves to, so a
110
+ # single seam flips them all. A namespaced identity gets its own pair (RealtorAuth /
111
+ # Rails.configuration.x.realtor_auth) so two identities never share tunables.
112
+ def config_namespace = "#{singular}_auth"
113
+ def config_storage = "Rails.configuration.x.#{config_namespace}"
114
+ def config_constant = "#{@class_name}Auth"
115
+ def config_reference = config_constant
116
+ def initializer_file = default? ? "authentication" : "#{singular}_authentication"
117
+
118
+ # A namespaced identity's task gets its own namespace so it cannot sweep another identity's
119
+ # sessions.
120
+ def task_namespace = namespaced? ? singular : nil
121
+ def rake_task(name) = ["authnz_eleven", task_namespace, name].compact.join(":")
122
+
123
+ # The controller-level concerns. "Authentication" for the default identity
124
+ # (included in ApplicationController); Realtors::Authentication once namespaced,
125
+ # in the same namespace as the identity's controllers and included by its
126
+ # base controller.
127
+ def authentication_concern = nested_module("Authentication")
128
+ def authentication_concern_file = authentication_concern.underscore
129
+ def authorization_concern = nested_module("Authorization")
130
+ def authorization_concern_file = authorization_concern.underscore
131
+
132
+ # The base controller a namespaced identity's controllers inherit from
133
+ # (e.g. Realtors::BaseController). Unused for the default identity, which wires
134
+ # the concern straight into ApplicationController.
135
+ def base_controller = "#{controller_module.camelize}::BaseController"
136
+ def base_controller_file = "#{controller_module}/base_controller"
137
+
138
+ # What this identity's own controllers (sessions, registrations, account
139
+ # area, ...) inherit from: ApplicationController for the default identity;
140
+ # the identity's own BaseController once namespaced, so its authentication
141
+ # concern is never injected into the shared ApplicationController.
142
+ def controller_superclass = namespaced? ? base_controller : "ApplicationController"
143
+
144
+ # Mailers. Shared "UserMailer"/"InvitationMailer" for the default identity; a
145
+ # namespaced identity gets its own so two identities' mailers (and mailer views,
146
+ # which Rails locates by the mailer's underscored name) never collide.
147
+ def mailer_class = namespaced? ? "#{@class_name}Mailer" : "UserMailer"
148
+ def mailer_file = mailer_class.underscore
149
+
150
+ def current_class = namespaced_class("Current")
151
+ def current_file = current_class.underscore
152
+
153
+ def event_class = namespaced_class("Event")
154
+ def event_file = event_class.underscore
155
+ def events_table = table_for(event_class)
156
+
157
+ def api_token_class = namespaced_class("ApiToken")
158
+ def api_token_file = api_token_class.underscore
159
+ def api_tokens_table = table_for(api_token_class)
160
+
161
+ def recovery_code_class = namespaced_class("RecoveryCode")
162
+ def recovery_code_file = recovery_code_class.underscore
163
+ def recovery_codes_table = table_for(recovery_code_class)
164
+
165
+ # Named for the credential, not for one of its roles: this table holds passkeys
166
+ # as well as second-factor security keys.
167
+ def webauthn_credential_class = namespaced_class("WebauthnCredential")
168
+ def webauthn_credential_file = webauthn_credential_class.underscore
169
+ def webauthn_credentials_table = table_for(webauthn_credential_class)
170
+
171
+ def password_history_class = namespaced_class("PasswordHistory")
172
+ def password_history_file = password_history_class.underscore
173
+ def password_histories_table = table_for(password_history_class)
174
+
175
+ def invitation_class = namespaced_class("Invitation")
176
+ def invitation_file = invitation_class.underscore
177
+ def invitations_table = table_for(invitation_class)
178
+
179
+ def omniauth_identity_class = namespaced_class("OmniauthIdentity")
180
+ def omniauth_identity_file = omniauth_identity_class.underscore
181
+ def omniauth_identities_table = table_for(omniauth_identity_class)
182
+
183
+ def pending_registration_class = namespaced_class("PendingRegistration")
184
+ def pending_registration_file = pending_registration_class.underscore
185
+ def pending_registrations_table = table_for(pending_registration_class)
186
+
187
+ def team_class = namespaced_class("Team")
188
+ def team_file = team_class.underscore
189
+ def teams_table = table_for(team_class)
190
+
191
+ def membership_class = namespaced_class("Membership")
192
+ def membership_file = membership_class.underscore
193
+ def memberships_table = table_for(membership_class)
194
+
195
+ # The two things an account and a sign-up-in-progress must agree about: what
196
+ # identifies them, and what makes a password acceptable. Both are included by
197
+ # the model and by PendingRegistration, so a form that validates while staged
198
+ # still validates when it becomes a row.
199
+ def principals_concern = namespaced_class("Principals")
200
+ def principals_concern_file = principals_concern.underscore
201
+ def password_policy_concern = namespaced_class("PasswordPolicy")
202
+ def password_policy_concern_file = password_policy_concern.underscore
203
+
204
+ def nav_partial = namespaced? ? "#{controller_module}/authnz_eleven_nav" : "shared/authnz_eleven_nav"
205
+
206
+ def sudo_partial
207
+ namespaced? ? "#{controller_module}/authnz_eleven_sudo_challenge" : "shared/authnz_eleven_sudo_challenge"
208
+ end
209
+ end
210
+ end