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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +384 -0
- data/lib/authnz_eleven/identity.rb +210 -0
- data/lib/authnz_eleven/principal.rb +201 -0
- data/lib/authnz_eleven/principals.rb +111 -0
- data/lib/authnz_eleven/version.rb +5 -0
- data/lib/authnz_eleven/wizard.rb +570 -0
- data/lib/authnz_eleven.rb +15 -0
- data/lib/generators/authnz_eleven/USAGE +26 -0
- data/lib/generators/authnz_eleven/authnz_eleven_generator.rb +3040 -0
- data/lib/generators/authnz_eleven/templates/config/forbidden_usernames.LICENSE +22 -0
- data/lib/generators/authnz_eleven/templates/config/forbidden_usernames.txt +558 -0
- data/lib/generators/authnz_eleven/templates/config/initializers/authentication.rb.tt +275 -0
- data/lib/generators/authnz_eleven/templates/config/initializers/omniauth.rb.tt +9 -0
- data/lib/generators/authnz_eleven/templates/config/initializers/team_slug.rb.tt +24 -0
- data/lib/generators/authnz_eleven/templates/config/initializers/webauthn.rb.tt +14 -0
- data/lib/generators/authnz_eleven/templates/config/locales/authentication.en.yml.tt +11 -0
- data/lib/generators/authnz_eleven/templates/controllers/active_teams_controller.rb.tt +6 -0
- data/lib/generators/authnz_eleven/templates/controllers/admin/dashboard_controller.rb.tt +4 -0
- data/lib/generators/authnz_eleven/templates/controllers/admin/sessions_controller.rb.tt +17 -0
- data/lib/generators/authnz_eleven/templates/controllers/admin/users_controller.rb.tt +91 -0
- data/lib/generators/authnz_eleven/templates/controllers/admin_controller.rb.tt +6 -0
- data/lib/generators/authnz_eleven/templates/controllers/base_controller.rb.tt +11 -0
- data/lib/generators/authnz_eleven/templates/controllers/concerns/api_token_authentication.rb.tt +21 -0
- data/lib/generators/authnz_eleven/templates/controllers/concerns/authentication/easy_dev_login.rb.tt +47 -0
- data/lib/generators/authnz_eleven/templates/controllers/concerns/authentication/registering.rb.tt +25 -0
- data/lib/generators/authnz_eleven/templates/controllers/concerns/authentication/sudoable.rb.tt +117 -0
- data/lib/generators/authnz_eleven/templates/controllers/concerns/authentication/webauthn_ceremony.rb.tt +51 -0
- data/lib/generators/authnz_eleven/templates/controllers/concerns/authentication.rb.tt +421 -0
- data/lib/generators/authnz_eleven/templates/controllers/concerns/authorization.rb.tt +48 -0
- data/lib/generators/authnz_eleven/templates/controllers/concerns/captchable.rb.tt +11 -0
- data/lib/generators/authnz_eleven/templates/controllers/impersonations_controller.rb.tt +8 -0
- data/lib/generators/authnz_eleven/templates/controllers/invitations_controller.rb.tt +65 -0
- data/lib/generators/authnz_eleven/templates/controllers/settings/api_tokens_controller.rb.tt +54 -0
- data/lib/generators/authnz_eleven/templates/controllers/settings/authentications/events_controller.rb.tt +5 -0
- data/lib/generators/authnz_eleven/templates/controllers/settings/dashboard_controller.rb.tt +4 -0
- data/lib/generators/authnz_eleven/templates/controllers/settings/email_verifications_controller.rb.tt +15 -0
- data/lib/generators/authnz_eleven/templates/controllers/settings/emails_controller.rb.tt +93 -0
- data/lib/generators/authnz_eleven/templates/controllers/settings/invitations_controller.rb.tt +61 -0
- data/lib/generators/authnz_eleven/templates/controllers/settings/multi_factor_authentication/authenticators_controller.rb.tt +87 -0
- data/lib/generators/authnz_eleven/templates/controllers/settings/multi_factor_authentication/recovery_codes_controller.rb.tt +27 -0
- data/lib/generators/authnz_eleven/templates/controllers/settings/multi_factor_authentication/security_keys_controller.rb.tt +86 -0
- data/lib/generators/authnz_eleven/templates/controllers/settings/multi_factor_authentication/sms_controller.rb.tt +84 -0
- data/lib/generators/authnz_eleven/templates/controllers/settings/omniauth_identities_controller.rb.tt +27 -0
- data/lib/generators/authnz_eleven/templates/controllers/settings/passkeys_controller.rb.tt +70 -0
- data/lib/generators/authnz_eleven/templates/controllers/settings/passwords_controller.rb.tt +74 -0
- data/lib/generators/authnz_eleven/templates/controllers/settings/phone_verifications_controller.rb.tt +39 -0
- data/lib/generators/authnz_eleven/templates/controllers/settings/phones_controller.rb.tt +88 -0
- data/lib/generators/authnz_eleven/templates/controllers/settings/sessions_controller.rb.tt +29 -0
- data/lib/generators/authnz_eleven/templates/controllers/settings/users_controller.rb.tt +42 -0
- data/lib/generators/authnz_eleven/templates/controllers/settings_controller.rb.tt +5 -0
- data/lib/generators/authnz_eleven/templates/controllers/teams_controller.rb.tt +38 -0
- data/lib/generators/authnz_eleven/templates/controllers/users/credential_enrollments/passkeys_controller.rb.tt +36 -0
- data/lib/generators/authnz_eleven/templates/controllers/users/credential_enrollments/passwords_controller.rb.tt +28 -0
- data/lib/generators/authnz_eleven/templates/controllers/users/credential_enrollments_controller.rb.tt +14 -0
- data/lib/generators/authnz_eleven/templates/controllers/users/easy_dev_logins_controller.rb.tt +19 -0
- data/lib/generators/authnz_eleven/templates/controllers/users/email_verifications_controller.rb.tt +36 -0
- data/lib/generators/authnz_eleven/templates/controllers/users/magic_link_controller.rb.tt +53 -0
- data/lib/generators/authnz_eleven/templates/controllers/users/multi_factor_authentication/challenge/recovery_codes_controller.rb.tt +30 -0
- data/lib/generators/authnz_eleven/templates/controllers/users/multi_factor_authentication/challenge/security_keys_controller.rb.tt +34 -0
- data/lib/generators/authnz_eleven/templates/controllers/users/multi_factor_authentication/challenge/sms_controller.rb.tt +45 -0
- data/lib/generators/authnz_eleven/templates/controllers/users/multi_factor_authentication/challenge/totps_controller.rb.tt +42 -0
- data/lib/generators/authnz_eleven/templates/controllers/users/omniauth/registrations_controller.rb.tt +26 -0
- data/lib/generators/authnz_eleven/templates/controllers/users/omniauth_controller.rb.tt +139 -0
- data/lib/generators/authnz_eleven/templates/controllers/users/passkeys/sessions_controller.rb.tt +18 -0
- data/lib/generators/authnz_eleven/templates/controllers/users/password_resets_controller.rb.tt +64 -0
- data/lib/generators/authnz_eleven/templates/controllers/users/registrations/email_verifications_controller.rb.tt +53 -0
- data/lib/generators/authnz_eleven/templates/controllers/users/registrations/phone_verifications_controller.rb.tt +37 -0
- data/lib/generators/authnz_eleven/templates/controllers/users/registrations_controller.rb.tt +112 -0
- data/lib/generators/authnz_eleven/templates/controllers/users/sessions_controller.rb.tt +52 -0
- data/lib/generators/authnz_eleven/templates/controllers/users/sms_sessions_controller.rb.tt +53 -0
- data/lib/generators/authnz_eleven/templates/controllers/users/sudos_controller.rb.tt +42 -0
- data/lib/generators/authnz_eleven/templates/erb/admin/dashboard/index.html.erb.tt +9 -0
- data/lib/generators/authnz_eleven/templates/erb/admin/sessions/index.html.erb.tt +27 -0
- data/lib/generators/authnz_eleven/templates/erb/admin/users/index.html.erb.tt +32 -0
- data/lib/generators/authnz_eleven/templates/erb/admin/users/show.html.erb.tt +70 -0
- data/lib/generators/authnz_eleven/templates/erb/invitations/show.html.erb.tt +9 -0
- data/lib/generators/authnz_eleven/templates/erb/settings/api_tokens/create.html.erb.tt +7 -0
- data/lib/generators/authnz_eleven/templates/erb/settings/api_tokens/index.html.erb.tt +29 -0
- data/lib/generators/authnz_eleven/templates/erb/settings/authentications/events/index.html.erb.tt +18 -0
- data/lib/generators/authnz_eleven/templates/erb/settings/dashboard/index.html.erb.tt +75 -0
- data/lib/generators/authnz_eleven/templates/erb/settings/emails/edit.html.erb.tt +68 -0
- data/lib/generators/authnz_eleven/templates/erb/settings/invitations/new.html.erb.tt +31 -0
- data/lib/generators/authnz_eleven/templates/erb/settings/multi_factor_authentication/authenticators/new.html.erb.tt +57 -0
- data/lib/generators/authnz_eleven/templates/erb/settings/multi_factor_authentication/recovery_codes/create.html.erb.tt +14 -0
- data/lib/generators/authnz_eleven/templates/erb/settings/multi_factor_authentication/recovery_codes/index.html.erb.tt +13 -0
- data/lib/generators/authnz_eleven/templates/erb/settings/multi_factor_authentication/security_keys/edit.html.erb.tt +14 -0
- data/lib/generators/authnz_eleven/templates/erb/settings/multi_factor_authentication/security_keys/index.html.erb.tt +20 -0
- data/lib/generators/authnz_eleven/templates/erb/settings/multi_factor_authentication/security_keys/new.html.erb.tt +17 -0
- data/lib/generators/authnz_eleven/templates/erb/settings/multi_factor_authentication/sms/new.html.erb.tt +35 -0
- data/lib/generators/authnz_eleven/templates/erb/settings/omniauth_identities/index.html.erb.tt +28 -0
- data/lib/generators/authnz_eleven/templates/erb/settings/passkeys/edit.html.erb.tt +14 -0
- data/lib/generators/authnz_eleven/templates/erb/settings/passkeys/index.html.erb.tt +21 -0
- data/lib/generators/authnz_eleven/templates/erb/settings/passkeys/new.html.erb.tt +13 -0
- data/lib/generators/authnz_eleven/templates/erb/settings/passwords/edit.html.erb.tt +68 -0
- data/lib/generators/authnz_eleven/templates/erb/settings/phone_verifications/new.html.erb.tt +22 -0
- data/lib/generators/authnz_eleven/templates/erb/settings/phones/edit.html.erb.tt +58 -0
- data/lib/generators/authnz_eleven/templates/erb/settings/sessions/index.html.erb.tt +29 -0
- data/lib/generators/authnz_eleven/templates/erb/settings/users/show.html.erb.tt +28 -0
- data/lib/generators/authnz_eleven/templates/erb/shared/_authnz_eleven_nav.html.erb.tt +24 -0
- data/lib/generators/authnz_eleven/templates/erb/shared/_authnz_eleven_sudo_challenge.html.erb.tt +43 -0
- data/lib/generators/authnz_eleven/templates/erb/shared/_captcha.html.erb.tt +8 -0
- data/lib/generators/authnz_eleven/templates/erb/teams/index.html.erb.tt +34 -0
- data/lib/generators/authnz_eleven/templates/erb/teams/new.html.erb.tt +22 -0
- data/lib/generators/authnz_eleven/templates/erb/teams/show.html.erb.tt +5 -0
- data/lib/generators/authnz_eleven/templates/erb/user_mailer/email_change_verification.html.erb.tt +7 -0
- data/lib/generators/authnz_eleven/templates/erb/user_mailer/invitation.html.erb.tt +11 -0
- data/lib/generators/authnz_eleven/templates/erb/user_mailer/magic_link.html.erb.tt +5 -0
- data/lib/generators/authnz_eleven/templates/erb/user_mailer/password_reset.html.erb.tt +6 -0
- data/lib/generators/authnz_eleven/templates/erb/user_mailer/registration_email_verification.html.erb.tt +7 -0
- data/lib/generators/authnz_eleven/templates/erb/user_mailer/security_notification.html.erb.tt +15 -0
- data/lib/generators/authnz_eleven/templates/erb/users/credential_enrollments/passkeys/new.html.erb.tt +24 -0
- data/lib/generators/authnz_eleven/templates/erb/users/credential_enrollments/passwords/new.html.erb.tt +21 -0
- data/lib/generators/authnz_eleven/templates/erb/users/credential_enrollments/show.html.erb.tt +21 -0
- data/lib/generators/authnz_eleven/templates/erb/users/email_verifications/show.html.erb.tt +9 -0
- data/lib/generators/authnz_eleven/templates/erb/users/magic_link/edit.html.erb.tt +9 -0
- data/lib/generators/authnz_eleven/templates/erb/users/magic_link/new.html.erb.tt +22 -0
- data/lib/generators/authnz_eleven/templates/erb/users/multi_factor_authentication/challenge/recovery_codes/new.html.erb.tt +20 -0
- data/lib/generators/authnz_eleven/templates/erb/users/multi_factor_authentication/challenge/security_keys/new.html.erb.tt +17 -0
- data/lib/generators/authnz_eleven/templates/erb/users/multi_factor_authentication/challenge/sms/new.html.erb.tt +25 -0
- data/lib/generators/authnz_eleven/templates/erb/users/multi_factor_authentication/challenge/totps/new.html.erb.tt +21 -0
- data/lib/generators/authnz_eleven/templates/erb/users/omniauth/registrations/new.html.erb.tt +28 -0
- data/lib/generators/authnz_eleven/templates/erb/users/password_resets/edit.html.erb.tt +26 -0
- data/lib/generators/authnz_eleven/templates/erb/users/password_resets/new.html.erb.tt +20 -0
- data/lib/generators/authnz_eleven/templates/erb/users/registrations/email_verifications/new.html.erb.tt +11 -0
- data/lib/generators/authnz_eleven/templates/erb/users/registrations/email_verifications/show.html.erb.tt +34 -0
- data/lib/generators/authnz_eleven/templates/erb/users/registrations/new.html.erb.tt +87 -0
- data/lib/generators/authnz_eleven/templates/erb/users/registrations/phone_verifications/new.html.erb.tt +46 -0
- data/lib/generators/authnz_eleven/templates/erb/users/sessions/new.html.erb.tt +88 -0
- data/lib/generators/authnz_eleven/templates/erb/users/sms_sessions/edit.html.erb.tt +30 -0
- data/lib/generators/authnz_eleven/templates/erb/users/sms_sessions/new.html.erb.tt +22 -0
- data/lib/generators/authnz_eleven/templates/erb/users/sudos/new.html.erb.tt +9 -0
- data/lib/generators/authnz_eleven/templates/javascript/controllers/webauthn_controller.js.tt +142 -0
- data/lib/generators/authnz_eleven/templates/jobs/authnz_eleven_deferred_job.rb.tt +9 -0
- data/lib/generators/authnz_eleven/templates/jobs/authnz_eleven_mail_delivery_job.rb.tt +3 -0
- data/lib/generators/authnz_eleven/templates/jobs/sms_delivery_job.rb.tt +11 -0
- data/lib/generators/authnz_eleven/templates/lib/captcha/null.rb.tt +9 -0
- data/lib/generators/authnz_eleven/templates/lib/captcha/turnstile.rb.tt +39 -0
- data/lib/generators/authnz_eleven/templates/lib/captcha.rb.tt +15 -0
- data/lib/generators/authnz_eleven/templates/lib/sms/log.rb.tt +8 -0
- data/lib/generators/authnz_eleven/templates/lib/sms/twilio.rb.tt +70 -0
- data/lib/generators/authnz_eleven/templates/lib/sms.rb.tt +15 -0
- data/lib/generators/authnz_eleven/templates/mailers/user_mailer.rb.tt +98 -0
- data/lib/generators/authnz_eleven/templates/migrations/add_guest_to_users_migration.rb.tt +6 -0
- data/lib/generators/authnz_eleven/templates/migrations/create_api_tokens_migration.rb.tt +14 -0
- data/lib/generators/authnz_eleven/templates/migrations/create_events_migration.rb.tt +26 -0
- data/lib/generators/authnz_eleven/templates/migrations/create_invitations_migration.rb.tt +20 -0
- data/lib/generators/authnz_eleven/templates/migrations/create_memberships_migration.rb.tt +12 -0
- data/lib/generators/authnz_eleven/templates/migrations/create_omniauth_identities_migration.rb.tt +16 -0
- data/lib/generators/authnz_eleven/templates/migrations/create_password_histories_migration.rb.tt +11 -0
- data/lib/generators/authnz_eleven/templates/migrations/create_pending_registrations_migration.rb.tt +31 -0
- data/lib/generators/authnz_eleven/templates/migrations/create_recovery_codes_migration.rb.tt +11 -0
- data/lib/generators/authnz_eleven/templates/migrations/create_sessions_migration.rb.tt +19 -0
- data/lib/generators/authnz_eleven/templates/migrations/create_teams_migration.rb.tt +9 -0
- data/lib/generators/authnz_eleven/templates/migrations/create_users_migration.rb.tt +70 -0
- data/lib/generators/authnz_eleven/templates/migrations/create_webauthn_credentials_migration.rb.tt +16 -0
- data/lib/generators/authnz_eleven/templates/models/api_token.rb.tt +24 -0
- data/lib/generators/authnz_eleven/templates/models/concerns/adminable.rb.tt +15 -0
- data/lib/generators/authnz_eleven/templates/models/concerns/authenticatable.rb.tt +151 -0
- data/lib/generators/authnz_eleven/templates/models/concerns/bannable.rb.tt +24 -0
- data/lib/generators/authnz_eleven/templates/models/concerns/deadboltable.rb.tt +39 -0
- data/lib/generators/authnz_eleven/templates/models/concerns/email_verifiable.rb.tt +52 -0
- data/lib/generators/authnz_eleven/templates/models/concerns/eventable.rb.tt +22 -0
- data/lib/generators/authnz_eleven/templates/models/concerns/guestable.rb.tt +30 -0
- data/lib/generators/authnz_eleven/templates/models/concerns/invitable.rb.tt +12 -0
- data/lib/generators/authnz_eleven/templates/models/concerns/magic_linkable.rb.tt +34 -0
- data/lib/generators/authnz_eleven/templates/models/concerns/password_historical.rb.tt +28 -0
- data/lib/generators/authnz_eleven/templates/models/concerns/password_policy.rb.tt +38 -0
- data/lib/generators/authnz_eleven/templates/models/concerns/password_rotatable.rb.tt +15 -0
- data/lib/generators/authnz_eleven/templates/models/concerns/phone_verifiable.rb.tt +50 -0
- data/lib/generators/authnz_eleven/templates/models/concerns/principals.rb.tt +67 -0
- data/lib/generators/authnz_eleven/templates/models/concerns/recoverable.rb.tt +19 -0
- data/lib/generators/authnz_eleven/templates/models/concerns/second_factor.rb.tt +52 -0
- data/lib/generators/authnz_eleven/templates/models/concerns/sms_challengeable.rb.tt +38 -0
- data/lib/generators/authnz_eleven/templates/models/concerns/team_member.rb.tt +20 -0
- data/lib/generators/authnz_eleven/templates/models/current.rb.tt +28 -0
- data/lib/generators/authnz_eleven/templates/models/event.rb.tt +32 -0
- data/lib/generators/authnz_eleven/templates/models/invitation.rb.tt +122 -0
- data/lib/generators/authnz_eleven/templates/models/membership.rb.tt +16 -0
- data/lib/generators/authnz_eleven/templates/models/omniauth_identity.rb.tt +12 -0
- data/lib/generators/authnz_eleven/templates/models/password_history.rb.tt +3 -0
- data/lib/generators/authnz_eleven/templates/models/pending_registration.rb.tt +216 -0
- data/lib/generators/authnz_eleven/templates/models/recovery_code.rb.tt +41 -0
- data/lib/generators/authnz_eleven/templates/models/session.rb.tt +46 -0
- data/lib/generators/authnz_eleven/templates/models/team.rb.tt +12 -0
- data/lib/generators/authnz_eleven/templates/models/user.rb.tt +59 -0
- data/lib/generators/authnz_eleven/templates/models/webauthn_credential.rb.tt +12 -0
- data/lib/generators/authnz_eleven/templates/tasks/authnz_eleven.rake.tt +41 -0
- data/lib/generators/authnz_eleven/templates/test_unit/controllers/account_existence_parity_test.rb.tt +158 -0
- data/lib/generators/authnz_eleven/templates/test_unit/controllers/admin/access_test.rb.tt +139 -0
- data/lib/generators/authnz_eleven/templates/test_unit/controllers/admin/multi_factor_authentication_test.rb.tt +65 -0
- data/lib/generators/authnz_eleven/templates/test_unit/controllers/api_token_authentication_test.rb.tt +45 -0
- data/lib/generators/authnz_eleven/templates/test_unit/controllers/credential_enrollment_test.rb.tt +84 -0
- data/lib/generators/authnz_eleven/templates/test_unit/controllers/guest_access_test.rb.tt +113 -0
- data/lib/generators/authnz_eleven/templates/test_unit/controllers/invitations_controller_test.rb.tt +230 -0
- data/lib/generators/authnz_eleven/templates/test_unit/controllers/passkey_ceremony_test.rb.tt +126 -0
- data/lib/generators/authnz_eleven/templates/test_unit/controllers/password_rotation_test.rb.tt +67 -0
- data/lib/generators/authnz_eleven/templates/test_unit/controllers/security_key_ceremony_test.rb.tt +218 -0
- data/lib/generators/authnz_eleven/templates/test_unit/controllers/security_notifications_test.rb.tt +39 -0
- data/lib/generators/authnz_eleven/templates/test_unit/controllers/session_lifecycle_test.rb.tt +102 -0
- data/lib/generators/authnz_eleven/templates/test_unit/controllers/settings/api_tokens_controller_test.rb.tt +61 -0
- data/lib/generators/authnz_eleven/templates/test_unit/controllers/settings/authentications/events_controller_test.rb.tt +17 -0
- data/lib/generators/authnz_eleven/templates/test_unit/controllers/settings/email_verifications_controller_test.rb.tt +39 -0
- data/lib/generators/authnz_eleven/templates/test_unit/controllers/settings/invitations_controller_test.rb.tt +157 -0
- data/lib/generators/authnz_eleven/templates/test_unit/controllers/settings/management_test.rb.tt +256 -0
- data/lib/generators/authnz_eleven/templates/test_unit/controllers/settings/multi_factor_authentication/management_test.rb.tt +222 -0
- data/lib/generators/authnz_eleven/templates/test_unit/controllers/settings/omniauth_identities_controller_test.rb.tt +57 -0
- data/lib/generators/authnz_eleven/templates/test_unit/controllers/settings/phone_verifications_controller_test.rb.tt +146 -0
- data/lib/generators/authnz_eleven/templates/test_unit/controllers/teams_controller_test.rb.tt +83 -0
- data/lib/generators/authnz_eleven/templates/test_unit/controllers/users/email_verifications_controller_test.rb.tt +74 -0
- data/lib/generators/authnz_eleven/templates/test_unit/controllers/users/magic_link_controller_test.rb.tt +90 -0
- data/lib/generators/authnz_eleven/templates/test_unit/controllers/users/multi_factor_authentication/challenge_test.rb.tt +143 -0
- data/lib/generators/authnz_eleven/templates/test_unit/controllers/users/omniauth/registrations_controller_test.rb.tt +138 -0
- data/lib/generators/authnz_eleven/templates/test_unit/controllers/users/omniauth_controller_test.rb.tt +97 -0
- data/lib/generators/authnz_eleven/templates/test_unit/controllers/users/password_resets_controller_test.rb.tt +58 -0
- data/lib/generators/authnz_eleven/templates/test_unit/controllers/users/registrations/email_verifications_controller_test.rb.tt +102 -0
- data/lib/generators/authnz_eleven/templates/test_unit/controllers/users/registrations/phone_verifications_controller_test.rb.tt +69 -0
- data/lib/generators/authnz_eleven/templates/test_unit/controllers/users/registrations_controller_test.rb.tt +180 -0
- data/lib/generators/authnz_eleven/templates/test_unit/controllers/users/sessions_controller_test.rb.tt +124 -0
- data/lib/generators/authnz_eleven/templates/test_unit/controllers/users/sms_sessions_controller_test.rb.tt +111 -0
- data/lib/generators/authnz_eleven/templates/test_unit/controllers/users/sudos_controller_test.rb.tt +110 -0
- data/lib/generators/authnz_eleven/templates/test_unit/fixtures/memberships.yml.tt +13 -0
- data/lib/generators/authnz_eleven/templates/test_unit/fixtures/teams.yml.tt +2 -0
- data/lib/generators/authnz_eleven/templates/test_unit/fixtures/users.yml.tt +68 -0
- data/lib/generators/authnz_eleven/templates/test_unit/mailers/user_mailer_test.rb.tt +88 -0
- data/lib/generators/authnz_eleven/templates/test_unit/models/api_token_test.rb.tt +38 -0
- data/lib/generators/authnz_eleven/templates/test_unit/models/password_history_test.rb.tt +51 -0
- data/lib/generators/authnz_eleven/templates/test_unit/models/session_test.rb.tt +16 -0
- data/lib/generators/authnz_eleven/templates/test_unit/models/sms_challengeable_test.rb.tt +108 -0
- data/lib/generators/authnz_eleven/templates/test_unit/models/user/guestable_test.rb.tt +78 -0
- data/lib/generators/authnz_eleven/templates/test_unit/models/user/recovery_codes_test.rb.tt +32 -0
- data/lib/generators/authnz_eleven/templates/test_unit/models/user/sign_in_methods_test.rb.tt +56 -0
- data/lib/generators/authnz_eleven/templates/test_unit/models/user_test.rb.tt +145 -0
- data/lib/generators/authnz_eleven/wizard_generator.rb +52 -0
- 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
|