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,151 @@
1
+ module <%= identity.class_name %>::Authenticatable
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ has_many :sessions, dependent: :destroy
6
+ <%- if api_tokens? -%>
7
+ has_many :api_tokens, dependent: :destroy
8
+ <%- end -%>
9
+ <%- if webauthn_credentials? -%>
10
+ has_many :webauthn_credentials, dependent: :destroy
11
+ <%- end -%>
12
+ <%- if omniauth? -%>
13
+ has_many :omniauth_identities, dependent: :destroy
14
+ <%- end -%>
15
+ <%- if webauthn_credentials? -%>
16
+
17
+ before_create do
18
+ self.webauthn_handle ||= WebAuthn.generate_user_id
19
+ end
20
+ <%- end -%>
21
+ <%- if password? -%>
22
+
23
+ scope :with_password, -> { where.not(password_digest: nil) }
24
+
25
+ after_update if: :password_digest_previously_changed? do
26
+ sessions.where.not(id: <%= identity.current_class %>.session).delete_all
27
+ end
28
+ <%- if trackable? -%>
29
+
30
+ after_update if: :password_digest_previously_changed? do
31
+ record_event("<%= identity.singular %>.password_changed")
32
+ end
33
+ <%- end -%>
34
+ <%- end -%>
35
+ end
36
+ <%- if multi_login? || password? || sms_code? -%>
37
+
38
+ class_methods do
39
+ <%- if password? -%>
40
+ def authenticate_with_password(identifier, password)
41
+ identifier = identifier.to_s.strip
42
+ <%- keys = principals.login_dispatch_principals -%>
43
+ <%- if keys.size == 1 -%>
44
+ user = <%= keys.first.authenticate_lookup %>
45
+ <%- else -%>
46
+ user =
47
+ <%- keys.each_with_index do |principal, i| -%>
48
+ <%- if i.zero? -%>
49
+ if <%= principal.login_dispatch_test %>
50
+ <%- elsif i < keys.size - 1 -%>
51
+ elsif <%= principal.login_dispatch_test %>
52
+ <%- else -%>
53
+ else
54
+ <%- end -%>
55
+ <%= principal.authenticate_lookup %>
56
+ <%- end -%>
57
+ end
58
+ <%- end -%>
59
+
60
+ <%- if deadboltable? -%>
61
+ user unless user&.deadbolted?
62
+ <%- else -%>
63
+ user
64
+ <%- end -%>
65
+ end
66
+ <%- end -%>
67
+ <%- if multi_login? && password? -%>
68
+
69
+ <%- end -%>
70
+ <%- if multi_login? -%>
71
+ def find_by_login(identifier)
72
+ identifier = identifier.to_s.strip
73
+ <%- keys = principals.login_dispatch_principals -%>
74
+ <%- keys.each_with_index do |principal, i| -%>
75
+ <%- lookup = principal.find_by_login_lookup -%>
76
+ <%- if i < keys.size - 1 -%>
77
+ return <%= lookup %> if <%= principal.login_dispatch_test %>
78
+ <%- else -%>
79
+ <%= lookup %>
80
+ <%- end -%>
81
+ <%- end -%>
82
+ end
83
+ <%- end -%>
84
+ <%- if sms_code? -%>
85
+ <%- if multi_login? || password? -%>
86
+
87
+ <%- end -%>
88
+ def issue_sms_sign_in_code(phone)
89
+ find_by(phone:)&.tap(&:issue_sms_challenge)
90
+ end
91
+ <%- end -%>
92
+ end
93
+ <%- end -%>
94
+ <%- if password_nullable? && !password_blank_rows_all? -%>
95
+
96
+ def password_may_be_blank? = <%= password_blank_rows_expression %>
97
+ <%- end -%>
98
+ <%- if removable_credentials? -%>
99
+
100
+ def sign_in_methods
101
+ methods = []
102
+ <%- if password? -%>
103
+ methods << :password if password_set?
104
+ <%- end -%>
105
+ <%- if passkey? -%>
106
+ methods << :passkey if webauthn_credentials<%= ".passkeys" if passkey_and_security_keys? %>.any?
107
+ <%- end -%>
108
+ <%- if omniauth? -%>
109
+ methods << :omniauth if omniauth_identities.any?
110
+ <%- end -%>
111
+ <%- if magic_link? -%>
112
+ methods << :magic_link if email.present?
113
+ <%- end -%>
114
+ <%- if sms_code? -%>
115
+ methods << :sms_code if phone.present?
116
+ <%- end -%>
117
+ methods
118
+ end
119
+
120
+ def last_sign_in_method?(kind)
121
+ return false if (sign_in_methods - [ kind ]).any?
122
+
123
+ <%- if passkey? || omniauth? -%>
124
+ case kind
125
+ <%- if passkey? -%>
126
+ when :passkey then webauthn_credentials<%= ".passkeys" if passkey_and_security_keys? %>.count <= 1
127
+ <%- end -%>
128
+ <%- if omniauth? -%>
129
+ when :omniauth then omniauth_identities.count <= 1
130
+ <%- end -%>
131
+ else true
132
+ end
133
+ <%- else -%>
134
+ true
135
+ <%- end -%>
136
+ end
137
+ <%- end -%>
138
+ <%- if max_sessions_prompt? -%>
139
+
140
+ def within_session_limit?
141
+ limit = <%= identity.config_reference %>.session.concurrent_limit
142
+ limit.nil? || sessions.count <= limit
143
+ end
144
+ <%- end -%>
145
+ <%- if last_seenable? -%>
146
+
147
+ def mark_last_seen!
148
+ update_column(:last_seen_at, Time.current)
149
+ end
150
+ <%- end -%>
151
+ end
@@ -0,0 +1,24 @@
1
+ module <%= identity.class_name %>::Bannable
2
+ def banned?
3
+ banned_at.present? && (banned_until.nil? || banned_until.future?)
4
+ end
5
+
6
+ def ban!(expires_at: nil)
7
+ return if banned?
8
+ update!(banned_at: Time.current, banned_until: expires_at)
9
+ <%- if trackable? -%>
10
+ record_event("<%= identity.singular %>.banned")
11
+ <%- end -%>
12
+ sessions.destroy_all
13
+ <%- if api_tokens? -%>
14
+ api_tokens.destroy_all
15
+ <%- end -%>
16
+ end
17
+
18
+ def unban!
19
+ update!(banned_at: nil, banned_until: nil)
20
+ <%- if trackable? -%>
21
+ record_event("<%= identity.singular %>.unbanned")
22
+ <%- end -%>
23
+ end
24
+ end
@@ -0,0 +1,39 @@
1
+ module <%= identity.class_name %>::Deadboltable
2
+ extend ActiveSupport::Concern
3
+
4
+ class_methods do
5
+ def register_failed_attempt(identifier)
6
+ <%= multi_login? ? "find_by_login(identifier)" : "find_by(#{login_field}: identifier)" %>&.tap(&:register_failed_attempt!)
7
+ end
8
+ end
9
+
10
+ def deadbolted?
11
+ deadbolted_at.present? && deadbolted_at > <%= identity.config_reference %>.deadbolt.duration.ago
12
+ end
13
+
14
+ def deadbolt!
15
+ update!(deadbolted_at: Time.current, failed_attempts: 0)
16
+ <%- if trackable? -%>
17
+ record_event("<%= identity.singular %>.deadbolted")
18
+ <%- end -%>
19
+ end
20
+
21
+ def undeadbolt!
22
+ update!(deadbolted_at: nil, failed_attempts: 0)
23
+ <%- if trackable? -%>
24
+ record_event("<%= identity.singular %>.undeadbolted")
25
+ <%- end -%>
26
+ end
27
+
28
+ def register_failed_attempt!
29
+ return if deadbolted?
30
+ increment!(:failed_attempts)
31
+
32
+ maximum_attempts = <%= identity.config_reference %>.deadbolt.maximum_attempts
33
+ deadbolt! if maximum_attempts && failed_attempts >= maximum_attempts
34
+ end
35
+
36
+ def reset_failed_attempts!
37
+ update_column(:failed_attempts, 0) if failed_attempts.positive?
38
+ end
39
+ end
@@ -0,0 +1,52 @@
1
+ module <%= identity.class_name %>::EmailVerifiable
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ normalizes :pending_email, with: -> { it.strip.downcase.presence }
6
+ <%- if email_encrypted? -%>
7
+ encrypts :pending_email
8
+ <%- end -%>
9
+ validates :pending_email, <%= principals.email.validation %>, allow_nil: true
10
+ <%- unless coy? -%>
11
+ validate { errors.add(:pending_email, :taken) if pending_email_taken? }
12
+ <%- end -%>
13
+
14
+ generates_token_for :email_change_verification, expires_in: <%= identity.config_reference %>.tokens.email_change_verification do
15
+ pending_email
16
+ end
17
+ <%- if trackable? -%>
18
+
19
+ after_update if: :email_previously_changed? do
20
+ record_event("<%= identity.singular %>.email_changed")
21
+ end
22
+ <%- end -%>
23
+ end
24
+
25
+ def stage_email(address)
26
+ self.pending_email = address
27
+
28
+ if pending_email.blank?
29
+ self.email = nil
30
+ elsif pending_email == email
31
+ self.pending_email = nil
32
+ end
33
+ self
34
+ end
35
+
36
+ def pending_email_taken?
37
+ pending_email.present? && <%= identity.class_name %>.exists?(email: pending_email)
38
+ end
39
+
40
+ def verify_pending_email
41
+ update(email: pending_email, pending_email: nil)
42
+ rescue ActiveRecord::RecordNotUnique
43
+ errors.add(:email, :taken)
44
+ false
45
+ end
46
+
47
+ def send_email_change_verification
48
+ return if pending_email.nil? || pending_email_taken?
49
+
50
+ <%= identity.mailer_class %>.with(signed_id: generate_token_for(:email_change_verification), to: pending_email).email_change_verification.deliver_later
51
+ end
52
+ end
@@ -0,0 +1,22 @@
1
+ module <%= identity.namespaced_class("Eventable") %>
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ has_many :events, as: :subject, dependent: :destroy
6
+ end
7
+
8
+ def record_event(event_type, actor: nil, context: nil, occurred_at: nil, data: nil)
9
+ <%= identity.event_class %>.record!(
10
+ event_type: event_type,
11
+ subject: self,
12
+ actor: actor,
13
+ context: context,
14
+ occurred_at: occurred_at,
15
+ data: data
16
+ )
17
+ end
18
+
19
+ def event_context
20
+ self
21
+ end
22
+ end
@@ -0,0 +1,30 @@
1
+ module <%= identity.class_name %>::Guestable
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ scope :guests, -> { where(guest: true) }
6
+
7
+ scope :expired_guests, -> { guests.where(created_at: ...<%= identity.config_reference %>.guest.ttl.ago) }
8
+ end
9
+
10
+ class_methods do
11
+ def create_guest!
12
+ create! do |user|
13
+ <%- if principals.email -%>
14
+ user.email = "guest_#{SecureRandom.uuid}@example.com"
15
+ <%- end -%>
16
+ <%- if principals.username -%>
17
+ user.username = "guest_#{SecureRandom.base58(12)}"
18
+ <%- end -%>
19
+ user.guest = true
20
+ end
21
+ end
22
+ end
23
+
24
+ def absorb_guest(guest)
25
+ transaction do
26
+ # Extension point: move any guest-owned state onto this account before the guest row goes.
27
+ guest.destroy
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,12 @@
1
+ module <%= identity.class_name %>::Invitable
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ <%- if teams? -%>
6
+ has_many :accepted_invitations, class_name: "<%= identity.invitation_class %>", foreign_key: :invitee_id, inverse_of: :invitee, dependent: :destroy
7
+ <%- else -%>
8
+ has_one :invitation, foreign_key: :invitee_id, inverse_of: :invitee, dependent: :destroy
9
+ <%- end -%>
10
+ has_many :sent_invitations, class_name: "<%= identity.invitation_class %>", foreign_key: :inviter_id, inverse_of: :inviter, dependent: :destroy
11
+ end
12
+ end
@@ -0,0 +1,34 @@
1
+ module <%= identity.class_name %>::MagicLinkable
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ generates_token_for :magic_link, expires_in: <%= identity.config_reference %>.tokens.magic_link do
6
+ [ email, Rails.cache.read(magic_link_nonce_key) ] # nonce makes magic links single-use
7
+ end
8
+ end
9
+
10
+ class_methods do
11
+ def request_magic_link(email)
12
+ find_by(email:)&.tap(&:send_magic_link)
13
+ end
14
+
15
+ def consume_magic_link(token)
16
+ if (user = find_by_token_for(:magic_link, token)) && Rails.cache.delete(user.magic_link_nonce_key)
17
+ user
18
+ end
19
+ end
20
+ end
21
+
22
+ def generate_magic_link
23
+ Rails.cache.write(magic_link_nonce_key, SecureRandom.hex(16), expires_in: <%= identity.config_reference %>.tokens.magic_link)
24
+ generate_token_for(:magic_link)
25
+ end
26
+
27
+ def magic_link_nonce_key
28
+ "<%= identity.singular %>:magic_link_nonce:#{id}"
29
+ end
30
+
31
+ def send_magic_link
32
+ <%= identity.mailer_class %>.with(signed_id: generate_magic_link, to: email).magic_link.deliver_later
33
+ end
34
+ end
@@ -0,0 +1,28 @@
1
+ module <%= identity.class_name %>::PasswordHistorical
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ has_many :password_histories, dependent: :delete_all
6
+
7
+ validate :password_not_recently_used, if: :will_save_change_to_password_digest?
8
+
9
+ after_update if: :password_digest_previously_changed? do
10
+ password_histories.create!(password_digest: password_digest_previously_was)
11
+ depth = <%= identity.config_reference %>.password.history_depth
12
+ password_histories.order(id: :desc).offset(depth).destroy_all if depth
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def password_not_recently_used
19
+ depth = <%= identity.config_reference %>.password.history_depth
20
+ return if depth.nil?
21
+
22
+ recent = password_histories.order(id: :desc).limit(depth).pluck(:password_digest)
23
+ recent << password_digest_in_database if password_digest_in_database.present?
24
+ if recent.any? { |digest| self.class.new(password_digest: digest).authenticate(password) }
25
+ errors.add(:password, "was used recently. Please choose a different one")
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,38 @@
1
+ module <%= identity.password_policy_concern %>
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ has_secure_password
6
+
7
+ <%- unless password_nullable? -%>
8
+ validates :password, presence: true, length: { minimum: <%= identity.config_reference %>.password.minimum_length }, allow_nil: true
9
+ <%- else -%>
10
+ validates :password, length: { minimum: <%= identity.config_reference %>.password.minimum_length }, allow_nil: true
11
+ <%- end -%>
12
+ <%- if pwned? -%>
13
+ validates :password, not_pwned: { message: "was found in a data breach and can't be used" },
14
+ if: -> { <%= identity.config_reference %>.password.check_breaches }
15
+ <%- end -%>
16
+ <%- if strong_passwords? -%>
17
+ validate :password_strength, if: -> { password.present? }
18
+ <%- end -%>
19
+ end
20
+
21
+ def password_set? = password_digest_in_database.present?
22
+ <%- if password_nullable? -%>
23
+
24
+ def errors
25
+ super.tap { |errors| errors.delete(:password, :blank)<%= " if password_may_be_blank?" unless password_blank_rows_all? %> }
26
+ end
27
+ <%- end -%>
28
+ <%- if strong_passwords? -%>
29
+
30
+ private
31
+
32
+ def password_strength
33
+ return if Zxcvbn.test(password).score >= <%= identity.config_reference %>.password.minimum_strength
34
+
35
+ errors.add(:password, :too_weak, message: "is too weak. Try adding length or making it less predictable.")
36
+ end
37
+ <%- end -%>
38
+ end
@@ -0,0 +1,15 @@
1
+ module <%= identity.class_name %>::PasswordRotatable
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ before_save if: :will_save_change_to_password_digest? do
6
+ self.password_changed_at = Time.current
7
+ end
8
+ end
9
+
10
+ def password_fresh?
11
+ max_age = <%= identity.config_reference %>.password.maximum_age
12
+ return true if max_age.nil? || password_changed_at.nil?
13
+ password_changed_at > max_age.ago
14
+ end
15
+ end
@@ -0,0 +1,50 @@
1
+ module <%= identity.class_name %>::PhoneVerifiable
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ normalizes :pending_phone, with: -> { Phonelib.parse(it, <%= identity.config_reference %>.phone.default_country).to_s.presence }
6
+ <%- if phone_encrypted? -%>
7
+ encrypts :pending_phone
8
+ <%- end -%>
9
+ validates :pending_phone, phone: { allow_blank: true, countries: <%= identity.config_reference %>.phone.allowed_countries, format: :e164 }
10
+ <%- unless coy? -%>
11
+ validate { errors.add(:pending_phone, :taken) if pending_phone_taken? }
12
+ <%- end -%>
13
+ <%- if trackable? -%>
14
+
15
+ after_update if: :phone_previously_changed? do
16
+ record_event("<%= identity.singular %>.phone_changed")
17
+ end
18
+ <%- end -%>
19
+ end
20
+
21
+ def stage_phone(number)
22
+ self.pending_phone = number
23
+
24
+ if pending_phone.blank?
25
+ self.phone = nil
26
+ elsif pending_phone == phone
27
+ self.pending_phone = nil
28
+ end
29
+ self
30
+ end
31
+
32
+ def pending_phone_taken?
33
+ pending_phone.present? && <%= identity.class_name %>.exists?(phone: pending_phone)
34
+ end
35
+
36
+ def request_phone_change_verification
37
+ if pending_phone_taken?
38
+ clear_sms_challenge
39
+ else
40
+ issue_sms_challenge(to: pending_phone)
41
+ end
42
+ end
43
+
44
+ def verify_pending_phone
45
+ update(phone: pending_phone, pending_phone: nil)
46
+ rescue ActiveRecord::RecordNotUnique
47
+ errors.add(:phone, :taken)
48
+ false
49
+ end
50
+ end
@@ -0,0 +1,67 @@
1
+ module <%= identity.principals_concern %>
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ <%- principals.each_with_index do |principal, index| -%>
6
+ <%- unless index.zero? -%>
7
+
8
+ <%- end -%>
9
+ normalizes :<%= principal.column %>, with: <%= principal.normalization(config_reference: identity.config_reference) %>
10
+ <%- if principal.channel? && encrypted_pii? -%>
11
+ encrypts :<%= principal.column %>, deterministic: true # .find_by works, sorting, ranges, and %LIKE% will not.
12
+ <%- end -%>
13
+ <%- if principal.type == :email && coy? -%>
14
+ # Do not add a uniqueness validation for email or phone as this would reveal the existence of users. The database index enforces uniqueness.
15
+ <%- end -%>
16
+ <%- if principal.dedicated_validation? -%>
17
+ <%- if guest_excused_from_phone? -%>
18
+ validates :phone, presence: true, unless: :guest?
19
+ <%- elsif principal.required? -%>
20
+ validates :phone, presence: true
21
+ <%- end -%>
22
+ validates :phone, phone: { allow_blank: true, countries: <%= identity.config_reference %>.phone.allowed_countries, format: :e164 }
23
+ <%- else -%>
24
+ validates :<%= principal.column %>, <%= principal.validates_arguments %>
25
+ <%- end -%>
26
+ <%- if principal.type == :username -%>
27
+ validate :username_not_forbidden
28
+ <%- end -%>
29
+ <%- end -%>
30
+ <%- if login_key_may_be_missing? -%>
31
+
32
+ validate :at_least_one_login_key
33
+ <%- end -%>
34
+ <%- if contactable_validation? -%>
35
+
36
+ validate :must_be_contactable
37
+ <%- end -%>
38
+ end
39
+ <%- if principals.username || login_key_may_be_missing? -%>
40
+
41
+ private
42
+ <%- end -%>
43
+ <%- if login_key_may_be_missing? -%>
44
+
45
+ def at_least_one_login_key
46
+ return if <%= login_principals.map { |p| "#{p.column}.present?" }.join(" || ") %>
47
+
48
+ errors.add(:base, "Enter <%= login_keys_article_phrase %>.")
49
+ end
50
+ <%- end -%>
51
+ <%- if contactable_validation? -%>
52
+
53
+ def must_be_contactable
54
+ return if <%= principals.select(&:channel?).map { |p| "#{p.column}.present?" }.join(" || ") %>
55
+
56
+ errors.add(:base, "Add <%= channel_keys_article_phrase %>.")
57
+ end
58
+ <%- end -%>
59
+ <%- if principals.username -%>
60
+
61
+ def username_not_forbidden
62
+ return if username.blank?
63
+
64
+ errors.add(:username, "is forbidden") if <%= identity.config_reference %>.username.forbidden.include?(username.downcase)
65
+ end
66
+ <%- end -%>
67
+ end
@@ -0,0 +1,19 @@
1
+ module <%= identity.class_name %>::Recoverable
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ generates_token_for :password_reset, expires_in: <%= identity.config_reference %>.tokens.password_reset do
6
+ [ password_salt&.last(10), email ]
7
+ end
8
+ end
9
+
10
+ class_methods do
11
+ def request_password_reset(email)
12
+ find_by(email:)&.tap(&:send_password_reset)
13
+ end
14
+ end
15
+
16
+ def send_password_reset
17
+ <%= identity.mailer_class %>.with(signed_id: generate_token_for(:password_reset), to: email).password_reset.deliver_later
18
+ end
19
+ end
@@ -0,0 +1,52 @@
1
+ module <%= identity.class_name %>::SecondFactor
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ has_many :recovery_codes, dependent: :destroy
6
+
7
+ <%- if password? -%>
8
+ generates_token_for :authentication_challenge, expires_in: <%= identity.config_reference %>.tokens.mfa_challenge do
9
+ password_salt
10
+ end
11
+ <%- else -%>
12
+ generates_token_for :authentication_challenge, expires_in: <%= identity.config_reference %>.tokens.mfa_challenge
13
+ <%- end -%>
14
+ <%- if totp? -%>
15
+
16
+ encrypts :totp_secret
17
+
18
+ before_create do
19
+ self.totp_secret = ROTP::Base32.random
20
+ end
21
+ <%- end -%>
22
+ end
23
+
24
+ def second_factor_enrolled?
25
+ <%= second_factor_enrolled_clauses.join(" || ") %>
26
+ end
27
+
28
+ def reset_second_factor!
29
+ transaction do
30
+ <%- if totp? -%>
31
+ update!(totp_enrolled_at: nil, totp_secret: ROTP::Base32.random)
32
+ <%- end -%>
33
+ <%- if sms_second_factor? -%>
34
+ update!(sms_enrolled_at: nil)
35
+ clear_sms_challenge
36
+ <%- end -%>
37
+ <%- if security_keys? -%>
38
+ <%= second_factor_keys_expr %>.destroy_all
39
+ <%- end -%>
40
+ discard_orphaned_recovery_codes!
41
+ <%- if trackable? -%>
42
+ record_event("<%= identity.singular %>.second_factor_reset")
43
+ <%- end -%>
44
+ end
45
+ end
46
+
47
+ # Recovery codes bypass the second factor as a whole, so they outlive any one
48
+ # of them and die with the last.
49
+ def discard_orphaned_recovery_codes!
50
+ recovery_codes.delete_all unless second_factor_enrolled?
51
+ end
52
+ end
@@ -0,0 +1,38 @@
1
+ module <%= identity.class_name %>::SmsChallengeable
2
+ def issue_sms_challenge(to: phone)
3
+ return false if to.blank?
4
+
5
+ code = format("%06d", SecureRandom.random_number(1_000_000))
6
+ Rails.cache.write(sms_challenge_key(:code), { code: code, to: to }, expires_in: <%= identity.config_reference %>.tokens.sms_code)
7
+ Rails.cache.delete(sms_challenge_key(:tries))
8
+
9
+ Sms.deliver_later(to: to, body: "#{code} is your #{<%= identity.config_reference %>.app_name} verification code.")
10
+ true
11
+ end
12
+
13
+ def verify_sms_challenge(submitted, to: phone)
14
+ tries = Rails.cache.increment(sms_challenge_key(:tries), 1, expires_in: <%= identity.config_reference %>.tokens.sms_code)
15
+ challenge = Rails.cache.read(sms_challenge_key(:code))
16
+ return false if tries.nil? || tries > <%= identity.config_reference %>.sms.max_attempts
17
+ return false unless challenge && challenge[:to] == to
18
+ return false unless ActiveSupport::SecurityUtils.secure_compare(challenge[:code], submitted.to_s)
19
+
20
+ clear_sms_challenge
21
+ true
22
+ end
23
+
24
+ def sms_challenge?(to: phone)
25
+ to.present? && Rails.cache.read(sms_challenge_key(:code))&.fetch(:to) == to
26
+ end
27
+
28
+ def clear_sms_challenge
29
+ Rails.cache.delete(sms_challenge_key(:code))
30
+ Rails.cache.delete(sms_challenge_key(:tries))
31
+ end
32
+
33
+ private
34
+
35
+ def sms_challenge_key(kind)
36
+ "sms-#{kind}:#{self.class.name}:#{id}"
37
+ end
38
+ end